Merge branch 'master' of github.com:Start9Labs/start-os into rebase/integration/refactors

This commit is contained in:
Matt Hill
2023-07-26 10:48:45 -06:00
89 changed files with 3972 additions and 2851 deletions

View File

@@ -8,7 +8,7 @@ on:
type: choice
description: Environment
options:
- "<NONE>"
- NONE
- dev
- unstable
- dev-unstable
@@ -18,6 +18,16 @@ on:
options:
- standard
- fast
platform:
type: choice
description: Platform
options:
- ALL
- x86_64
- x86_64-nonfree
- aarch64
- aarch64-nonfree
- raspberrypi
push:
branches:
- master
@@ -29,7 +39,7 @@ on:
env:
NODEJS_VERSION: "18.15.0"
ENVIRONMENT: '${{ fromJson(format(''["{0}", ""]'', github.event.inputs.environment || ''dev''))[github.event.inputs.environment == ''<NONE>''] }}'
ENVIRONMENT: '${{ fromJson(format(''["{0}", ""]'', github.event.inputs.environment || ''dev''))[github.event.inputs.environment == ''NONE''] }}'
jobs:
all:
@@ -37,27 +47,41 @@ jobs:
strategy:
fail-fast: false
matrix:
platform:
[x86_64, x86_64-nonfree, aarch64, aarch64-nonfree, raspberrypi]
platform: >-
${{
fromJson(
format(
'[
["{0}"],
["x86_64", "x86_64-nonfree", "aarch64", "aarch64-nonfree", "raspberrypi"]
]',
github.event.inputs.platform || 'ALL'
)
)[(github.event.inputs.platform || 'ALL') == 'ALL']
}}
runs-on: >-
${{
fromJson(
format(
'["ubuntu-22.04", "{0}"]',
fromJson('{
"x86_64": "buildjet-32vcpu-ubuntu-2204",
"x86_64-nonfree": "buildjet-32vcpu-ubuntu-2204",
"aarch64": "buildjet-8vcpu-ubuntu-2204-arm",
"aarch64-nonfree": "buildjet-8vcpu-ubuntu-2204-arm",
"raspberrypi": "buildjet-16vcpu-ubuntu-2204-arm",
}')[matrix.platform]
"x86_64": ["buildjet-32vcpu-ubuntu-2204", "buildjet-32vcpu-ubuntu-2204"],
"x86_64-nonfree": ["buildjet-32vcpu-ubuntu-2204", "buildjet-32vcpu-ubuntu-2204"],
"aarch64": ["buildjet-16vcpu-ubuntu-2204-arm", "buildjet-32vcpu-ubuntu-2204-arm"],
"aarch64-nonfree": ["buildjet-16vcpu-ubuntu-2204-arm", "buildjet-32vcpu-ubuntu-2204-arm"],
"raspberrypi": ["buildjet-16vcpu-ubuntu-2204-arm", "buildjet-32vcpu-ubuntu-2204-arm"],
}')[matrix.platform][github.event.inputs.platform == matrix.platform]
)
)[github.event.inputs.runner == 'fast']
}}
steps:
- name: Free space
run: df -h && rm -rf /opt/hostedtoolcache* && df -h
if: ${{ github.event.inputs.runner != 'fast' }}
- run: |
sudo mount -t tmpfs tmpfs .
if: ${{ github.event.inputs.runner == 'fast' && (matrix.platform == 'x86_64' || matrix.platform == 'x86_64-nonfree') }}
if: ${{ github.event.inputs.runner == 'fast' && (matrix.platform == 'x86_64' || matrix.platform == 'x86_64-nonfree' || github.event.inputs.platform == matrix.platform) }}
- uses: actions/checkout@v3
with:
@@ -72,8 +96,7 @@ jobs:
- run: |
cp -r debian embassyos-0.3.x/
VERSION=0.3.x ./control.sh
cp embassyos-0.3.x/backend/embassyd.service embassyos-0.3.x/debian/embassyos.embassyd.service
cp embassyos-0.3.x/backend/embassy-init.service embassyos-0.3.x/debian/embassyos.embassy-init.service
cp embassyos-0.3.x/backend/startd.service embassyos-0.3.x/debian/embassyos.startd.service
working-directory: embassy-os-deb
- uses: actions/setup-node@v3
@@ -84,7 +107,7 @@ jobs:
id: npm-cache-dir
run: |
echo "dir=$(npm config get cache)" >> $GITHUB_OUTPUT
- uses: buildjet/cache@v3
- uses: actions/cache@v3
id: npm-cache
with:
path: ${{ steps.npm-cache-dir.outputs.dir }}
@@ -134,7 +157,7 @@ jobs:
- run: sudo mount -t tmpfs tmpfs /var/tmp/debspawn
if: ${{ github.event.inputs.runner == 'fast' && (matrix.platform == 'x86_64' || matrix.platform == 'x86_64-nonfree') }}
- uses: buildjet/cache@v3
- uses: actions/cache@v3
with:
path: /var/lib/debspawn
key: ${{ runner.os }}-${{ matrix.platform }}-debspawn-init
@@ -143,7 +166,7 @@ jobs:
- run: "mv embassy-os-deb/embassyos_0.3.x-1_*.deb startos-image-recipes/overlays/deb/"
- run: "rm -rf embassy-os-deb"
- run: "rm -rf embassy-os-deb ${{ steps.npm-cache-dir.outputs.dir }} $HOME/.cargo"
- name: Run iso build
working-directory: startos-image-recipes

View File

@@ -3,20 +3,20 @@ ARCH := $(shell if [ "$(OS_ARCH)" = "raspberrypi" ]; then echo aarch64; else ech
ENVIRONMENT_FILE = $(shell ./check-environment.sh)
GIT_HASH_FILE = $(shell ./check-git-hash.sh)
VERSION_FILE = $(shell ./check-version.sh)
EMBASSY_BINS := backend/target/$(ARCH)-unknown-linux-gnu/release/embassyd backend/target/$(ARCH)-unknown-linux-gnu/release/embassy-init backend/target/$(ARCH)-unknown-linux-gnu/release/embassy-cli backend/target/$(ARCH)-unknown-linux-gnu/release/embassy-sdk backend/target/$(ARCH)-unknown-linux-gnu/release/avahi-alias libs/target/aarch64-unknown-linux-musl/release/embassy_container_init libs/target/x86_64-unknown-linux-musl/release/embassy_container_init
EMBASSY_UIS := frontend/dist/ui frontend/dist/setup-wizard frontend/dist/diagnostic-ui frontend/dist/install-wizard
EMBASSY_BINS := backend/target/$(ARCH)-unknown-linux-gnu/release/startbox libs/target/aarch64-unknown-linux-musl/release/embassy_container_init libs/target/x86_64-unknown-linux-musl/release/embassy_container_init
EMBASSY_UIS := frontend/dist/raw/ui frontend/dist/raw/setup-wizard frontend/dist/raw/diagnostic-ui frontend/dist/raw/install-wizard
BUILD_SRC := $(shell find build)
EMBASSY_SRC := backend/embassyd.service backend/embassy-init.service $(EMBASSY_UIS) $(BUILD_SRC)
EMBASSY_SRC := backend/startd.service $(BUILD_SRC)
COMPAT_SRC := $(shell find system-images/compat/ -not -path 'system-images/compat/target/*' -and -not -name *.tar -and -not -name target)
UTILS_SRC := $(shell find system-images/utils/ -not -name *.tar)
BINFMT_SRC := $(shell find system-images/binfmt/ -not -name *.tar)
BACKEND_SRC := $(shell find backend/src) $(shell find backend/migrations) $(shell find patch-db/*/src) $(shell find libs/*/src) libs/*/Cargo.toml backend/Cargo.toml backend/Cargo.lock
BACKEND_SRC := $(shell find backend/src) $(shell find backend/migrations) $(shell find patch-db/*/src) $(shell find libs/*/src) libs/*/Cargo.toml backend/Cargo.toml backend/Cargo.lock frontend/dist/static
FRONTEND_SHARED_SRC := $(shell find frontend/projects/shared) $(shell ls -p frontend/ | grep -v / | sed 's/^/frontend\//g') frontend/package.json frontend/node_modules frontend/config.json patch-db/client/dist frontend/patchdb-ui-seed.json
FRONTEND_UI_SRC := $(shell find frontend/projects/ui)
FRONTEND_SETUP_WIZARD_SRC := $(shell find frontend/projects/setup-wizard)
FRONTEND_DIAGNOSTIC_UI_SRC := $(shell find frontend/projects/diagnostic-ui)
FRONTEND_INSTALL_WIZARD_SRC := $(shell find frontend/projects/install-wizard)
PATCH_DB_CLIENT_SRC := $(shell find patch-db/client -not -path patch-db/client/dist)
PATCH_DB_CLIENT_SRC := $(shell find patch-db/client -not -path patch-db/client/dist -and -not -path patch-db/client/node_modules)
GZIP_BIN := $(shell which pigz || which gzip)
ALL_TARGETS := $(EMBASSY_BINS) system-images/compat/docker-images/$(ARCH).tar system-images/utils/docker-images/$(ARCH).tar system-images/binfmt/docker-images/$(ARCH).tar $(EMBASSY_SRC) $(shell if [ "$(OS_ARCH)" = "raspberrypi" ]; then echo cargo-deps/aarch64-unknown-linux-gnu/release/pi-beep; fi) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE) $(VERSION_FILE)
@@ -24,9 +24,11 @@ ifeq ($(REMOTE),)
mkdir = mkdir -p $1
rm = rm -rf $1
cp = cp -r $1 $2
ln = ln -sf $1 $2
else
mkdir = ssh $(REMOTE) 'mkdir -p $1'
rm = ssh $(REMOTE) 'sudo rm -rf $1'
ln = ssh $(REMOTE) 'sudo ln -sf $1 $2'
define cp
tar --transform "s|^$1|x|" -czv -f- $1 | ssh $(REMOTE) "sudo tar --transform 's|^x|$2|' -xzv -f- -C /"
endef
@@ -71,10 +73,12 @@ startos_raspberrypi.img: $(BUILD_SRC) startos.raspberrypi.squashfs $(VERSION_FIL
# For creating os images. DO NOT USE
install: $(ALL_TARGETS)
$(call mkdir,$(DESTDIR)/usr/bin)
$(call cp,backend/target/$(ARCH)-unknown-linux-gnu/release/embassy-init,$(DESTDIR)/usr/bin/embassy-init)
$(call cp,backend/target/$(ARCH)-unknown-linux-gnu/release/embassyd,$(DESTDIR)/usr/bin/embassyd)
$(call cp,backend/target/$(ARCH)-unknown-linux-gnu/release/embassy-cli,$(DESTDIR)/usr/bin/embassy-cli)
$(call cp,backend/target/$(ARCH)-unknown-linux-gnu/release/avahi-alias,$(DESTDIR)/usr/bin/avahi-alias)
$(call cp,backend/target/$(ARCH)-unknown-linux-gnu/release/startbox,$(DESTDIR)/usr/bin/startbox)
$(call ln,/usr/bin/startbox,$(DESTDIR)/usr/bin/startd)
$(call ln,/usr/bin/startbox,$(DESTDIR)/usr/bin/start-cli)
$(call ln,/usr/bin/startbox,$(DESTDIR)/usr/bin/start-sdk)
$(call ln,/usr/bin/startbox,$(DESTDIR)/usr/bin/avahi-alias)
$(call ln,/usr/bin/startbox,$(DESTDIR)/usr/bin/embassy-cli)
if [ "$(OS_ARCH)" = "raspberrypi" ]; then $(call cp,cargo-deps/aarch64-unknown-linux-gnu/release/pi-beep,$(DESTDIR)/usr/bin/pi-beep); fi
$(call mkdir,$(DESTDIR)/usr/lib)
@@ -94,22 +98,14 @@ install: $(ALL_TARGETS)
$(call cp,system-images/utils/docker-images/$(ARCH).tar,$(DESTDIR)/usr/lib/embassy/system-images/utils.tar)
$(call cp,system-images/binfmt/docker-images/$(ARCH).tar,$(DESTDIR)/usr/lib/embassy/system-images/binfmt.tar)
$(call mkdir,$(DESTDIR)/var/www/html)
$(call cp,frontend/dist/diagnostic-ui,$(DESTDIR)/var/www/html/diagnostic)
$(call cp,frontend/dist/setup-wizard,$(DESTDIR)/var/www/html/setup)
$(call cp,frontend/dist/install-wizard,$(DESTDIR)/var/www/html/install)
$(call cp,frontend/dist/ui,$(DESTDIR)/var/www/html/main)
$(call cp,index.html,$(DESTDIR)/var/www/html/index.html)
update-overlay:
@echo "\033[33m!!! THIS WILL ONLY REFLASH YOUR DEVICE IN MEMORY !!!\033[0m"
@echo "\033[33mALL CHANGES WILL BE REVERTED IF YOU RESTART THE DEVICE\033[0m"
@if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi
@if [ "`ssh $(REMOTE) 'cat /usr/lib/embassy/VERSION.txt'`" != "`cat ./VERSION.txt`" ]; then >&2 echo "Embassy requires migrations: update-overlay is unavailable." && false; fi
@if ssh $(REMOTE) "pidof embassy-init"; then >&2 echo "Embassy in INIT: update-overlay is unavailable." && false; fi
ssh $(REMOTE) "sudo systemctl stop embassyd"
@if [ "`ssh $(REMOTE) 'cat /usr/lib/embassy/VERSION.txt'`" != "`cat ./VERSION.txt`" ]; then >&2 echo "StartOS requires migrations: update-overlay is unavailable." && false; fi
ssh $(REMOTE) "sudo systemctl stop startd"
$(MAKE) install REMOTE=$(REMOTE) OS_ARCH=$(OS_ARCH)
ssh $(REMOTE) "sudo systemctl start embassyd"
ssh $(REMOTE) "sudo systemctl start startd"
update:
@if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi
@@ -132,11 +128,6 @@ system-images/utils/docker-images/aarch64.tar system-images/utils/docker-images/
system-images/binfmt/docker-images/aarch64.tar system-images/binfmt/docker-images/x86_64.tar: $(BINFMT_SRC) | sudo
cd system-images/binfmt && make
raspios.img:
wget --continue https://downloads.raspberrypi.org/raspios_lite_arm64/images/raspios_lite_arm64-2022-01-28/2022-01-28-raspios-bullseye-arm64-lite.zip
unzip 2022-01-28-raspios-bullseye-arm64-lite.zip
mv 2022-01-28-raspios-bullseye-arm64-lite.img raspios.img
snapshots: libs/snapshot_creator/Cargo.toml
cd libs/ && ./build-v8-snapshot.sh
cd libs/ && ./build-arm-v8-snapshot.sh
@@ -148,18 +139,21 @@ $(EMBASSY_BINS): $(BACKEND_SRC) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE) frontend/pa
frontend/node_modules: frontend/package.json
npm --prefix frontend ci
frontend/dist/ui: $(FRONTEND_UI_SRC) $(FRONTEND_SHARED_SRC) $(ENVIRONMENT_FILE)
frontend/dist/raw/ui: $(FRONTEND_UI_SRC) $(FRONTEND_SHARED_SRC) $(ENVIRONMENT_FILE)
npm --prefix frontend run build:ui
frontend/dist/setup-wizard: $(FRONTEND_SETUP_WIZARD_SRC) $(FRONTEND_SHARED_SRC) $(ENVIRONMENT_FILE)
frontend/dist/raw/setup-wizard: $(FRONTEND_SETUP_WIZARD_SRC) $(FRONTEND_SHARED_SRC) $(ENVIRONMENT_FILE)
npm --prefix frontend run build:setup
frontend/dist/diagnostic-ui: $(FRONTEND_DIAGNOSTIC_UI_SRC) $(FRONTEND_SHARED_SRC) $(ENVIRONMENT_FILE)
frontend/dist/raw/diagnostic-ui: $(FRONTEND_DIAGNOSTIC_UI_SRC) $(FRONTEND_SHARED_SRC) $(ENVIRONMENT_FILE)
npm --prefix frontend run build:dui
frontend/dist/install-wizard: $(FRONTEND_INSTALL_WIZARD_SRC) $(FRONTEND_SHARED_SRC) $(ENVIRONMENT_FILE)
frontend/dist/raw/install-wizard: $(FRONTEND_INSTALL_WIZARD_SRC) $(FRONTEND_SHARED_SRC) $(ENVIRONMENT_FILE)
npm --prefix frontend run build:install-wiz
frontend/dist/static: $(EMBASSY_UIS)
./compress-uis.sh
frontend/config.json: $(GIT_HASH_FILE) frontend/config-sample.json
jq '.useMocks = false' frontend/config-sample.json > frontend/config.json
jq '.packageArch = "$(ARCH)"' frontend/config.json > frontend/config.json.tmp
@@ -186,13 +180,10 @@ backend-$(ARCH).tar: $(EMBASSY_BINS)
frontends: $(EMBASSY_UIS)
# this is a convenience step to build the UI
ui: frontend/dist/ui
ui: frontend/dist/raw/ui
# used by github actions
backend: $(EMBASSY_BINS)
cargo-deps/aarch64-unknown-linux-gnu/release/nc-broadcast: | sudo
ARCH=$(ARCH) ./build-cargo-dep.sh nc-broadcast
cargo-deps/aarch64-unknown-linux-gnu/release/pi-beep: | sudo
ARCH=$(ARCH) ./build-cargo-dep.sh pi-beep
ARCH=aarch64 ./build-cargo-dep.sh pi-beep

View File

@@ -32,6 +32,8 @@ To pursue this option, follow one of our [DIY guides](https://start9.com/latest/
## :heart: Contributing
There are multiple ways to contribute: work directly on StartOS, package a service for the marketplace, or help with documentation and guides. To learn more about contributing, see [here](https://start9.com/contribute/).
To report security issues, please email our security team - security@start9.com.
## UI Screenshots
<p align="center">
<img src="assets/StartOS.png" alt="StartOS" width="85%">

2440
backend/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
[package]
authors = ["Aiden McClelland <me@drbonez.dev>"]
description = "The core of StartOS"
documentation = "https://docs.rs/embassy-os"
documentation = "https://docs.rs/start-os"
edition = "2021"
keywords = [
"self-hosted",
@@ -11,40 +11,28 @@ keywords = [
"full-node",
"lightning",
]
name = "embassy-os"
name = "start-os"
readme = "README.md"
repository = "https://github.com/Start9Labs/start-os"
version = "0.3.4-rev.3"
version = "0.3.4-rev.4"
[lib]
name = "embassy"
name = "startos"
path = "src/lib.rs"
[[bin]]
name = "embassyd"
path = "src/bin/embassyd.rs"
[[bin]]
name = "embassy-init"
path = "src/bin/embassy-init.rs"
[[bin]]
name = "embassy-sdk"
path = "src/bin/embassy-sdk.rs"
[[bin]]
name = "embassy-cli"
path = "src/bin/embassy-cli.rs"
[[bin]]
name = "avahi-alias"
path = "src/bin/avahi-alias.rs"
name = "startbox"
path = "src/main.rs"
[features]
avahi = ["avahi-sys"]
default = ["avahi", "js_engine"]
default = ["avahi-alias", "cli", "sdk", "daemon", "js_engine"]
dev = []
unstable = ["patch-db/unstable"]
avahi-alias = ["avahi"]
cli = []
sdk = []
daemon = []
[dependencies]
aes = { version = "0.7.5", features = ["ctr"] }
@@ -95,11 +83,14 @@ id-pool = { version = "0.2.2", features = [
"serde",
], default-features = false }
imbl = "2.0.0"
include_dir = "0.7.3"
indexmap = { version = "1.9.1", features = ["serde"] }
ipnet = { version = "2.7.1", features = ["serde"] }
iprange = { version = "0.6.7", features = ["serde"] }
isocountry = "0.3.2"
itertools = "0.10.3"
jaq-core = "0.10.0"
jaq-std = "0.10.0"
josekit = "0.8.1"
js_engine = { path = '../libs/js_engine', optional = true }
jsonpath_lib = "0.3.0"
@@ -108,6 +99,7 @@ libc = "0.2.126"
log = "0.4.17"
mbrman = "0.5.0"
models = { version = "*", path = "../libs/models" }
new_mime_guess = "4"
nix = "0.25.0"
nom = "7.1.1"
num = "0.4.0"

View File

@@ -12,18 +12,17 @@
## Structure
The StartOS backend is broken up into 4 different binaries:
The StartOS backend is packed into a single binary `startbox` that is symlinked under
several different names for different behaviour:
- embassyd: This is the main workhorse of StartOS - any new functionality you
- startd: This is the main workhorse of StartOS - any new functionality you
want will likely go here
- embassy-init: This is the component responsible for allowing you to set up
your device, and handles system initialization on startup
- embassy-cli: This is a CLI tool that will allow you to issue commands to
embassyd and control it similarly to the UI
- embassy-sdk: This is a CLI tool that aids in building and packaging services
- start-cli: This is a CLI tool that will allow you to issue commands to
startd and control it similarly to the UI
- start-sdk: This is a CLI tool that aids in building and packaging services
you wish to deploy to StartOS
Finally there is a library `embassy` that supports all four of these tools.
Finally there is a library `startos` that supports all of these tools.
See [here](/backend/Cargo.toml) for details.

View File

@@ -1,24 +0,0 @@
#!/bin/bash
set -e
shopt -s expand_aliases
if [ "$0" != "./build-dev.sh" ]; then
>&2 echo "Must be run from backend directory"
exit 1
fi
USE_TTY=
if tty -s; then
USE_TTY="-it"
fi
alias 'rust-arm64-builder'='docker run $USE_TTY --rm -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$(pwd)":/home/rust/src start9/rust-arm-cross:aarch64'
cd ..
rust-arm64-builder sh -c "(cd backend && cargo build --locked)"
cd backend
sudo chown -R $USER target
sudo chown -R $USER ~/.cargo
#rust-arm64-builder aarch64-linux-gnu-strip target/aarch64-unknown-linux-gnu/release/embassyd

View File

@@ -1,23 +0,0 @@
#!/bin/bash
set -e
shopt -s expand_aliases
if [ "$0" != "./build-portable-dev.sh" ]; then
>&2 echo "Must be run from backend directory"
exit 1
fi
USE_TTY=
if tty -s; then
USE_TTY="-it"
fi
alias 'rust-musl-builder'='docker run $USE_TTY --rm -v "$HOME"/.cargo/registry:/root/.cargo/registry -v "$(pwd)":/home/rust/src start9/rust-musl-cross:x86_64-musl'
cd ..
rust-musl-builder sh -c "(cd backend && cargo +beta build --target=x86_64-unknown-linux-musl --no-default-features --locked)"
cd backend
sudo chown -R $USER target
sudo chown -R $USER ~/.cargo

View File

@@ -72,5 +72,3 @@ sudo chown -R $USER ../libs/target
if [ -n "$fail" ]; then
exit 1
fi
#rust-arm64-builder aarch64-linux-gnu-strip target/aarch64-unknown-linux-gnu/release/embassyd

View File

@@ -1,15 +0,0 @@
[Unit]
Description=Embassy Init
After=network-online.target
Requires=network-online.target
Wants=avahi-daemon.service
[Service]
Type=oneshot
Environment=RUST_LOG=embassy_init=debug,embassy=debug,js_engine=debug,patch_db=warn
ExecStart=/usr/bin/embassy-init
RemainAfterExit=true
StandardOutput=append:/var/log/embassy-init.log
[Install]
WantedBy=embassyd.service

View File

@@ -1,17 +0,0 @@
[Unit]
Description=Embassy Daemon
After=embassy-init.service
Requires=embassy-init.service
[Service]
Type=simple
Environment=RUST_LOG=embassyd=debug,embassy=debug,js_engine=debug,patch_db=warn
ExecStart=/usr/bin/embassyd
Restart=always
RestartSec=3
ManagedOOMPreference=avoid
CPUAccounting=true
CPUWeight=1000
[Install]
WantedBy=multi-user.target

View File

@@ -9,7 +9,10 @@ if [ "$0" != "./install-sdk.sh" ]; then
fi
if [ -z "$OS_ARCH" ]; then
OS_ARCH=$(uname -m)
export OS_ARCH=$(uname -m)
fi
cargo install --bin=embassy-sdk --bin=embassy-cli --path=. --no-default-features --features=js_engine --locked
cargo install --path=. --no-default-features --features=js_engine,sdk,cli --locked
startbox_loc=$(which startbox)
ln -sf $startbox_loc $(dirname $startbox_loc)/start-cli
ln -sf $startbox_loc $(dirname $startbox_loc)/start-sdk

View File

@@ -334,7 +334,7 @@ async fn perform_backup<Db: DbHandle>(
}
let luks_folder = Path::new("/media/embassy/config/luks");
if tokio::fs::metadata(&luks_folder).await.is_ok() {
dir_copy(&luks_folder, &luks_folder_bak).await?;
dir_copy(&luks_folder, &luks_folder_bak, None).await?;
}
let timestamp = Some(Utc::now());

View File

@@ -109,7 +109,7 @@ async fn approximate_progress(
if tokio::fs::metadata(&dir).await.is_err() {
*size = 0;
} else {
*size = dir_size(&dir).await?;
*size = dir_size(&dir, None).await?;
}
}
Ok(())
@@ -285,7 +285,7 @@ async fn restore_packages(
progress_info.package_installs.insert(id.clone(), progress);
progress_info
.src_volume_size
.insert(id.clone(), dir_size(backup_dir(&id)).await?);
.insert(id.clone(), dir_size(backup_dir(&id), None).await?);
progress_info.target_volume_size.insert(id.clone(), 0);
let package_id = id.clone();
tasks.push(

View File

@@ -14,7 +14,7 @@ fn log_str_error(action: &str, e: i32) {
}
}
fn main() {
pub fn main() {
let aliases: Vec<_> = std::env::args().skip(1).collect();
unsafe {
let simple_poll = avahi_sys::avahi_simple_poll_new();

View File

@@ -0,0 +1,9 @@
pub fn renamed(old: &str, new: &str) -> ! {
eprintln!("{old} has been renamed to {new}");
std::process::exit(1)
}
pub fn removed(name: &str) -> ! {
eprintln!("{name} has been removed");
std::process::exit(1)
}

55
backend/src/bins/mod.rs Normal file
View File

@@ -0,0 +1,55 @@
use std::path::Path;
#[cfg(feature = "avahi-alias")]
pub mod avahi_alias;
pub mod deprecated;
#[cfg(feature = "cli")]
pub mod start_cli;
#[cfg(feature = "daemon")]
pub mod start_init;
#[cfg(feature = "sdk")]
pub mod start_sdk;
#[cfg(feature = "daemon")]
pub mod startd;
fn select_executable(name: &str) -> Option<fn()> {
match name {
#[cfg(feature = "avahi-alias")]
"avahi-alias" => Some(avahi_alias::main),
#[cfg(feature = "cli")]
"start-cli" => Some(start_cli::main),
#[cfg(feature = "sdk")]
"start-sdk" => Some(start_sdk::main),
#[cfg(feature = "daemon")]
"startd" => Some(startd::main),
"embassy-cli" => Some(|| deprecated::renamed("embassy-cli", "start-cli")),
"embassy-sdk" => Some(|| deprecated::renamed("embassy-sdk", "start-sdk")),
"embassyd" => Some(|| deprecated::renamed("embassyd", "startd")),
"embassy-init" => Some(|| deprecated::removed("embassy-init")),
_ => None,
}
}
pub fn startbox() {
let args = std::env::args().take(2).collect::<Vec<_>>();
if let Some(x) = args
.get(0)
.and_then(|s| Path::new(&*s).file_name())
.and_then(|s| s.to_str())
.and_then(|s| select_executable(&s))
{
x()
} else if let Some(x) = args.get(1).and_then(|s| select_executable(&s)) {
x()
} else {
eprintln!(
"unknown executable: {}",
args.get(0)
.filter(|x| &**x != "startbox")
.or_else(|| args.get(1))
.map(|s| s.as_str())
.unwrap_or("N/A")
);
std::process::exit(1);
}
}

View File

@@ -1,21 +1,22 @@
use clap::Arg;
use embassy::context::CliContext;
use embassy::util::logger::EmbassyLogger;
use embassy::version::{Current, VersionT};
use embassy::Error;
use rpc_toolkit::run_cli;
use rpc_toolkit::yajrc::RpcError;
use serde_json::Value;
use crate::context::CliContext;
use crate::util::logger::EmbassyLogger;
use crate::version::{Current, VersionT};
use crate::Error;
lazy_static::lazy_static! {
static ref VERSION_STRING: String = Current::new().semver().to_string();
}
fn inner_main() -> Result<(), Error> {
run_cli!({
command: embassy::main_api,
command: crate::main_api,
app: app => app
.name("Embassy CLI")
.name("StartOS CLI")
.version(&**VERSION_STRING)
.arg(
clap::Arg::with_name("config")
@@ -48,7 +49,7 @@ fn inner_main() -> Result<(), Error> {
Ok(())
}
fn main() {
pub fn main() {
match inner_main() {
Ok(_) => (),
Err(e) => {

View File

@@ -3,21 +3,22 @@ use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;
use embassy::context::rpc::RpcContextConfig;
use embassy::context::{DiagnosticContext, InstallContext, SetupContext};
use embassy::disk::fsck::RepairStrategy;
use embassy::disk::main::DEFAULT_PASSWORD;
use embassy::disk::REPAIR_DISK_PATH;
use embassy::init::STANDBY_MODE_PATH;
use embassy::net::web_server::WebServer;
use embassy::shutdown::Shutdown;
use embassy::sound::CHIME;
use embassy::util::logger::EmbassyLogger;
use embassy::util::Invoke;
use embassy::{Error, ErrorKind, ResultExt, OS_ARCH};
use tokio::process::Command;
use tracing::instrument;
use crate::context::rpc::RpcContextConfig;
use crate::context::{DiagnosticContext, InstallContext, SetupContext};
use crate::disk::fsck::RepairStrategy;
use crate::disk::main::DEFAULT_PASSWORD;
use crate::disk::REPAIR_DISK_PATH;
use crate::init::STANDBY_MODE_PATH;
use crate::net::web_server::WebServer;
use crate::shutdown::Shutdown;
use crate::sound::CHIME;
use crate::util::logger::EmbassyLogger;
use crate::util::Invoke;
use crate::{Error, ErrorKind, ResultExt, OS_ARCH};
#[instrument(skip_all)]
async fn setup_or_init(cfg_path: Option<PathBuf>) -> Result<(), Error> {
Command::new("ln")
@@ -78,7 +79,7 @@ async fn setup_or_init(cfg_path: Option<PathBuf>) -> Result<(), Error> {
server.shutdown().await;
Command::new("reboot")
.invoke(embassy::ErrorKind::Unknown)
.invoke(crate::ErrorKind::Unknown)
.await?;
} else if tokio::fs::metadata("/media/embassy/config/disk.guid")
.await
@@ -116,7 +117,7 @@ async fn setup_or_init(cfg_path: Option<PathBuf>) -> Result<(), Error> {
let guid_string = tokio::fs::read_to_string("/media/embassy/config/disk.guid") // unique identifier for volume group - keeps track of the disk that goes with your embassy
.await?;
let guid = guid_string.trim();
let requires_reboot = embassy::disk::main::import(
let requires_reboot = crate::disk::main::import(
guid,
cfg.datadir(),
if tokio::fs::metadata(REPAIR_DISK_PATH).await.is_ok() {
@@ -124,22 +125,26 @@ async fn setup_or_init(cfg_path: Option<PathBuf>) -> Result<(), Error> {
} else {
RepairStrategy::Preen
},
DEFAULT_PASSWORD,
if guid.ends_with("_UNENC") {
None
} else {
Some(DEFAULT_PASSWORD)
},
)
.await?;
if tokio::fs::metadata(REPAIR_DISK_PATH).await.is_ok() {
tokio::fs::remove_file(REPAIR_DISK_PATH)
.await
.with_ctx(|_| (embassy::ErrorKind::Filesystem, REPAIR_DISK_PATH))?;
.with_ctx(|_| (crate::ErrorKind::Filesystem, REPAIR_DISK_PATH))?;
}
if requires_reboot.0 {
embassy::disk::main::export(guid, cfg.datadir()).await?;
crate::disk::main::export(guid, cfg.datadir()).await?;
Command::new("reboot")
.invoke(embassy::ErrorKind::Unknown)
.invoke(crate::ErrorKind::Unknown)
.await?;
}
tracing::info!("Loaded Disk");
embassy::init::init(&cfg).await?;
crate::init::init(&cfg).await?;
}
Ok(())
@@ -168,11 +173,11 @@ async fn inner_main(cfg_path: Option<PathBuf>) -> Result<Option<Shutdown>, Error
if OS_ARCH == "raspberrypi" && tokio::fs::metadata(STANDBY_MODE_PATH).await.is_ok() {
tokio::fs::remove_file(STANDBY_MODE_PATH).await?;
Command::new("sync").invoke(ErrorKind::Filesystem).await?;
embassy::sound::SHUTDOWN.play().await?;
crate::sound::SHUTDOWN.play().await?;
futures::future::pending::<()>().await;
}
embassy::sound::BEP.play().await?;
crate::sound::BEP.play().await?;
run_script_if_exists("/media/embassy/config/preinit.sh").await;
@@ -180,7 +185,7 @@ async fn inner_main(cfg_path: Option<PathBuf>) -> Result<Option<Shutdown>, Error
async move {
tracing::error!("{}", e.source);
tracing::debug!("{}", e.source);
embassy::sound::BEETHOVEN.play().await?;
crate::sound::BEETHOVEN.play().await?;
let ctx = DiagnosticContext::init(
cfg_path,
@@ -223,8 +228,8 @@ async fn inner_main(cfg_path: Option<PathBuf>) -> Result<Option<Shutdown>, Error
res
}
fn main() {
let matches = clap::App::new("embassy-init")
pub fn main() {
let matches = clap::App::new("start-init")
.arg(
clap::Arg::with_name("config")
.short('c')
@@ -233,8 +238,6 @@ fn main() {
)
.get_matches();
EmbassyLogger::init();
let cfg_path = matches.value_of("config").map(|p| Path::new(p).to_owned());
let res = {
let rt = tokio::runtime::Builder::new_multi_thread()

View File

@@ -1,20 +1,21 @@
use embassy::context::SdkContext;
use embassy::util::logger::EmbassyLogger;
use embassy::version::{Current, VersionT};
use embassy::Error;
use rpc_toolkit::run_cli;
use rpc_toolkit::yajrc::RpcError;
use serde_json::Value;
use crate::context::SdkContext;
use crate::util::logger::EmbassyLogger;
use crate::version::{Current, VersionT};
use crate::Error;
lazy_static::lazy_static! {
static ref VERSION_STRING: String = Current::new().semver().to_string();
}
fn inner_main() -> Result<(), Error> {
run_cli!({
command: embassy::portable_api,
command: crate::portable_api,
app: app => app
.name("Embassy SDK")
.name("StartOS SDK")
.version(&**VERSION_STRING)
.arg(
clap::Arg::with_name("config")
@@ -47,7 +48,7 @@ fn inner_main() -> Result<(), Error> {
Ok(())
}
fn main() {
pub fn main() {
match inner_main() {
Ok(_) => (),
Err(e) => {

View File

@@ -3,16 +3,17 @@ use std::path::{Path, PathBuf};
use std::sync::Arc;
use color_eyre::eyre::eyre;
use embassy::context::{DiagnosticContext, RpcContext};
use embassy::net::web_server::WebServer;
use embassy::shutdown::Shutdown;
use embassy::system::launch_metrics_task;
use embassy::util::logger::EmbassyLogger;
use embassy::{Error, ErrorKind, ResultExt};
use futures::{FutureExt, TryFutureExt};
use tokio::signal::unix::signal;
use tracing::instrument;
use crate::context::{DiagnosticContext, RpcContext};
use crate::net::web_server::WebServer;
use crate::shutdown::Shutdown;
use crate::system::launch_metrics_task;
use crate::util::logger::EmbassyLogger;
use crate::{Error, ErrorKind, ResultExt};
#[instrument(skip_all)]
async fn inner_main(cfg_path: Option<PathBuf>) -> Result<Option<Shutdown>, Error> {
let (rpc_ctx, server, shutdown) = {
@@ -26,7 +27,7 @@ async fn inner_main(cfg_path: Option<PathBuf>) -> Result<Option<Shutdown>, Error
),
)
.await?;
embassy::hostname::sync_hostname(&rpc_ctx.account.read().await.hostname).await?;
crate::hostname::sync_hostname(&rpc_ctx.account.read().await.hostname).await?;
let server = WebServer::main(
SocketAddr::new(Ipv6Addr::UNSPECIFIED.into(), 80),
rpc_ctx.clone(),
@@ -71,7 +72,7 @@ async fn inner_main(cfg_path: Option<PathBuf>) -> Result<Option<Shutdown>, Error
.await
});
embassy::sound::CHIME.play().await?;
crate::sound::CHIME.play().await?;
metrics_task
.map_err(|e| {
@@ -100,8 +101,15 @@ async fn inner_main(cfg_path: Option<PathBuf>) -> Result<Option<Shutdown>, Error
Ok(shutdown)
}
fn main() {
let matches = clap::App::new("embassyd")
pub fn main() {
EmbassyLogger::init();
if !Path::new("/run/embassy/initialized").exists() {
super::start_init::main();
std::fs::write("/run/embassy/initialized", "").unwrap();
}
let matches = clap::App::new("startd")
.arg(
clap::Arg::with_name("config")
.short('c')
@@ -110,8 +118,6 @@ fn main() {
)
.get_matches();
EmbassyLogger::init();
let cfg_path = matches.value_of("config").map(|p| Path::new(p).to_owned());
let res = {
@@ -126,7 +132,7 @@ fn main() {
async {
tracing::error!("{}", e.source);
tracing::debug!("{:?}", e.source);
embassy::sound::BEETHOVEN.play().await?;
crate::sound::BEETHOVEN.play().await?;
let ctx = DiagnosticContext::init(
cfg_path,
if tokio::fs::metadata("/media/embassy/config/disk.guid")

View File

@@ -269,6 +269,45 @@ impl RpcContext {
pub async fn cleanup(&self) -> Result<(), Error> {
let mut db = self.db.handle();
let receipts = RpcCleanReceipts::new(&mut db).await?;
let packages = receipts.packages.get(&mut db).await?.0;
let mut current_dependents = packages
.keys()
.map(|k| (k.clone(), BTreeMap::new()))
.collect::<BTreeMap<_, _>>();
for (package_id, package) in packages {
for (k, v) in package
.into_installed()
.into_iter()
.flat_map(|i| i.current_dependencies.0)
{
let mut entry: BTreeMap<_, _> = current_dependents.remove(&k).unwrap_or_default();
entry.insert(package_id.clone(), v);
current_dependents.insert(k, entry);
}
}
for (package_id, current_dependents) in current_dependents {
if let Some(deps) = crate::db::DatabaseModel::new()
.package_data()
.idx_model(&package_id)
.and_then(|pde| pde.installed())
.map::<_, CurrentDependents>(|i| i.current_dependents())
.check(&mut db)
.await?
{
deps.put(&mut db, &CurrentDependents(current_dependents))
.await?;
} else if let Some(deps) = crate::db::DatabaseModel::new()
.package_data()
.idx_model(&package_id)
.and_then(|pde| pde.removing())
.map::<_, CurrentDependents>(|i| i.current_dependents())
.check(&mut db)
.await?
{
deps.put(&mut db, &CurrentDependents(current_dependents))
.await?;
}
}
for (package_id, package) in receipts.packages.get(&mut db).await?.0 {
if let Err(e) = async {
match package {
@@ -338,31 +377,6 @@ impl RpcContext {
tracing::debug!("{:?}", e);
}
}
let mut current_dependents = BTreeMap::new();
for (package_id, package) in receipts.packages.get(&mut db).await?.0 {
for (k, v) in package
.into_installed()
.into_iter()
.flat_map(|i| i.current_dependencies.0)
{
let mut entry: BTreeMap<_, _> = current_dependents.remove(&k).unwrap_or_default();
entry.insert(package_id.clone(), v);
current_dependents.insert(k, entry);
}
}
for (package_id, current_dependents) in current_dependents {
if let Some(deps) = crate::db::DatabaseModel::new()
.package_data()
.idx_model(&package_id)
.and_then(|pde| pde.installed())
.map::<_, CurrentDependents>(|i| i.current_dependents())
.check(&mut db)
.await?
{
deps.put(&mut db, &CurrentDependents(current_dependents))
.await?;
}
}
Ok(())
}

View File

@@ -45,6 +45,8 @@ pub struct SetupContextConfig {
pub migration_batch_rows: Option<usize>,
pub migration_prefetch_rows: Option<usize>,
pub datadir: Option<PathBuf>,
#[serde(default)]
pub disable_encryption: bool,
}
impl SetupContextConfig {
#[instrument(skip_all)]
@@ -75,6 +77,7 @@ pub struct SetupContextSeed {
pub config_path: Option<PathBuf>,
pub migration_batch_rows: usize,
pub migration_prefetch_rows: usize,
pub disable_encryption: bool,
pub shutdown: Sender<()>,
pub datadir: PathBuf,
pub selected_v2_drive: RwLock<Option<PathBuf>>,
@@ -102,6 +105,7 @@ impl SetupContext {
config_path: path.as_ref().map(|p| p.as_ref().to_owned()),
migration_batch_rows: cfg.migration_batch_rows.unwrap_or(25000),
migration_prefetch_rows: cfg.migration_prefetch_rows.unwrap_or(100_000),
disable_encryption: cfg.disable_encryption,
shutdown,
datadir,
selected_v2_drive: RwLock::new(None),

View File

@@ -4,9 +4,10 @@ pub mod package;
use std::future::Future;
use std::sync::Arc;
use color_eyre::eyre::eyre;
use futures::{FutureExt, SinkExt, StreamExt};
use patch_db::json_ptr::JsonPointer;
use patch_db::{Dump, Revision};
use patch_db::{DbHandle, Dump, LockType, Revision};
use rpc_toolkit::command;
use rpc_toolkit::hyper::upgrade::Upgraded;
use rpc_toolkit::hyper::{Body, Error as HyperError, Request, Response};
@@ -24,6 +25,7 @@ use tracing::instrument;
pub use self::model::DatabaseModel;
use crate::context::RpcContext;
use crate::middleware::auth::{HasValidSession, HashSessionToken};
use crate::util::display_none;
use crate::util::serde::{display_serializable, IoFormat};
use crate::{Error, ResultExt};
@@ -163,7 +165,7 @@ pub async fn subscribe(ctx: RpcContext, req: Request<Body>) -> Result<Response<B
Ok(res)
}
#[command(subcommands(revisions, dump, put))]
#[command(subcommands(revisions, dump, put, apply))]
pub fn db() -> Result<(), RpcError> {
Ok(())
}
@@ -199,6 +201,85 @@ pub async fn dump(
Ok(ctx.db.dump().await?)
}
fn apply_expr(input: jaq_core::Val, expr: &str) -> Result<jaq_core::Val, Error> {
let (expr, errs) = jaq_core::parse::parse(expr, jaq_core::parse::main());
let Some(expr) = expr else {
return Err(Error::new(
eyre!("Failed to parse expression: {:?}", errs),
crate::ErrorKind::InvalidRequest,
));
};
let mut errs = Vec::new();
let mut defs = jaq_core::Definitions::core();
for def in jaq_std::std() {
defs.insert(def, &mut errs);
}
let filter = defs.finish(expr, Vec::new(), &mut errs);
if !errs.is_empty() {
return Err(Error::new(
eyre!("Failed to compile expression: {:?}", errs),
crate::ErrorKind::InvalidRequest,
));
};
let inputs = jaq_core::RcIter::new(std::iter::empty());
let mut res_iter = filter.run(jaq_core::Ctx::new([], &inputs), input);
let Some(res) = res_iter
.next()
.transpose()
.map_err(|e| eyre!("{e}"))
.with_kind(crate::ErrorKind::Deserialization)?
else {
return Err(Error::new(
eyre!("expr returned no results"),
crate::ErrorKind::InvalidRequest,
));
};
if res_iter.next().is_some() {
return Err(Error::new(
eyre!("expr returned too many results"),
crate::ErrorKind::InvalidRequest,
));
}
Ok(res)
}
#[command(display(display_none))]
pub async fn apply(#[context] ctx: RpcContext, #[arg] expr: String) -> Result<(), Error> {
let mut db = ctx.db.handle();
DatabaseModel::new().lock(&mut db, LockType::Write).await?;
let root_ptr = JsonPointer::<String>::default();
let input = db.get_value(&root_ptr, None).await?;
let res = (|| {
let res = apply_expr(input.into(), &expr)?;
serde_json::from_value::<model::Database>(res.clone().into()).with_ctx(|_| {
(
crate::ErrorKind::Deserialization,
"result does not match database model",
)
})?;
Ok::<serde_json::Value, Error>(res.into())
})()?;
db.put_value(&root_ptr, &res).await?;
Ok(())
}
#[command(subcommands(ui))]
pub fn put() -> Result<(), RpcError> {
Ok(())

View File

@@ -51,7 +51,7 @@ impl Database {
last_wifi_region: None,
eos_version_compat: Current::new().compat().clone(),
lan_address,
tor_address: format!("http://{}", account.key.tor_address())
tor_address: format!("https://{}", account.key.tor_address())
.parse()
.unwrap(),
ip_info: BTreeMap::new(),
@@ -110,6 +110,7 @@ pub struct ServerInfo {
pub lan_address: Url,
pub tor_address: Url,
#[model]
#[serde(default)]
pub ip_info: BTreeMap<String, IpInfo>,
#[model]
#[serde(default)]

View File

@@ -9,11 +9,10 @@ use crate::disk::repair;
use crate::init::SYSTEM_REBUILD_PATH;
use crate::logs::{fetch_logs, LogResponse, LogSource};
use crate::shutdown::Shutdown;
use crate::system::SYSTEMD_UNIT;
use crate::util::display_none;
use crate::Error;
pub const SYSTEMD_UNIT: &'static str = "embassy-init";
#[command(subcommands(error, logs, exit, restart, forget_disk, disk, rebuild))]
pub fn diagnostic() -> Result<(), Error> {
Ok(())

View File

@@ -13,7 +13,7 @@ use crate::disk::mount::util::unmount;
use crate::util::Invoke;
use crate::{Error, ErrorKind, ResultExt};
pub const PASSWORD_PATH: &'static str = "/etc/embassy/password";
pub const PASSWORD_PATH: &'static str = "/run/embassy/password";
pub const DEFAULT_PASSWORD: &'static str = "password";
pub const MAIN_FS_SIZE: FsSize = FsSize::Gigabytes(8);
@@ -22,13 +22,13 @@ pub async fn create<I, P>(
disks: &I,
pvscan: &BTreeMap<PathBuf, Option<String>>,
datadir: impl AsRef<Path>,
password: &str,
password: Option<&str>,
) -> Result<String, Error>
where
for<'a> &'a I: IntoIterator<Item = &'a P>,
P: AsRef<Path>,
{
let guid = create_pool(disks, pvscan).await?;
let guid = create_pool(disks, pvscan, password.is_some()).await?;
create_all_fs(&guid, &datadir, password).await?;
export(&guid, datadir).await?;
Ok(guid)
@@ -38,6 +38,7 @@ where
pub async fn create_pool<I, P>(
disks: &I,
pvscan: &BTreeMap<PathBuf, Option<String>>,
encrypted: bool,
) -> Result<String, Error>
where
for<'a> &'a I: IntoIterator<Item = &'a P>,
@@ -62,13 +63,16 @@ where
.invoke(crate::ErrorKind::DiskManagement)
.await?;
}
let guid = format!(
let mut guid = format!(
"EMBASSY_{}",
base32::encode(
base32::Alphabet::RFC4648 { padding: false },
&rand::random::<[u8; 32]>(),
)
);
if !encrypted {
guid += "_UNENC";
}
let mut cmd = Command::new("vgcreate");
cmd.arg("-y").arg(&guid);
for disk in disks {
@@ -90,11 +94,8 @@ pub async fn create_fs<P: AsRef<Path>>(
datadir: P,
name: &str,
size: FsSize,
password: &str,
password: Option<&str>,
) -> Result<(), Error> {
tokio::fs::write(PASSWORD_PATH, password)
.await
.with_ctx(|_| (crate::ErrorKind::Filesystem, PASSWORD_PATH))?;
let mut cmd = Command::new("lvcreate");
match size {
FsSize::Gigabytes(a) => cmd.arg("-L").arg(format!("{}G", a)),
@@ -106,37 +107,41 @@ pub async fn create_fs<P: AsRef<Path>>(
.arg(guid)
.invoke(crate::ErrorKind::DiskManagement)
.await?;
let crypt_path = Path::new("/dev").join(guid).join(name);
Command::new("cryptsetup")
.arg("-q")
.arg("luksFormat")
.arg(format!("--key-file={}", PASSWORD_PATH))
.arg(format!("--keyfile-size={}", password.len()))
.arg(&crypt_path)
.invoke(crate::ErrorKind::DiskManagement)
.await?;
Command::new("cryptsetup")
.arg("-q")
.arg("luksOpen")
.arg(format!("--key-file={}", PASSWORD_PATH))
.arg(format!("--keyfile-size={}", password.len()))
.arg(&crypt_path)
.arg(format!("{}_{}", guid, name))
.invoke(crate::ErrorKind::DiskManagement)
.await?;
let mut blockdev_path = Path::new("/dev").join(guid).join(name);
if let Some(password) = password {
if let Some(parent) = Path::new(PASSWORD_PATH).parent() {
tokio::fs::create_dir_all(parent).await?;
}
tokio::fs::write(PASSWORD_PATH, password)
.await
.with_ctx(|_| (crate::ErrorKind::Filesystem, PASSWORD_PATH))?;
Command::new("cryptsetup")
.arg("-q")
.arg("luksFormat")
.arg(format!("--key-file={}", PASSWORD_PATH))
.arg(format!("--keyfile-size={}", password.len()))
.arg(&blockdev_path)
.invoke(crate::ErrorKind::DiskManagement)
.await?;
Command::new("cryptsetup")
.arg("-q")
.arg("luksOpen")
.arg(format!("--key-file={}", PASSWORD_PATH))
.arg(format!("--keyfile-size={}", password.len()))
.arg(&blockdev_path)
.arg(format!("{}_{}", guid, name))
.invoke(crate::ErrorKind::DiskManagement)
.await?;
tokio::fs::remove_file(PASSWORD_PATH)
.await
.with_ctx(|_| (crate::ErrorKind::Filesystem, PASSWORD_PATH))?;
blockdev_path = Path::new("/dev/mapper").join(format!("{}_{}", guid, name));
}
Command::new("mkfs.btrfs")
.arg(Path::new("/dev/mapper").join(format!("{}_{}", guid, name)))
.arg(&blockdev_path)
.invoke(crate::ErrorKind::DiskManagement)
.await?;
mount(
Path::new("/dev/mapper").join(format!("{}_{}", guid, name)),
datadir.as_ref().join(name),
ReadWrite,
)
.await?;
tokio::fs::remove_file(PASSWORD_PATH)
.await
.with_ctx(|_| (crate::ErrorKind::Filesystem, PASSWORD_PATH))?;
mount(&blockdev_path, datadir.as_ref().join(name), ReadWrite).await?;
Ok(())
}
@@ -144,7 +149,7 @@ pub async fn create_fs<P: AsRef<Path>>(
pub async fn create_all_fs<P: AsRef<Path>>(
guid: &str,
datadir: P,
password: &str,
password: Option<&str>,
) -> Result<(), Error> {
create_fs(guid, &datadir, "main", MAIN_FS_SIZE, password).await?;
create_fs(
@@ -161,12 +166,14 @@ pub async fn create_all_fs<P: AsRef<Path>>(
#[instrument(skip_all)]
pub async fn unmount_fs<P: AsRef<Path>>(guid: &str, datadir: P, name: &str) -> Result<(), Error> {
unmount(datadir.as_ref().join(name)).await?;
Command::new("cryptsetup")
.arg("-q")
.arg("luksClose")
.arg(format!("{}_{}", guid, name))
.invoke(crate::ErrorKind::DiskManagement)
.await?;
if !guid.ends_with("_UNENC") {
Command::new("cryptsetup")
.arg("-q")
.arg("luksClose")
.arg(format!("{}_{}", guid, name))
.invoke(crate::ErrorKind::DiskManagement)
.await?;
}
Ok(())
}
@@ -203,7 +210,7 @@ pub async fn import<P: AsRef<Path>>(
guid: &str,
datadir: P,
repair: RepairStrategy,
password: &str,
password: Option<&str>,
) -> Result<RequiresReboot, Error> {
let scan = pvscan().await?;
if scan
@@ -261,46 +268,56 @@ pub async fn mount_fs<P: AsRef<Path>>(
datadir: P,
name: &str,
repair: RepairStrategy,
password: &str,
password: Option<&str>,
) -> Result<RequiresReboot, Error> {
tokio::fs::write(PASSWORD_PATH, password)
.await
.with_ctx(|_| (crate::ErrorKind::Filesystem, PASSWORD_PATH))?;
let crypt_path = Path::new("/dev").join(guid).join(name);
let orig_path = Path::new("/dev").join(guid).join(name);
let mut blockdev_path = orig_path.clone();
let full_name = format!("{}_{}", guid, name);
Command::new("cryptsetup")
.arg("-q")
.arg("luksOpen")
.arg(format!("--key-file={}", PASSWORD_PATH))
.arg(format!("--keyfile-size={}", password.len()))
.arg(&crypt_path)
.arg(&full_name)
.invoke(crate::ErrorKind::DiskManagement)
.await?;
let mapper_path = Path::new("/dev/mapper").join(&full_name);
let reboot = repair.fsck(&mapper_path).await?;
// Backup LUKS header if e2fsck succeeded
let luks_folder = Path::new("/media/embassy/config/luks");
tokio::fs::create_dir_all(luks_folder).await?;
let tmp_luks_bak = luks_folder.join(format!(".{full_name}.luks.bak.tmp"));
if tokio::fs::metadata(&tmp_luks_bak).await.is_ok() {
tokio::fs::remove_file(&tmp_luks_bak).await?;
if !guid.ends_with("_UNENC") {
let password = password.unwrap_or(DEFAULT_PASSWORD);
if let Some(parent) = Path::new(PASSWORD_PATH).parent() {
tokio::fs::create_dir_all(parent).await?;
}
tokio::fs::write(PASSWORD_PATH, password)
.await
.with_ctx(|_| (crate::ErrorKind::Filesystem, PASSWORD_PATH))?;
Command::new("cryptsetup")
.arg("-q")
.arg("luksOpen")
.arg(format!("--key-file={}", PASSWORD_PATH))
.arg(format!("--keyfile-size={}", password.len()))
.arg(&blockdev_path)
.arg(&full_name)
.invoke(crate::ErrorKind::DiskManagement)
.await?;
tokio::fs::remove_file(PASSWORD_PATH)
.await
.with_ctx(|_| (crate::ErrorKind::Filesystem, PASSWORD_PATH))?;
blockdev_path = Path::new("/dev/mapper").join(&full_name);
}
let luks_bak = luks_folder.join(format!("{full_name}.luks.bak"));
Command::new("cryptsetup")
.arg("-q")
.arg("luksHeaderBackup")
.arg("--header-backup-file")
.arg(&tmp_luks_bak)
.arg(&crypt_path)
.invoke(crate::ErrorKind::DiskManagement)
.await?;
tokio::fs::rename(&tmp_luks_bak, &luks_bak).await?;
mount(&mapper_path, datadir.as_ref().join(name), ReadWrite).await?;
let reboot = repair.fsck(&blockdev_path).await?;
tokio::fs::remove_file(PASSWORD_PATH)
.await
.with_ctx(|_| (crate::ErrorKind::Filesystem, PASSWORD_PATH))?;
if !guid.ends_with("_UNENC") {
// Backup LUKS header if e2fsck succeeded
let luks_folder = Path::new("/media/embassy/config/luks");
tokio::fs::create_dir_all(luks_folder).await?;
let tmp_luks_bak = luks_folder.join(format!(".{full_name}.luks.bak.tmp"));
if tokio::fs::metadata(&tmp_luks_bak).await.is_ok() {
tokio::fs::remove_file(&tmp_luks_bak).await?;
}
let luks_bak = luks_folder.join(format!("{full_name}.luks.bak"));
Command::new("cryptsetup")
.arg("-q")
.arg("luksHeaderBackup")
.arg("--header-backup-file")
.arg(&tmp_luks_bak)
.arg(&orig_path)
.invoke(crate::ErrorKind::DiskManagement)
.await?;
tokio::fs::rename(&tmp_luks_bak, &luks_bak).await?;
}
mount(&blockdev_path, datadir.as_ref().join(name), ReadWrite).await?;
Ok(reboot)
}
@@ -310,7 +327,7 @@ pub async fn mount_all_fs<P: AsRef<Path>>(
guid: &str,
datadir: P,
repair: RepairStrategy,
password: &str,
password: Option<&str>,
) -> Result<RequiresReboot, Error> {
let mut reboot = RequiresReboot(false);
reboot |= mount_fs(guid, &datadir, "main", repair, password).await?;

View File

@@ -16,6 +16,9 @@ use crate::util::Invoke;
use crate::Error;
async fn resolve_hostname(hostname: &str) -> Result<IpAddr, Error> {
if let Ok(addr) = hostname.parse() {
return Ok(addr);
}
#[cfg(feature = "avahi")]
if hostname.ends_with(".local") {
return Ok(IpAddr::V4(crate::net::mdns::resolve_mdns(hostname).await?));

View File

@@ -324,11 +324,13 @@ pub async fn list(os: &OsPartitionInfo) -> Result<Vec<DiskInfo>, Error> {
if index.internal {
for part in index.parts {
let mut disk_info = disk_info(disk.clone()).await;
disk_info.logicalname = part;
let part_info = part_info(part).await;
disk_info.logicalname = part_info.logicalname.clone();
disk_info.capacity = part_info.capacity;
if let Some(g) = disk_guids.get(&disk_info.logicalname) {
disk_info.guid = g.clone();
} else {
disk_info.partitions = vec![part_info(disk_info.logicalname.clone()).await];
disk_info.partitions = vec![part_info];
}
res.push(disk_info);
}

View File

@@ -17,6 +17,7 @@ pub mod account;
pub mod action;
pub mod auth;
pub mod backup;
pub mod bins;
pub mod config;
pub mod context;
pub mod control;

3
backend/src/main.rs Normal file
View File

@@ -0,0 +1,3 @@
fn main() {
startos::bins::startbox()
}

View File

@@ -39,6 +39,7 @@ use crate::disk::mount::backup::BackupMountGuard;
use crate::disk::mount::guard::TmpMountGuard;
use crate::install::cleanup::remove_from_current_dependents_lists;
use crate::net::net_controller::NetService;
use crate::net::vhost::AlpnInfo;
use crate::procedure::docker::{DockerContainer, DockerProcedure, LongRunning};
use crate::procedure::{NoOutput, ProcedureName};
use crate::s9pk::manifest::Manifest;
@@ -824,8 +825,14 @@ async fn add_network_for_main(
let mut tx = secrets.begin().await?;
for (id, interface) in &seed.manifest.interfaces.0 {
for (external, internal) in interface.lan_config.iter().flatten() {
svc.add_lan(&mut tx, id.clone(), external.0, internal.internal, false)
.await?;
svc.add_lan(
&mut tx,
id.clone(),
external.0,
internal.internal,
Err(AlpnInfo::Specified(vec![])),
)
.await?;
}
for (external, internal) in interface.tor_config.iter().flat_map(|t| &t.port_mapping) {
svc.add_tor(&mut tx, id.clone(), external.0, internal.0)

View File

@@ -17,7 +17,7 @@ use crate::net::keys::Key;
use crate::net::mdns::MdnsController;
use crate::net::ssl::{export_cert, export_key, SslManager};
use crate::net::tor::TorController;
use crate::net::vhost::VHostController;
use crate::net::vhost::{AlpnInfo, VHostController};
use crate::s9pk::manifest::PackageId;
use crate::volume::cert_dir;
use crate::{Error, HOST_IP};
@@ -59,6 +59,8 @@ impl NetController {
}
async fn add_os_bindings(&mut self, hostname: &Hostname, key: &Key) -> Result<(), Error> {
let alpn = Err(AlpnInfo::Specified(vec!["http/1.1".into(), "h2".into()]));
// Internal DNS
self.vhost
.add(
@@ -66,7 +68,7 @@ impl NetController {
Some("embassy".into()),
443,
([127, 0, 0, 1], 80).into(),
false,
alpn.clone(),
)
.await?;
self.os_bindings
@@ -75,7 +77,13 @@ impl NetController {
// LAN IP
self.os_bindings.push(
self.vhost
.add(key.clone(), None, 443, ([127, 0, 0, 1], 80).into(), false)
.add(
key.clone(),
None,
443,
([127, 0, 0, 1], 80).into(),
alpn.clone(),
)
.await?,
);
@@ -87,7 +95,7 @@ impl NetController {
Some("localhost".into()),
443,
([127, 0, 0, 1], 80).into(),
false,
alpn.clone(),
)
.await?,
);
@@ -98,7 +106,7 @@ impl NetController {
Some(hostname.no_dot_host_name()),
443,
([127, 0, 0, 1], 80).into(),
false,
alpn.clone(),
)
.await?,
);
@@ -111,7 +119,7 @@ impl NetController {
Some(hostname.local_domain_name()),
443,
([127, 0, 0, 1], 80).into(),
false,
alpn.clone(),
)
.await?,
);
@@ -131,7 +139,7 @@ impl NetController {
Some(key.tor_address().to_string()),
443,
([127, 0, 0, 1], 80).into(),
false,
alpn.clone(),
)
.await?,
);
@@ -184,7 +192,7 @@ impl NetController {
key: Key,
external: u16,
target: SocketAddr,
connect_ssl: bool,
connect_ssl: Result<(), AlpnInfo>,
) -> Result<Vec<Arc<()>>, Error> {
let mut rcs = Vec::with_capacity(2);
rcs.push(
@@ -278,8 +286,8 @@ impl NetService {
id: InterfaceId,
external: u16,
internal: u16,
connect_ssl: bool,
) -> Result<String, Error>
connect_ssl: Result<(), AlpnInfo>,
) -> Result<(), Error>
where
for<'a> &'a mut Ex: PgExecutor<'a>,
{

View File

@@ -1,16 +1,19 @@
use std::borrow::Cow;
use std::fs::Metadata;
use std::path::Path;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::UNIX_EPOCH;
use async_compression::tokio::bufread::{BrotliEncoder, GzipEncoder};
use async_compression::tokio::bufread::GzipEncoder;
use color_eyre::eyre::eyre;
use digest::Digest;
use futures::FutureExt;
use http::header::{ACCEPT_ENCODING, CONTENT_ENCODING};
use http::header::ACCEPT_ENCODING;
use http::request::Parts as RequestParts;
use http::response::Builder;
use hyper::{Body, Method, Request, Response, StatusCode};
use include_dir::{include_dir, Dir};
use new_mime_guess::MimeGuess;
use openssl::hash::MessageDigest;
use openssl::x509::X509;
use rpc_toolkit::rpc_handler;
@@ -33,10 +36,7 @@ static NOT_FOUND: &[u8] = b"Not Found";
static METHOD_NOT_ALLOWED: &[u8] = b"Method Not Allowed";
static NOT_AUTHORIZED: &[u8] = b"Not Authorized";
pub const MAIN_UI_WWW_DIR: &str = "/var/www/html/main";
pub const SETUP_UI_WWW_DIR: &str = "/var/www/html/setup";
pub const DIAG_UI_WWW_DIR: &str = "/var/www/html/diagnostic";
pub const INSTALL_UI_WWW_DIR: &str = "/var/www/html/install";
static EMBEDDED_UIS: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../frontend/dist/static");
fn status_fn(_: i32) -> StatusCode {
StatusCode::OK
@@ -50,6 +50,17 @@ pub enum UiMode {
Main,
}
impl UiMode {
fn path(&self, path: &str) -> PathBuf {
match self {
Self::Setup => Path::new("setup-wizard").join(path),
Self::Diag => Path::new("diagnostic-ui").join(path),
Self::Install => Path::new("install-wizard").join(path),
Self::Main => Path::new("ui").join(path),
}
}
}
pub async fn setup_ui_file_router(ctx: SetupContext) -> Result<HttpHandler, Error> {
let handler: HttpHandler = Arc::new(move |req| {
let ctx = ctx.clone();
@@ -224,13 +235,6 @@ pub async fn main_ui_server_router(ctx: RpcContext) -> Result<HttpHandler, Error
}
async fn alt_ui(req: Request<Body>, ui_mode: UiMode) -> Result<Response<Body>, Error> {
let selected_root_dir = match ui_mode {
UiMode::Setup => SETUP_UI_WWW_DIR,
UiMode::Diag => DIAG_UI_WWW_DIR,
UiMode::Install => INSTALL_UI_WWW_DIR,
UiMode::Main => MAIN_UI_WWW_DIR,
};
let (request_parts, _body) = req.into_parts();
let accept_encoding = request_parts
.headers
@@ -243,46 +247,32 @@ async fn alt_ui(req: Request<Body>, ui_mode: UiMode) -> Result<Response<Body>, E
.collect::<Vec<_>>();
match &request_parts.method {
&Method::GET => {
let uri_path = request_parts
.uri
.path()
.strip_prefix('/')
.unwrap_or(request_parts.uri.path());
let uri_path = ui_mode.path(
request_parts
.uri
.path()
.strip_prefix('/')
.unwrap_or(request_parts.uri.path()),
);
let full_path = Path::new(selected_root_dir).join(uri_path);
file_send(
&request_parts,
if tokio::fs::metadata(&full_path)
let file = EMBEDDED_UIS
.get_file(&*uri_path)
.or_else(|| EMBEDDED_UIS.get_file(&*ui_mode.path("index.html")));
if let Some(file) = file {
FileData::from_embedded(&request_parts, file)
.into_response(&request_parts)
.await
.ok()
.map(|f| f.is_file())
.unwrap_or(false)
{
full_path
} else {
Path::new(selected_root_dir).join("index.html")
},
&accept_encoding,
)
.await
} else {
Ok(not_found())
}
}
_ => Ok(method_not_allowed()),
}
}
async fn main_embassy_ui(req: Request<Body>, ctx: RpcContext) -> Result<Response<Body>, Error> {
let selected_root_dir = MAIN_UI_WWW_DIR;
let (request_parts, _body) = req.into_parts();
let accept_encoding = request_parts
.headers
.get_all(ACCEPT_ENCODING)
.into_iter()
.filter_map(|h| h.to_str().ok())
.flat_map(|s| s.split(","))
.filter_map(|s| s.split(";").next())
.map(|s| s.trim())
.collect::<Vec<_>>();
match (
&request_parts.method,
request_parts
@@ -297,11 +287,12 @@ async fn main_embassy_ui(req: Request<Body>, ctx: RpcContext) -> Result<Response
Ok(_) => {
let sub_path = Path::new(path);
if let Ok(rest) = sub_path.strip_prefix("package-data") {
file_send(
FileData::from_path(
&request_parts,
ctx.datadir.join(PKG_PUBLIC_DIR).join(rest),
&accept_encoding,
&ctx.datadir.join(PKG_PUBLIC_DIR).join(rest),
)
.await?
.into_response(&request_parts)
.await
} else if let Ok(rest) = sub_path.strip_prefix("eos") {
match rest.to_str() {
@@ -323,28 +314,25 @@ async fn main_embassy_ui(req: Request<Body>, ctx: RpcContext) -> Result<Response
}
}
(&Method::GET, _) => {
let uri_path = request_parts
.uri
.path()
.strip_prefix('/')
.unwrap_or(request_parts.uri.path());
let uri_path = UiMode::Main.path(
request_parts
.uri
.path()
.strip_prefix('/')
.unwrap_or(request_parts.uri.path()),
);
let full_path = Path::new(selected_root_dir).join(uri_path);
file_send(
&request_parts,
if tokio::fs::metadata(&full_path)
let file = EMBEDDED_UIS
.get_file(&*uri_path)
.or_else(|| EMBEDDED_UIS.get_file(&*UiMode::Main.path("index.html")));
if let Some(file) = file {
FileData::from_embedded(&request_parts, file)
.into_response(&request_parts)
.await
.ok()
.map(|f| f.is_file())
.unwrap_or(false)
{
full_path
} else {
Path::new(selected_root_dir).join("index.html")
},
&accept_encoding,
)
.await
} else {
Ok(not_found())
}
}
_ => Ok(method_not_allowed()),
}
@@ -407,118 +395,163 @@ fn cert_send(cert: &X509) -> Result<Response<Body>, Error> {
.with_kind(ErrorKind::Network)
}
async fn file_send(
req: &RequestParts,
path: impl AsRef<Path>,
accept_encoding: &[&str],
) -> Result<Response<Body>, Error> {
// Serve a file by asynchronously reading it by chunks using tokio-util crate.
struct FileData {
data: Body,
len: Option<u64>,
encoding: Option<&'static str>,
e_tag: String,
mime: Option<String>,
}
impl FileData {
fn from_embedded(req: &RequestParts, file: &'static include_dir::File<'static>) -> Self {
let path = file.path();
let (encoding, data) = req
.headers
.get_all(ACCEPT_ENCODING)
.into_iter()
.filter_map(|h| h.to_str().ok())
.flat_map(|s| s.split(","))
.filter_map(|s| s.split(";").next())
.map(|s| s.trim())
.fold((None, file.contents()), |acc, e| {
if let Some(file) = (e == "br")
.then_some(())
.and_then(|_| EMBEDDED_UIS.get_file(format!("{}.br", path.display())))
{
(Some("br"), file.contents())
} else if let Some(file) = (e == "gzip" && acc.0 != Some("br"))
.then_some(())
.and_then(|_| EMBEDDED_UIS.get_file(format!("{}.gz", path.display())))
{
(Some("gzip"), file.contents())
} else {
acc
}
});
let path = path.as_ref();
let file = File::open(path)
.await
.with_ctx(|_| (ErrorKind::Filesystem, path.display().to_string()))?;
let metadata = file
.metadata()
.await
.with_ctx(|_| (ErrorKind::Filesystem, path.display().to_string()))?;
let e_tag = e_tag(path, &metadata)?;
let mut builder = Response::builder();
builder = with_content_type(path, builder);
builder = builder.header(http::header::ETAG, &e_tag);
builder = builder.header(
http::header::CACHE_CONTROL,
"public, max-age=21000000, immutable",
);
if req
.headers
.get_all(http::header::CONNECTION)
.iter()
.flat_map(|s| s.to_str().ok())
.flat_map(|s| s.split(","))
.any(|s| s.trim() == "keep-alive")
{
builder = builder.header(http::header::CONNECTION, "keep-alive");
Self {
len: Some(data.len() as u64),
encoding,
data: data.into(),
e_tag: e_tag(path, None),
mime: MimeGuess::from_path(path)
.first()
.map(|m| m.essence_str().to_owned()),
}
}
if req
.headers
.get("if-none-match")
.and_then(|h| h.to_str().ok())
== Some(e_tag.as_str())
{
builder = builder.status(StatusCode::NOT_MODIFIED);
builder.body(Body::empty())
} else {
let body = if false && accept_encoding.contains(&"br") && metadata.len() > u16::MAX as u64 {
builder = builder.header(CONTENT_ENCODING, "br");
Body::wrap_stream(ReaderStream::new(BrotliEncoder::new(BufReader::new(file))))
} else if accept_encoding.contains(&"gzip") && metadata.len() > u16::MAX as u64 {
builder = builder.header(CONTENT_ENCODING, "gzip");
Body::wrap_stream(ReaderStream::new(GzipEncoder::new(BufReader::new(file))))
async fn from_path(req: &RequestParts, path: &Path) -> Result<Self, Error> {
let encoding = req
.headers
.get_all(ACCEPT_ENCODING)
.into_iter()
.filter_map(|h| h.to_str().ok())
.flat_map(|s| s.split(","))
.filter_map(|s| s.split(";").next())
.map(|s| s.trim())
.any(|e| e == "gzip")
.then_some("gzip");
let file = File::open(path)
.await
.with_ctx(|_| (ErrorKind::Filesystem, path.display().to_string()))?;
let metadata = file
.metadata()
.await
.with_ctx(|_| (ErrorKind::Filesystem, path.display().to_string()))?;
let e_tag = e_tag(path, Some(&metadata));
let (len, data) = if encoding == Some("gzip") {
(
None,
Body::wrap_stream(ReaderStream::new(GzipEncoder::new(BufReader::new(file)))),
)
} else {
builder = with_content_length(&metadata, builder);
Body::wrap_stream(ReaderStream::new(file))
(
Some(metadata.len()),
Body::wrap_stream(ReaderStream::new(file)),
)
};
builder.body(body)
Ok(Self {
data,
len,
encoding,
e_tag,
mime: MimeGuess::from_path(path)
.first()
.map(|m| m.essence_str().to_owned()),
})
}
async fn into_response(self, req: &RequestParts) -> Result<Response<Body>, Error> {
let mut builder = Response::builder();
if let Some(mime) = self.mime {
builder = builder.header(http::header::CONTENT_TYPE, &*mime);
}
builder = builder.header(http::header::ETAG, &*self.e_tag);
builder = builder.header(
http::header::CACHE_CONTROL,
"public, max-age=21000000, immutable",
);
if req
.headers
.get_all(http::header::CONNECTION)
.iter()
.flat_map(|s| s.to_str().ok())
.flat_map(|s| s.split(","))
.any(|s| s.trim() == "keep-alive")
{
builder = builder.header(http::header::CONNECTION, "keep-alive");
}
if req
.headers
.get("if-none-match")
.and_then(|h| h.to_str().ok())
== Some(self.e_tag.as_ref())
{
builder = builder.status(StatusCode::NOT_MODIFIED);
builder.body(Body::empty())
} else {
if let Some(len) = self.len {
builder = builder.header(http::header::CONTENT_LENGTH, len);
}
if let Some(encoding) = self.encoding {
builder = builder.header(http::header::CONTENT_ENCODING, encoding);
}
builder.body(self.data)
}
.with_kind(ErrorKind::Network)
}
.with_kind(ErrorKind::Network)
}
fn e_tag(path: &Path, metadata: &Metadata) -> Result<String, Error> {
let modified = metadata.modified().with_kind(ErrorKind::Filesystem)?;
fn e_tag(path: &Path, metadata: Option<&Metadata>) -> String {
let mut hasher = sha2::Sha256::new();
hasher.update(format!("{:?}", path).as_bytes());
hasher.update(
format!(
"{}",
modified
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
)
.as_bytes(),
);
if let Some(modified) = metadata.and_then(|m| m.modified().ok()) {
hasher.update(
format!(
"{}",
modified
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
)
.as_bytes(),
);
}
let res = hasher.finalize();
Ok(format!(
format!(
"\"{}\"",
base32::encode(base32::Alphabet::RFC4648 { padding: false }, res.as_slice()).to_lowercase()
))
)
}
///https://en.wikipedia.org/wiki/Media_type
fn with_content_type(path: &Path, builder: Builder) -> Builder {
let content_type = match path.extension() {
Some(os_str) => match os_str.to_str() {
Some("apng") => "image/apng",
Some("avif") => "image/avif",
Some("flif") => "image/flif",
Some("gif") => "image/gif",
Some("jpg") | Some("jpeg") | Some("jfif") | Some("pjpeg") | Some("pjp") => "image/jpeg",
Some("jxl") => "image/jxl",
Some("png") => "image/png",
Some("svg") => "image/svg+xml",
Some("webp") => "image/webp",
Some("mng") | Some("x-mng") => "image/x-mng",
Some("css") => "text/css",
Some("csv") => "text/csv",
Some("html") => "text/html",
Some("php") => "text/php",
Some("plain") | Some("md") | Some("txt") => "text/plain",
Some("xml") => "text/xml",
Some("js") => "text/javascript",
Some("wasm") => "application/wasm",
None | Some(_) => "text/plain",
},
None => "text/plain",
};
builder.header(http::header::CONTENT_TYPE, content_type)
}
fn with_content_length(metadata: &Metadata, builder: Builder) -> Builder {
builder.header(http::header::CONTENT_LENGTH, metadata.len())
#[test]
fn test_packed_html() {
assert!(MainUi::get("index.html").is_some())
}

View File

@@ -3,6 +3,7 @@ use std::convert::Infallible;
use std::net::{IpAddr, Ipv6Addr, SocketAddr};
use std::str::FromStr;
use std::sync::{Arc, Weak};
use std::time::Duration;
use color_eyre::eyre::eyre;
use helpers::NonDetachingJoinHandle;
@@ -19,7 +20,7 @@ use tokio_rustls::{LazyConfigAcceptor, TlsConnector};
use crate::net::keys::Key;
use crate::net::ssl::SslManager;
use crate::net::utils::SingleAccept;
use crate::util::io::BackTrackingReader;
use crate::util::io::{BackTrackingReader, TimeoutStream};
use crate::Error;
// not allowed: <=1024, >=32768, 5355, 5432, 9050, 6010, 9051, 5353
@@ -41,7 +42,7 @@ impl VHostController {
hostname: Option<String>,
external: u16,
target: SocketAddr,
connect_ssl: bool,
connect_ssl: Result<(), AlpnInfo>,
) -> Result<Arc<()>, Error> {
let mut writable = self.servers.lock().await;
let server = if let Some(server) = writable.remove(&external) {
@@ -77,10 +78,16 @@ impl VHostController {
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
struct TargetInfo {
addr: SocketAddr,
connect_ssl: bool,
connect_ssl: Result<(), AlpnInfo>,
key: Key,
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum AlpnInfo {
Reflect,
Specified(Vec<Vec<u8>>),
}
struct VHostServer {
mapping: Weak<RwLock<BTreeMap<Option<String>, BTreeMap<TargetInfo, Weak<()>>>>>,
_thread: NonDetachingJoinHandle<()>,
@@ -98,6 +105,8 @@ impl VHostServer {
loop {
match listener.accept().await {
Ok((stream, _)) => {
let stream =
Box::pin(TimeoutStream::new(stream, Duration::from_secs(300)));
let mut stream = BackTrackingReader::new(stream);
stream.start_buffering();
let mapping = mapping.clone();
@@ -178,7 +187,7 @@ impl VHostServer {
let cfg = ServerConfig::builder()
.with_safe_defaults()
.with_no_client_auth();
let cfg =
let mut cfg =
if mid.client_hello().signature_schemes().contains(
&tokio_rustls::rustls::SignatureScheme::ED25519,
) {
@@ -213,49 +222,94 @@ impl VHostServer {
.private_key_to_der()?,
),
)
};
let mut tls_stream = mid
.into_stream(Arc::new(
cfg.with_kind(crate::ErrorKind::OpenSsl)?,
))
.await?;
tls_stream.get_mut().0.stop_buffering();
if target.connect_ssl {
tokio::io::copy_bidirectional(
&mut tls_stream,
&mut TlsConnector::from(Arc::new(
}
.with_kind(crate::ErrorKind::OpenSsl)?;
match target.connect_ssl {
Ok(()) => {
let mut client_cfg =
tokio_rustls::rustls::ClientConfig::builder()
.with_safe_defaults()
.with_root_certificates({
let mut store = RootCertStore::empty();
store.add(
&tokio_rustls::rustls::Certificate(
key.root_ca().to_der()?,
),
).with_kind(crate::ErrorKind::OpenSsl)?;
&tokio_rustls::rustls::Certificate(
key.root_ca().to_der()?,
),
).with_kind(crate::ErrorKind::OpenSsl)?;
store
})
.with_no_client_auth(),
))
.connect(
key.key()
.internal_address()
.as_str()
.try_into()
.with_kind(crate::ErrorKind::OpenSsl)?,
tcp_stream,
.with_no_client_auth();
client_cfg.alpn_protocols = mid
.client_hello()
.alpn()
.into_iter()
.flatten()
.map(|x| x.to_vec())
.collect();
let mut target_stream =
TlsConnector::from(Arc::new(client_cfg))
.connect_with(
key.key()
.internal_address()
.as_str()
.try_into()
.with_kind(
crate::ErrorKind::OpenSsl,
)?,
tcp_stream,
|conn| {
cfg.alpn_protocols.extend(
conn.alpn_protocol()
.into_iter()
.map(|p| p.to_vec()),
)
},
)
.await
.with_kind(crate::ErrorKind::OpenSsl)?;
let mut tls_stream =
mid.into_stream(Arc::new(cfg)).await?;
tls_stream.get_mut().0.stop_buffering();
tokio::io::copy_bidirectional(
&mut tls_stream,
&mut target_stream,
)
.await
.with_kind(crate::ErrorKind::OpenSsl)?,
)
.await?;
} else {
tokio::io::copy_bidirectional(
&mut tls_stream,
&mut tcp_stream,
)
.await?;
}
Err(AlpnInfo::Reflect) => {
for proto in
mid.client_hello().alpn().into_iter().flatten()
{
cfg.alpn_protocols.push(proto.into());
}
let mut tls_stream =
mid.into_stream(Arc::new(cfg)).await?;
tls_stream.get_mut().0.stop_buffering();
tokio::io::copy_bidirectional(
&mut tls_stream,
&mut tcp_stream,
)
.await
}
Err(AlpnInfo::Specified(alpn)) => {
cfg.alpn_protocols = alpn;
let mut tls_stream =
mid.into_stream(Arc::new(cfg)).await?;
tls_stream.get_mut().0.stop_buffering();
tokio::io::copy_bidirectional(
&mut tls_stream,
&mut tcp_stream,
)
.await
}
}
.map_or_else(
|e| match e.kind() {
std::io::ErrorKind::UnexpectedEof => Ok(()),
_ => Err(e),
},
|_| Ok(()),
)?;
} else {
// 503
}

View File

@@ -149,29 +149,36 @@ pub async fn execute(
if !overwrite {
if let Ok(guard) =
TmpMountGuard::mount(&BlockDev::new(part_info.root.clone()), MountType::ReadOnly).await
TmpMountGuard::mount(&BlockDev::new(part_info.root.clone()), MountType::ReadWrite).await
{
if let Err(e) = async {
// cp -r ${guard}/config /tmp/config
Command::new("cp")
.arg("-r")
.arg(guard.as_ref().join("config"))
.arg("/tmp/config.bak")
.invoke(crate::ErrorKind::Filesystem)
.await?;
if tokio::fs::metadata(guard.as_ref().join("config/upgrade"))
.await
.is_ok()
{
tokio::fs::remove_file(guard.as_ref().join("config/upgrade")).await?;
}
guard.unmount().await
if tokio::fs::metadata(guard.as_ref().join("config/disk.guid"))
.await
.is_ok()
{
tokio::fs::remove_file(guard.as_ref().join("config/disk.guid")).await?;
}
Command::new("cp")
.arg("-r")
.arg(guard.as_ref().join("config"))
.arg("/tmp/config.bak")
.invoke(crate::ErrorKind::Filesystem)
.await?;
Ok::<_, Error>(())
}
.await
{
tracing::error!("Error recovering previous config: {e}");
tracing::debug!("{e:?}");
}
guard.unmount().await?;
}
}

View File

@@ -1,9 +1,9 @@
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;
use color_eyre::eyre::eyre;
use futures::StreamExt;
use helpers::{Rsync, RsyncOptions};
use josekit::jwk::Jwk;
use openssl::x509::X509;
use patch_db::DbHandle;
@@ -13,6 +13,7 @@ use serde::{Deserialize, Serialize};
use sqlx::Connection;
use tokio::fs::File;
use tokio::io::AsyncWriteExt;
use tokio::try_join;
use torut::onion::OnionAddressV3;
use tracing::instrument;
@@ -32,6 +33,7 @@ use crate::disk::REPAIR_DISK_PATH;
use crate::hostname::Hostname;
use crate::init::{init, InitResult};
use crate::middleware::encrypt::EncryptedWire;
use crate::util::io::{dir_copy, dir_size, Counter};
use crate::{Error, ErrorKind, ResultExt};
#[command(subcommands(status, disk, attach, execute, cifs, complete, get_pubkey, exit))]
@@ -123,7 +125,7 @@ pub async fn attach(
} else {
RepairStrategy::Preen
},
DEFAULT_PASSWORD,
if guid.ends_with("_UNENC") { None } else { Some(DEFAULT_PASSWORD) },
)
.await?;
if tokio::fs::metadata(REPAIR_DISK_PATH).await.is_ok() {
@@ -142,7 +144,7 @@ pub async fn attach(
}
let (hostname, tor_addr, root_ca) = setup_init(&ctx, password).await?;
*ctx.setup_result.write().await = Some((guid, SetupResult {
tor_address: format!("http://{}", tor_addr),
tor_address: format!("https://{}", tor_addr),
lan_address: hostname.lan_address(),
root_ca: String::from_utf8(root_ca.to_pem()?)?,
}));
@@ -279,7 +281,7 @@ pub async fn execute(
*ctx.setup_result.write().await = Some((
guid,
SetupResult {
tor_address: format!("http://{}", tor_addr),
tor_address: format!("https://{}", tor_addr),
lan_address: hostname.lan_address(),
root_ca: String::from_utf8(
root_ca.to_pem().expect("failed to serialize root ca"),
@@ -335,12 +337,17 @@ pub async fn execute_inner(
recovery_source: Option<RecoverySource>,
recovery_password: Option<String>,
) -> Result<(Arc<String>, Hostname, OnionAddressV3, X509), Error> {
let encryption_password = if ctx.disable_encryption {
None
} else {
Some(DEFAULT_PASSWORD)
};
let guid = Arc::new(
crate::disk::main::create(
&[embassy_logicalname],
&pvscan().await?,
&ctx.datadir,
DEFAULT_PASSWORD,
encryption_password,
)
.await?,
);
@@ -348,7 +355,7 @@ pub async fn execute_inner(
&*guid,
&ctx.datadir,
RepairStrategy::Preen,
DEFAULT_PASSWORD,
encryption_password,
)
.await?;
@@ -416,74 +423,78 @@ async fn migrate(
&old_guid,
"/media/embassy/migrate",
RepairStrategy::Preen,
DEFAULT_PASSWORD,
)
.await?;
let mut main_transfer = Rsync::new(
"/media/embassy/migrate/main/",
"/embassy-data/main/",
RsyncOptions {
delete: true,
force: true,
ignore_existing: false,
exclude: Vec::new(),
no_permissions: false,
no_owner: false,
if guid.ends_with("_UNENC") {
None
} else {
Some(DEFAULT_PASSWORD)
},
)
.await?;
let mut package_data_transfer = Rsync::new(
let main_transfer_args = ("/media/embassy/migrate/main/", "/embassy-data/main/");
let package_data_transfer_args = (
"/media/embassy/migrate/package-data/",
"/embassy-data/package-data/",
RsyncOptions {
delete: true,
force: true,
ignore_existing: false,
exclude: vec!["tmp".to_owned()],
no_permissions: false,
no_owner: false,
},
)
.await?;
);
let mut main_prog = 0.0;
let mut main_complete = false;
let mut pkg_prog = 0.0;
let mut pkg_complete = false;
loop {
tokio::select! {
p = main_transfer.progress.next() => {
if let Some(p) = p {
main_prog = p;
} else {
main_prog = 1.0;
main_complete = true;
}
}
p = package_data_transfer.progress.next() => {
if let Some(p) = p {
pkg_prog = p;
} else {
pkg_prog = 1.0;
pkg_complete = true;
}
}
}
if main_prog > 0.0 && pkg_prog > 0.0 {
*ctx.setup_status.write().await = Some(Ok(SetupStatus {
bytes_transferred: ((main_prog * 50.0) + (pkg_prog * 950.0)) as u64,
total_bytes: Some(1000),
complete: false,
}));
}
if main_complete && pkg_complete {
break;
}
let tmpdir = Path::new(package_data_transfer_args.0).join("tmp");
if tokio::fs::metadata(&tmpdir).await.is_ok() {
tokio::fs::remove_dir_all(&tmpdir).await?;
}
main_transfer.wait().await?;
package_data_transfer.wait().await?;
let ordering = std::sync::atomic::Ordering::Relaxed;
let main_transfer_size = Counter::new(0, ordering);
let package_data_transfer_size = Counter::new(0, ordering);
let size = tokio::select! {
res = async {
let (main_size, package_data_size) = try_join!(
dir_size(main_transfer_args.0, Some(&main_transfer_size)),
dir_size(package_data_transfer_args.0, Some(&package_data_transfer_size))
)?;
Ok::<_, Error>(main_size + package_data_size)
} => { res? },
res = async {
loop {
tokio::time::sleep(Duration::from_secs(1)).await;
*ctx.setup_status.write().await = Some(Ok(SetupStatus {
bytes_transferred: 0,
total_bytes: Some(main_transfer_size.load() + package_data_transfer_size.load()),
complete: false,
}));
}
} => res,
};
*ctx.setup_status.write().await = Some(Ok(SetupStatus {
bytes_transferred: 0,
total_bytes: Some(size),
complete: false,
}));
let main_transfer_progress = Counter::new(0, ordering);
let package_data_transfer_progress = Counter::new(0, ordering);
tokio::select! {
res = async {
try_join!(
dir_copy(main_transfer_args.0, main_transfer_args.1, Some(&main_transfer_progress)),
dir_copy(package_data_transfer_args.0, package_data_transfer_args.1, Some(&package_data_transfer_progress))
)?;
Ok::<_, Error>(())
} => { res? },
res = async {
loop {
tokio::time::sleep(Duration::from_secs(1)).await;
*ctx.setup_status.write().await = Some(Ok(SetupStatus {
bytes_transferred: main_transfer_progress.load() + package_data_transfer_progress.load(),
total_bytes: Some(size),
complete: false,
}));
}
} => res,
}
let (hostname, tor_addr, root_ca) = setup_init(&ctx, Some(embassy_password)).await?;

View File

@@ -22,7 +22,7 @@ use crate::util::serde::{display_serializable, IoFormat};
use crate::util::{display_none, Invoke};
use crate::{Error, ErrorKind, ResultExt};
pub const SYSTEMD_UNIT: &'static str = "embassyd";
pub const SYSTEMD_UNIT: &'static str = "startd";
#[command(subcommands(zram))]
pub async fn experimental() -> Result<(), Error> {

View File

@@ -2,7 +2,9 @@ use std::future::Future;
use std::io::Cursor;
use std::os::unix::prelude::MetadataExt;
use std::path::Path;
use std::sync::atomic::AtomicU64;
use std::task::Poll;
use std::time::Duration;
use futures::future::{BoxFuture, Fuse};
use futures::{AsyncSeek, FutureExt, TryStreamExt};
@@ -11,6 +13,8 @@ use nix::unistd::{Gid, Uid};
use tokio::io::{
duplex, AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, DuplexStream, ReadBuf, WriteHalf,
};
use tokio::net::TcpStream;
use tokio::time::{Instant, Sleep};
use crate::ResultExt;
@@ -224,6 +228,7 @@ pub async fn copy_and_shutdown<R: AsyncRead + Unpin, W: AsyncWrite + Unpin>(
pub fn dir_size<'a, P: AsRef<Path> + 'a + Send + Sync>(
path: P,
ctr: Option<&'a Counter>,
) -> BoxFuture<'a, Result<u64, std::io::Error>> {
async move {
tokio_stream::wrappers::ReadDirStream::new(tokio::fs::read_dir(path.as_ref()).await?)
@@ -231,9 +236,12 @@ pub fn dir_size<'a, P: AsRef<Path> + 'a + Send + Sync>(
let m = e.metadata().await?;
Ok(acc
+ if m.is_file() {
if let Some(ctr) = ctr {
ctr.add(m.len());
}
m.len()
} else if m.is_dir() {
dir_size(e.path()).await?
dir_size(e.path(), ctr).await?
} else {
0
})
@@ -419,9 +427,60 @@ impl<T: AsyncWrite> AsyncWrite for BackTrackingReader<T> {
}
}
pub struct Counter {
atomic: AtomicU64,
ordering: std::sync::atomic::Ordering,
}
impl Counter {
pub fn new(init: u64, ordering: std::sync::atomic::Ordering) -> Self {
Self {
atomic: AtomicU64::new(init),
ordering,
}
}
pub fn load(&self) -> u64 {
self.atomic.load(self.ordering)
}
pub fn add(&self, value: u64) {
self.atomic.fetch_add(value, self.ordering);
}
}
#[pin_project::pin_project]
pub struct CountingReader<'a, R> {
ctr: &'a Counter,
#[pin]
rdr: R,
}
impl<'a, R> CountingReader<'a, R> {
pub fn new(rdr: R, ctr: &'a Counter) -> Self {
Self { ctr, rdr }
}
pub fn into_inner(self) -> R {
self.rdr
}
}
impl<'a, R: AsyncRead> AsyncRead for CountingReader<'a, R> {
fn poll_read(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<std::io::Result<()>> {
let this = self.project();
let start = buf.filled().len();
let res = this.rdr.poll_read(cx, buf);
let len = buf.filled().len() - start;
if len > 0 {
this.ctr.add(len as u64);
}
res
}
}
pub fn dir_copy<'a, P0: AsRef<Path> + 'a + Send + Sync, P1: AsRef<Path> + 'a + Send + Sync>(
src: P0,
dst: P1,
ctr: Option<&'a Counter>,
) -> BoxFuture<'a, Result<(), crate::Error>> {
async move {
let m = tokio::fs::metadata(&src).await?;
@@ -464,23 +523,23 @@ pub fn dir_copy<'a, P0: AsRef<Path> + 'a + Send + Sync, P1: AsRef<Path> + 'a + S
let dst_path = dst_path.join(e.file_name());
if m.is_file() {
let len = m.len();
let mut dst_file =
&mut tokio::fs::File::create(&dst_path).await.with_ctx(|_| {
(
crate::ErrorKind::Filesystem,
format!("create {}", dst_path.display()),
)
})?;
tokio::io::copy(
&mut tokio::fs::File::open(&src_path).await.with_ctx(|_| {
(
crate::ErrorKind::Filesystem,
format!("open {}", src_path.display()),
)
})?,
&mut dst_file,
)
.await
let mut dst_file = tokio::fs::File::create(&dst_path).await.with_ctx(|_| {
(
crate::ErrorKind::Filesystem,
format!("create {}", dst_path.display()),
)
})?;
let mut rdr = tokio::fs::File::open(&src_path).await.with_ctx(|_| {
(
crate::ErrorKind::Filesystem,
format!("open {}", src_path.display()),
)
})?;
if let Some(ctr) = ctr {
tokio::io::copy(&mut CountingReader::new(rdr, ctr), &mut dst_file).await
} else {
tokio::io::copy(&mut rdr, &mut dst_file).await
}
.with_ctx(|_| {
(
crate::ErrorKind::Filesystem,
@@ -508,7 +567,7 @@ pub fn dir_copy<'a, P0: AsRef<Path> + 'a + Send + Sync, P1: AsRef<Path> + 'a + S
)
})?;
} else if m.is_dir() {
dir_copy(src_path, dst_path).await?;
dir_copy(src_path, dst_path, ctr).await?;
} else if m.file_type().is_symlink() {
tokio::fs::symlink(
tokio::fs::read_link(&src_path).await.with_ctx(|_| {
@@ -535,3 +594,77 @@ pub fn dir_copy<'a, P0: AsRef<Path> + 'a + Send + Sync, P1: AsRef<Path> + 'a + S
}
.boxed()
}
#[pin_project::pin_project]
pub struct TimeoutStream<S: AsyncRead + AsyncWrite = TcpStream> {
timeout: Duration,
#[pin]
sleep: Sleep,
#[pin]
stream: S,
}
impl<S: AsyncRead + AsyncWrite> TimeoutStream<S> {
pub fn new(stream: S, timeout: Duration) -> Self {
Self {
timeout,
sleep: tokio::time::sleep(timeout),
stream,
}
}
}
impl<S: AsyncRead + AsyncWrite> AsyncRead for TimeoutStream<S> {
fn poll_read(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &mut tokio::io::ReadBuf<'_>,
) -> std::task::Poll<std::io::Result<()>> {
let mut this = self.project();
if let std::task::Poll::Ready(_) = this.sleep.as_mut().poll(cx) {
return std::task::Poll::Ready(Err(std::io::Error::new(
std::io::ErrorKind::TimedOut,
"timed out",
)));
}
let res = this.stream.poll_read(cx, buf);
if res.is_ready() {
this.sleep.reset(Instant::now() + *this.timeout);
}
res
}
}
impl<S: AsyncRead + AsyncWrite> AsyncWrite for TimeoutStream<S> {
fn poll_write(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &[u8],
) -> std::task::Poll<Result<usize, std::io::Error>> {
let mut this = self.project();
let res = this.stream.poll_write(cx, buf);
if res.is_ready() {
this.sleep.reset(Instant::now() + *this.timeout);
}
res
}
fn poll_flush(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Result<(), std::io::Error>> {
let mut this = self.project();
let res = this.stream.poll_flush(cx);
if res.is_ready() {
this.sleep.reset(Instant::now() + *this.timeout);
}
res
}
fn poll_shutdown(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Result<(), std::io::Error>> {
let mut this = self.project();
let res = this.stream.poll_shutdown(cx);
if res.is_ready() {
this.sleep.reset(Instant::now() + *this.timeout);
}
res
}
}

View File

@@ -0,0 +1,41 @@
use async_trait::async_trait;
use emver::VersionRange;
use models::ResultExt;
use super::v0_3_0::V0_3_0_COMPAT;
use super::*;
const V0_3_4_4: emver::Version = emver::Version::new(0, 3, 4, 4);
#[derive(Clone, Debug)]
pub struct Version;
#[async_trait]
impl VersionT for Version {
type Previous = v0_3_4_3::Version;
fn new() -> Self {
Version
}
fn semver(&self) -> emver::Version {
V0_3_4_4
}
fn compat(&self) -> &'static VersionRange {
&*V0_3_0_COMPAT
}
async fn up<Db: DbHandle>(&self, db: &mut Db, _secrets: &PgPool) -> Result<(), Error> {
let mut tor_addr = crate::db::DatabaseModel::new()
.server_info()
.tor_address()
.get_mut(db)
.await?;
tor_addr
.set_scheme("https")
.map_err(|_| eyre!("unable to update url scheme to https"))
.with_kind(crate::ErrorKind::ParseUrl)?;
tor_addr.save(db).await?;
Ok(())
}
async fn down<Db: DbHandle>(&self, _db: &mut Db, _secrets: &PgPool) -> Result<(), Error> {
Ok(())
}
}

19
backend/startd.service Normal file
View File

@@ -0,0 +1,19 @@
[Unit]
Description=StartOS Daemon
After=network-online.target
Requires=network-online.target
Wants=avahi-daemon.service
[Service]
Type=simple
Environment=RUST_LOG=startos=debug,js_engine=debug,patch_db=warn
ExecStart=/usr/bin/startd
Restart=always
RestartSec=3
ManagedOOMPreference=avoid
CPUAccounting=true
CPUWeight=1000
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target

View File

@@ -1145,7 +1145,14 @@ export const action = {
async "test-disk-usage"(effects, _input) {
const usage = await effects.diskUsage()
return usage
return {
result: {
copyable: false,
message: `${usage.used} / ${usage.total}`,
version: "0",
qr: false,
},
};
}
};

View File

@@ -1,5 +0,0 @@
os-partitions:
boot: /dev/mmcblk0p1
root: /dev/mmcblk0p2
ethernet-interface: end0
wifi-interface: wlan0

22
compress-uis.sh Executable file
View File

@@ -0,0 +1,22 @@
#!/bin/bash
set -e
rm -rf frontend/dist/static
find frontend/dist/raw -type f -not -name '*.gz' -and -not -name '*.br' | xargs -n 1 -P 0 gzip -kf
find frontend/dist/raw -type f -not -name '*.gz' -and -not -name '*.br' | xargs -n 1 -P 0 brotli -kf
for file in $(find frontend/dist/raw -type f -not -name '*.gz' -and -not -name '*.br'); do
raw_size=$(du --bytes $file | awk '{print $1}')
gz_size=$(du --bytes $file.gz | awk '{print $1}')
br_size=$(du --bytes $file.br | awk '{print $1}')
if [ $((gz_size * 100 / raw_size)) -gt 70 ]; then
rm $file.gz
fi
if [ $((br_size * 100 / raw_size)) -gt 70 ]; then
rm $file.br
fi
done
cp -r frontend/dist/raw frontend/dist/static

View File

@@ -14,7 +14,7 @@
"builder": "@angular-devkit/build-angular:browser",
"options": {
"preserveSymlinks": true,
"outputPath": "dist/ui",
"outputPath": "dist/raw/ui",
"index": "projects/ui/src/index.html",
"main": "projects/ui/src/main.ts",
"polyfills": "projects/ui/src/polyfills.ts",
@@ -39,7 +39,7 @@
"projects/ui/src/manifest.webmanifest",
{
"glob": "ngsw.json",
"input": "dist/ui",
"input": "dist/raw/ui",
"output": "projects/ui/src"
},
{
@@ -153,7 +153,7 @@
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/install-wizard",
"outputPath": "dist/raw/install-wizard",
"index": "projects/install-wizard/src/index.html",
"main": "projects/install-wizard/src/main.ts",
"polyfills": "projects/install-wizard/src/polyfills.ts",
@@ -283,7 +283,7 @@
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/setup-wizard",
"outputPath": "dist/raw/setup-wizard",
"index": "projects/setup-wizard/src/index.html",
"main": "projects/setup-wizard/src/main.ts",
"polyfills": "projects/setup-wizard/src/polyfills.ts",
@@ -403,7 +403,7 @@
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/diagnostic-ui",
"outputPath": "dist/raw/diagnostic-ui",
"index": "projects/diagnostic-ui/src/index.html",
"main": "projects/diagnostic-ui/src/main.ts",
"polyfills": "projects/diagnostic-ui/src/polyfills.ts",

View File

@@ -1,12 +1,12 @@
{
"name": "startos-ui",
"version": "0.3.4.3",
"version": "0.3.4.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "startos-ui",
"version": "0.3.4.3",
"version": "0.3.4.4",
"dependencies": {
"@angular/animations": "^14.1.0",
"@angular/common": "^14.1.0",

View File

@@ -1,6 +1,6 @@
{
"name": "startos-ui",
"version": "0.3.4.3",
"version": "0.3.4.4",
"author": "Start9 Labs, Inc",
"homepage": "https://start9.com/",
"scripts": {
@@ -22,7 +22,7 @@
"build:all": "npm run build:deps && npm run build:dui && npm run build:setup && npm run build:ui && npm run build:install-wiz",
"build:shared": "ng build shared",
"build:marketplace": "npm run build:shared && ng build marketplace",
"analyze:ui": "webpack-bundle-analyzer dist/ui/stats.json",
"analyze:ui": "webpack-bundle-analyzer dist/raw/ui/stats.json",
"publish:shared": "npm run build:shared && npm publish ./dist/shared --access public",
"publish:marketplace": "npm run build:marketplace && npm publish ./dist/marketplace --access public",
"start:dui": "npm run-script build-config && ionic serve --project diagnostic-ui --host 0.0.0.0",

View File

@@ -155,7 +155,6 @@ export class EmbassyPage {
await this.navCtrl.navigateForward(`/loading`)
} catch (e: any) {
this.errorToastService.present(e)
console.error(e)
} finally {
loader.dismiss()
}

View File

@@ -27,31 +27,15 @@
<section
style="
padding: 1rem 3rem 2rem 3rem;
border: solid #c4c4c5 3px;
margin-bottom: 24px;
border: solid #c4c4c5 3px;
border-radius: 20px;
"
>
<h2 style="font-variant-caps: all-small-caps">
Access from home (LAN)
</h2>
<p>
Visit the address below when you are connected to the same WiFi or
Local Area Network (LAN) as your server:
</p>
<p
style="
padding: 16px;
font-weight: bold;
font-size: 1.1rem;
overflow: auto;
"
>
<code id="lan-addr"></code>
</p>
<div>
<h3 style="color: #f8546a; font-weight: bold">Important!</h3>
<p>
Be sure to
Download your server's Root CA and
<a
href="https://docs.start9.com/latest/user-manual/connecting/connecting-lan"
target="_blank"
@@ -60,12 +44,10 @@
>
follow the instructions
</a>
to establish a secure connection by installing your server's root
certificate authority.
to establish a secure connection with your server.
</p>
</div>
<div style="padding: 2rem; text-align: center">
<div style="text-align: center">
<a
id="cert"
[download]="crtName"
@@ -88,12 +70,49 @@
</a>
</div>
</section>
<section
style="
padding: 1rem 3rem 2rem 3rem;
border: solid #c4c4c5 3px;
border-radius: 20px;
margin-bottom: 24px;
"
>
<h2 style="font-variant-caps: all-small-caps">
Access from home (LAN)
</h2>
<p>
Visit the address below when you are connected to the same WiFi or
Local Area Network (LAN) as your server.
</p>
<p
style="
padding: 16px;
font-weight: bold;
font-size: 1.1rem;
overflow: auto;
"
>
<code id="lan-addr"></code>
</p>
<section style="padding: 1rem 3rem 2rem 3rem; border: solid #c4c4c5 3px">
<h2 style="font-variant-caps: all-small-caps">
Access on the go (Tor)
</h2>
<p>Visit the address below when you are away from home:</p>
<p>Visit the address below when you are away from home.</p>
<p>
<span style="font-weight: bold">Note:</span>
This address will only work from a Tor-enabled browser.
<a
href="https://docs.start9.com/latest/user-manual/connecting/connecting-tor"
target="_blank"
rel="noreferrer"
style="color: #6866cc; font-weight: bold; text-decoration: none"
>
Follow the instructions
</a>
to get setup.
</p>
<p
style="
padding: 16px;
@@ -104,21 +123,6 @@
>
<code id="tor-addr"></code>
</p>
<div>
<h3 style="color: #f8546a; font-weight: bold">Important!</h3>
<p>
This address will only work from a Tor-enabled browser.
<a
href="https://docs.start9.com/latest/user-manual/connecting/connecting-tor"
target="_blank"
rel="noreferrer"
style="color: #6866cc; font-weight: bold; text-decoration: none"
>
Follow the instructions
</a>
to get setup.
</p>
</div>
</section>
</div>
</body>

View File

@@ -49,7 +49,7 @@ export class SuccessPage {
const ret = await this.api.complete()
if (!this.isKiosk) {
this.torAddress = ret['tor-address']
this.lanAddress = ret['lan-address'].replace('https', 'http')
this.lanAddress = ret['lan-address'].replace(/^https:/, 'http:')
this.cert = ret['root-ca']
await this.api.exit()

View File

@@ -7,10 +7,10 @@ import {
} from '@start9labs/shared'
import {
ApiService,
CifsRecoverySource,
AttachReq,
ExecuteReq,
CifsRecoverySource,
CompleteRes,
ExecuteReq,
} from './api.service'
import * as jose from 'node-jose'
import { interval, map, Observable } from 'rxjs'
@@ -151,7 +151,7 @@ export class MockApiService extends ApiService {
async complete(): Promise<CompleteRes> {
await pauseFor(1000)
return {
'tor-address': 'http://asdafsadasdasasdasdfasdfasdf.onion',
'tor-address': 'https://asdafsadasdasasdasdfasdfasdf.onion',
'lan-address': 'https://adjective-noun.local',
'root-ca': encodeBase64(rootCA),
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -38,7 +38,7 @@ export class AppComponent implements OnDestroy {
readonly themeSwitcher: ThemeSwitcherService,
) {}
ngOnInit() {
async ngOnInit() {
this.patch
.watch$('ui', 'name')
.subscribe(name => this.titleService.setTitle(name || 'StartOS'))

View File

@@ -41,6 +41,7 @@ const ICONS = [
'file-tray-stacked-outline',
'finger-print-outline',
'flash-outline',
'flask-outline',
'flash-off-outline',
'folder-open-outline',
'globe-outline',
@@ -48,7 +49,6 @@ const ICONS = [
'hammer-outline',
'help-circle-outline',
'hammer-outline',
'home-outline',
'information-circle-outline',
'key-outline',
'list-outline',
@@ -76,6 +76,7 @@ const ICONS = [
'remove-circle-outline',
'remove-outline',
'repeat-outline',
'ribbon-outline',
'rocket-outline',
'save-outline',
'server-outline',

View File

@@ -49,7 +49,6 @@
<!-- INSECURE -->
<ng-template #insecure>
<insecure-warning></insecure-warning>
<h2>This page cannot safely be accessed over an insecure connection</h2>
</ng-template>
</ng-template>
</ion-content>

View File

@@ -43,9 +43,6 @@ export class ExperimentalFeaturesPage {
label: 'Wipe state',
type: 'checkbox',
value: 'wipe',
handler: val => {
console.error(val)
},
},
],
buttons: [
@@ -56,8 +53,7 @@ export class ExperimentalFeaturesPage {
{
text: 'Reset',
handler: (value: string[]) => {
console.error(value)
this.resetTor(value.some(v => 'wipe'))
this.resetTor(value.some(v => v === 'wipe'))
},
cssClass: 'enter-click',
},

View File

@@ -1,6 +1,6 @@
<ion-header>
<ion-toolbar>
<ion-title>Secure LAN</ion-title>
<ion-title>Trust Root CA</ion-title>
<ion-buttons slot="start">
<ion-back-button defaultHref="system"></ion-back-button>
</ion-buttons>
@@ -13,7 +13,7 @@
<ion-item class="ion-padding-bottom">
<ion-label>
<h2>
For a secure local connection,
For a secure local connection and faster Tor experience,
<a
href="https://docs.start9.com/latest/user-manual/connecting/connecting-lan"
target="_blank"
@@ -29,7 +29,7 @@
<ion-item button (click)="installCert()" [disabled]="!(crtName$ | async)">
<ion-icon slot="start" name="download-outline" size="large"></ion-icon>
<ion-label>
<h1>Download Certificate</h1>
<h1>Download Root CA</h1>
</ion-label>
</ion-item>
</ion-item-group>

View File

@@ -1,10 +1,9 @@
import { DOCUMENT } from '@angular/common'
import { Component, Inject } from '@angular/core'
import {
AlertController,
LoadingController,
NavController,
ModalController,
NavController,
ToastController,
} from '@ionic/angular'
import { ApiService } from 'src/app/services/api/embassy-api.service'
@@ -23,6 +22,9 @@ import {
GenericInputOptions,
} from 'src/app/apps/ui/modals/generic-input/generic-input.component'
import { ConfigService } from 'src/app/services/config.service'
import { DOCUMENT } from '@angular/common'
import { getServerInfo } from 'src/app/util/get-server-info'
import * as argon2 from '@start9labs/argon2'
@Component({
selector: 'server-show',
@@ -38,6 +40,8 @@ export class ServerShowPage {
readonly showDiskRepair$ = this.ClientStorageService.showDiskRepair$
readonly secure = this.config.isSecure()
readonly isTorHttp =
this.config.isTor() && this.document.location.protocol === 'http:'
constructor(
private readonly alertCtrl: AlertController,
@@ -94,7 +98,103 @@ export class ServerShowPage {
await modal.present()
}
private async updateEos(): Promise<void> {
async presentAlertResetPassword() {
const alert = await this.alertCtrl.create({
header: 'Warning',
message:
'You will still need your current password to decrypt existing backups!',
buttons: [
{
text: 'Cancel',
role: 'cancel',
},
{
text: 'Continue',
handler: () => this.presentModalResetPassword(),
cssClass: 'enter-click',
},
],
cssClass: 'alert-warning-message',
})
await alert.present()
}
async presentModalResetPassword(): Promise<void> {
const modal = await this.modalCtrl.create({
component: GenericFormPage,
componentProps: {
title: 'Change Master Password',
spec: PasswordSpec,
buttons: [
{
text: 'Save',
handler: (value: any) => {
return this.resetPassword(value)
},
isSubmit: true,
},
],
},
})
await modal.present()
}
private async resetPassword(value: {
currPass: string
newPass: string
newPass2: string
}): Promise<boolean> {
let err = ''
if (value.newPass !== value.newPass2) {
err = 'New passwords do not match'
} else if (value.newPass.length < 12) {
err = 'New password must be 12 characters or greater'
} else if (value.newPass.length > 64) {
err = 'New password must be less than 65 characters'
}
// confirm current password is correct
const { 'password-hash': passwordHash } = await getServerInfo(this.patch)
try {
argon2.verify(passwordHash, value.currPass)
} catch (e) {
err = 'Current password is invalid'
}
if (err) {
this.errToast.present(err)
return false
}
const loader = await this.loadingCtrl.create({
message: 'Changing master password...',
})
await loader.present()
try {
await this.embassyApi.resetPassword({
'old-password': value.currPass,
'new-password': value.newPass,
})
const toast = await this.toastCtrl.create({
header: 'Password changed!',
position: 'bottom',
duration: 2000,
})
toast.present()
return true
} catch (e: any) {
this.errToast.present(e)
return false
} finally {
loader.dismiss()
}
}
async updateEos(): Promise<void> {
const modal = await this.modalCtrl.create({
component: OSUpdatePage,
})
@@ -369,11 +469,11 @@ export class ServerShowPage {
disabled$: of(false),
},
{
title: 'LAN',
description: `Download and trust your server's certificate for a secure local connection`,
icon: 'home-outline',
title: 'Root CA',
description: `Download and trust your server's root certificate authority`,
icon: 'ribbon-outline',
action: () =>
this.navCtrl.navigateForward(['lan'], { relativeTo: this.route }),
this.navCtrl.navigateForward(['root-ca'], { relativeTo: this.route }),
detail: true,
disabled$: of(false),
},
@@ -416,6 +516,14 @@ export class ServerShowPage {
detail: true,
disabled$: of(false),
},
{
title: 'Change Master Password',
description: `Change your StartOS master password`,
icon: 'key-outline',
action: () => this.presentAlertResetPassword(),
detail: false,
disabled$: of(!this.secure),
},
{
title: 'Experimental Features',
description: 'Try out new and potentially unstable new features',
@@ -510,11 +618,7 @@ export class ServerShowPage {
description: 'Get help from the Start9 team and community',
icon: 'chatbubbles-outline',
action: () =>
window.open(
'https://start9.com/contact',
'_blank',
'noreferrer',
),
window.open('https://start9.com/contact', '_blank', 'noreferrer'),
detail: true,
disabled$: of(false),
},
@@ -616,3 +720,30 @@ interface SettingBtn {
detail: boolean
disabled$: Observable<boolean>
}
const PasswordSpec: ConfigSpec = {
currPass: {
type: 'string',
name: 'Current Password',
placeholder: 'CurrentPass',
nullable: false,
masked: true,
copyable: false,
},
newPass: {
type: 'string',
name: 'New Password',
placeholder: 'NewPass',
nullable: false,
masked: true,
copyable: false,
},
newPass2: {
type: 'string',
name: 'Retype New Password',
placeholder: 'NewPass',
nullable: false,
masked: true,
copyable: false,
},
}

View File

@@ -10,7 +10,7 @@ const routes: Routes = [
),
},
{
path: 'lan',
path: 'root-ca',
loadChildren: () => import('./lan/lan.module').then(m => m.LANPageModule),
},
{

View File

@@ -12,6 +12,30 @@
<ion-content class="ion-padding">
<h2>This Release</h2>
<h4>0.3.4.4</h4>
<p class="note-padding">
View the complete
<a
href="https://github.com/Start9Labs/start-os/releases/tag/v0.3.4.4"
target="_blank"
noreferrer
>
release notes
</a>
for more details.
</p>
<h6>Highlights</h6>
<ul class="spaced-list">
<li>Https over Tor for faster UI loading times</li>
<li>Change password through UI</li>
<li>Use IP address for Network Folder backups</li>
<li>
Multiple bug fixes, performance enhancements, and other small features
</li>
</ul>
<h2>Previous Releases</h2>
<h4>0.3.4.3</h4>
<p class="note-padding">
View the complete
@@ -28,12 +52,10 @@
<ul class="spaced-list">
<li>Improved Tor reliability</li>
<li>Experimental features tab</li>
<li>multiple bugfixes and general performance enhancements</li>
<li>Multiple bugfixes and general performance enhancements</li>
<li>Update branding</li>
</ul>
<h2>Previous Releases</h2>
<h4>0.3.4.2</h4>
<p class="note-padding">
View the complete

View File

@@ -1,15 +1,28 @@
<alert *ngIf="show$ | async" header="Refresh Needed" (dismiss)="onDismiss()">
Your user interface is cached and out of date. Hard refresh the page to get
the latest UI.
<ul>
<li>
<b>On Mac</b>
: cmd + shift + R
</li>
<li>
<b>On Linux/Windows</b>
: ctrl + shift + R
</li>
</ul>
<a alertButton class="enter-click" role="cancel">Ok</a>
<ng-container *ngIf="!onPwa; else pwa">
Your user interface is cached and out of date. Hard refresh the page to get
the latest UI.
<ul>
<li>
<b>On Mac</b>
: cmd + shift + R
</li>
<li>
<b>On Linux/Windows</b>
: ctrl + shift + R
</li>
<li>
<b>On Android/iOS</b>
: Browser specific, typically a refresh button in the browser menu.
</li>
</ul>
</ng-container>
<ng-template #pwa>
Your user interface is cached and out of date. Attempt to reload the PWA
using the button below. If you continue to see this message, uninstall and
reinstall the PWA.
</ng-template>
<!-- alertButton needs to be a direct child of alert element for ionic styling -->
<a *ngIf="!onPwa" alertButton class="enter-click" role="cancel">Ok</a>
<a *ngIf="onPwa" alertButton (click)="pwaReload()" role="cancel">Reload</a>
</alert>

View File

@@ -1,7 +1,9 @@
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'
import { Observable, Subject, merge } from 'rxjs'
import { merge, Observable, Subject } from 'rxjs'
import { RefreshAlertService } from './refresh-alert.service'
import { SwUpdate } from '@angular/service-worker'
import { LoadingController } from '@ionic/angular'
@Component({
selector: 'refresh-alert',
@@ -10,13 +12,36 @@ import { RefreshAlertService } from './refresh-alert.service'
})
export class RefreshAlertComponent {
private readonly dismiss$ = new Subject<boolean>()
readonly show$ = merge(this.dismiss$, this.refresh$)
onPwa = false
constructor(
@Inject(RefreshAlertService) private readonly refresh$: Observable<boolean>,
private readonly updates: SwUpdate,
private readonly loadingCtrl: LoadingController,
) {}
ngOnInit() {
this.onPwa = window.matchMedia('(display-mode: standalone)').matches
}
async pwaReload() {
const loader = await this.loadingCtrl.create({
message: 'Reloading PWA...',
})
await loader.present()
try {
// attempt to update to the latest client version available
await this.updates.activateUpdate()
} catch (e) {
console.error('Error activating update from service worker: ', e)
} finally {
loader.dismiss()
// always reload, as this resolves most out of sync cases
window.location.reload()
}
}
onDismiss() {
this.dismiss$.next(false)
}

View File

@@ -46,11 +46,11 @@ export class WidgetListComponent {
qp: { back: 'true' },
},
{
title: 'Secure LAN',
icon: 'home-outline',
title: 'Root CA',
icon: 'ribbon-outline',
color: 'var(--alt-orange)',
description: `Download and trust your server's certificate`,
link: '/system/lan',
description: `Download and trust your server's root certificate authority`,
link: '/system/root-ca',
},
{
title: 'Create Backup',

View File

@@ -35,9 +35,10 @@ export module Mock {
'shutting-down': false,
}
export const MarketplaceEos: RR.GetMarketplaceEosRes = {
version: '0.3.4.3',
version: '0.3.4.4',
headline: 'Our biggest release ever.',
'release-notes': {
'0.3.4.4': 'Some **Markdown** release _notes_ for 0.3.4.4',
'0.3.4.3': 'Some **Markdown** release _notes_ for 0.3.4.3',
'0.3.4.2': 'Some **Markdown** release _notes_ for 0.3.4.2',
'0.3.4.1': 'Some **Markdown** release _notes_ for 0.3.4.1',

View File

@@ -29,6 +29,12 @@ export module RR {
export type LogoutReq = {} // auth.logout
export type LogoutRes = null
export type ResetPasswordReq = {
'old-password': string
'new-password': string
} // auth.reset-password
export type ResetPasswordRes = null
// server
export type EchoReq = { message: string } // server.echo
@@ -86,11 +92,6 @@ export module RR {
export type KillSessionsReq = { ids: string[] } // sessions.kill
export type KillSessionsRes = null
// password
export type UpdatePasswordReq = { password: string } // password.set
export type UpdatePasswordRes = null
// notification
export type GetNotificationsReq = {

View File

@@ -53,6 +53,10 @@ export abstract class ApiService {
abstract killSessions(params: RR.KillSessionsReq): Promise<RR.KillSessionsRes>
abstract resetPassword(
params: RR.ResetPasswordReq,
): Promise<RR.ResetPasswordRes>
// server
abstract echo(params: RR.EchoReq): Promise<RR.EchoRes>
@@ -128,9 +132,6 @@ export abstract class ApiService {
abstract getEos(): Promise<RR.GetMarketplaceEosRes>
// password
// abstract updatePassword (params: RR.UpdatePasswordReq): Promise<RR.UpdatePasswordRes>
// notification
abstract getNotifications(

View File

@@ -105,6 +105,12 @@ export class LiveApiService extends ApiService {
return this.rpcRequest({ method: 'auth.session.kill', params })
}
async resetPassword(
params: RR.ResetPasswordReq,
): Promise<RR.ResetPasswordRes> {
return this.rpcRequest({ method: 'auth.reset-password', params })
}
// server
async echo(params: RR.EchoReq): Promise<RR.EchoRes> {

View File

@@ -158,6 +158,13 @@ export class MockApiService extends ApiService {
return null
}
async resetPassword(
params: RR.ResetPasswordReq,
): Promise<RR.ResetPasswordRes> {
await pauseFor(2000)
return null
}
// server
async echo(params: RR.EchoReq): Promise<RR.EchoRes> {
@@ -388,12 +395,6 @@ export class MockApiService extends ApiService {
return Mock.MarketplaceEos
}
// password
// async updatePassword (params: RR.UpdatePasswordReq): Promise<RR.UpdatePasswordRes> {
// await pauseFor(2000)
// return null
// }
// notification
async getNotifications(

View File

@@ -39,7 +39,7 @@ export const mockPatchData: DataModel = {
country: 'us',
'last-backup': new Date(new Date().valueOf() - 604800001).toISOString(),
'lan-address': 'https://adjective-noun.local',
'tor-address': 'http://myveryownspecialtoraddress.onion',
'tor-address': 'https://myveryownspecialtoraddress.onion',
'ip-info': {
eth0: {
ipv4: '10.0.0.1',

View File

@@ -278,6 +278,7 @@ export class MarketplaceService implements AbstractMarketplaceService {
> = {},
): Observable<MarketplacePkg[]> {
return this.patch.watch$('server-info', 'eos-version-compat').pipe(
take(1),
switchMap(versionCompat => {
const qp: RR.GetMarketplacePackagesReq = {
...params,

View File

@@ -4,7 +4,7 @@
export const environment = {
production: false,
useServiceWorker: true,
useServiceWorker: false,
}
/*

View File

@@ -14,6 +14,11 @@
<meta name="msapplication-tap-highlight" content="no" />
<link rel="icon" type="image/x-icon" href="assets/icon/favicon.ico" />
<link
rel="apple-touch-icon"
sizes="256x256"
href="assets/img/icon_apple_touch.png"
/>
<link rel="manifest" href="manifest.webmanifest" />
<meta name="theme-color" content="#ff5b71" />
</head>

View File

@@ -5,8 +5,8 @@
"background_color": "#1e1e1e",
"display": "standalone",
"scope": ".",
"start_url": "/",
"id": "/",
"start_url": "/?version=0344",
"id": "/?version=0344",
"icons": [
{
"src": "assets/img/icon_pwa.png",

1681
libs/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,7 @@
# Reason for this being is that we need to create a snapshot for the deno runtime. It wants to pull 3 files from build, and during the creation it gets embedded, but for some
# reason during the actual runtime it is looking for them. So this will create a docker in arm that creates the snaphot needed for the arm
set -e
shopt -s expand_aliases
if [ "$0" != "./build-arm-v8-snapshot.sh" ]; then
>&2 echo "Must be run from backend/workspace directory"
@@ -13,9 +14,11 @@ if tty -s; then
USE_TTY="-it"
fi
alias 'rust-gnu-builder'='docker run $USE_TTY --rm -v "$HOME/.cargo/registry":/usr/local/cargo/registry -v "$(pwd)":/home/rust/src -w /home/rust/src -P start9/rust-arm-cross:aarch64'
echo "Building "
cd ..
docker run --rm $USE_TTY -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$(pwd)":/home/rust/src start9/rust-arm-cross:aarch64 sh -c "(cd libs/ && cargo build -p snapshot_creator --release )"
rust-gnu-builder sh -c "(cd libs/ && cargo build -p snapshot_creator --release --target=aarch64-unknown-linux-gnu)"
cd -
echo "Creating Arm v8 Snapshot"

View File

@@ -371,7 +371,7 @@ async fn main() {
tracing::error!("Error sending to {id:?}", id = req.id);
}
}
Err(e) =>
Err(e) =>
if let Err(err) = w
.lock()
.await

View File

@@ -8,35 +8,13 @@ edition = "2021"
[dependencies]
async-trait = "0.1.56"
dashmap = "5.3.4"
deno_core = "=0.136.0"
deno_ast = { version = "=0.15.0", features = ["transpiling"] }
dprint-swc-ext = "=0.1.1"
deno_core = "0.195.0"
deno_ast = { version = "0.27.2", features = ["transpiling"] }
embassy_container_init = { path = "../embassy_container_init" }
reqwest = { version = "0.11.11" }
swc_atoms = "=0.2.11"
swc_common = "=0.18.7"
swc_config = "=0.1.1"
swc_config_macro = "=0.1.0"
swc_ecma_ast = "=0.78.1"
swc_ecma_codegen = "=0.108.6"
swc_ecma_codegen_macros = "=0.7.0"
swc_ecma_parser = "=0.104.2"
swc_ecma_transforms = "=0.154.0"
swc_ecma_transforms_base = "=0.85.4"
swc_ecma_transforms_classes = "=0.73.0"
swc_ecma_transforms_macros = "=0.3.0"
swc_ecma_transforms_proposal = "=0.107.0"
swc_ecma_transforms_react = "=0.114.1"
swc_ecma_transforms_typescript = "=0.117.2"
swc_ecma_utils = "=0.85.1"
swc_ecma_visit = "=0.64.0"
swc_ecmascript = "=0.157.0"
swc_eq_ignore_macros = "=0.1.0"
swc_macros_common = "=0.3.5"
swc_visit = "=0.3.0"
swc_visit_macros = "=0.3.1"
sha2 = "0.10.2"
itertools = "0.10.5"
lazy_static = "1.4.0"
models = { path = "../models" }
helpers = { path = "../helpers" }
serde = { version = "1.0", features = ["derive", "rc"] }

View File

@@ -141,7 +141,7 @@ const removeFile = (
{ volumeId = requireParam("volumeId"), path = requireParam("path") } =
requireParam("options"),
) => Deno.core.opAsync("remove_file", volumeId, path);
const isSandboxed = () => Deno.core.opSync("is_sandboxed");
const isSandboxed = () => Deno.core.ops["is_sandboxed"]();
const writeJsonFile = (
{
@@ -265,15 +265,15 @@ const getServiceConfig = async (
);
};
const started = () => Deno.core.opSync("set_started");
const started = () => Deno.core.ops.set_started();
const restart = () => Deno.core.opAsync("restart");
const start = () => Deno.core.opAsync("start");
const stop = () => Deno.core.opAsync("stop");
const currentFunction = Deno.core.opSync("current_function");
const input = Deno.core.opSync("get_input");
const variable_args = Deno.core.opSync("get_variable_args");
const setState = (x) => Deno.core.opAsync("set_value", x);
const currentFunction = Deno.core.ops.current_function();
const input = Deno.core.ops.get_input();
const variable_args = Deno.core.ops.get_variable_args();
const setState = (x) => Deno.core.ops.set_value(x);
const effects = {
bindLocal,
bindTor,

View File

@@ -9,8 +9,9 @@ use std::time::SystemTime;
use deno_core::anyhow::{anyhow, bail};
use deno_core::error::AnyError;
use deno_core::{
resolve_import, Extension, JsRuntime, ModuleLoader, ModuleSource, ModuleSourceFuture,
ModuleSpecifier, ModuleType, OpDecl, RuntimeOptions, Snapshot,
resolve_import, Extension, FastString, JsRuntime, ModuleLoader, ModuleSource,
ModuleSourceFuture, ModuleSpecifier, ModuleType, OpDecl, ResolutionKind, RuntimeOptions,
Snapshot,
};
use embassy_container_init::ProcessGroupId;
use helpers::{script_dir, spawn_local, OsApi, Rsync, UnixRpcClient};
@@ -21,6 +22,12 @@ use tokio::io::AsyncReadExt;
use tokio::sync::{mpsc, Mutex};
use tracing::instrument;
lazy_static::lazy_static! {
static ref DENO_GLOBAL_JS: ModuleSpecifier = "file:///deno_global.js".parse().unwrap();
static ref LOAD_MODULE_JS: ModuleSpecifier = "file:///loadModule.js".parse().unwrap();
static ref EMBASSY_JS: ModuleSpecifier = "file:///embassy.js".parse().unwrap();
}
pub trait PathForVolumeId: Send + Sync {
fn path_for(
&self,
@@ -32,8 +39,8 @@ pub trait PathForVolumeId: Send + Sync {
fn readonly(&self, volume_id: &VolumeId) -> bool;
}
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct JsCode(String);
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct JsCode(Arc<str>);
#[derive(Debug, Clone, Copy)]
pub enum JsError {
@@ -131,7 +138,7 @@ impl ModuleLoader for ModsLoader {
&self,
specifier: &str,
referrer: &str,
_is_main: bool,
_is_main: ResolutionKind,
) -> Result<ModuleSpecifier, AnyError> {
if referrer.contains("embassy") {
bail!("Embassy.js cannot import anything else");
@@ -143,49 +150,42 @@ impl ModuleLoader for ModsLoader {
fn load(
&self,
module_specifier: &ModuleSpecifier,
maybe_referrer: Option<ModuleSpecifier>,
maybe_referrer: Option<&ModuleSpecifier>,
is_dyn_import: bool,
) -> Pin<Box<ModuleSourceFuture>> {
let module_specifier = module_specifier.as_str().to_owned();
let module = match &*module_specifier {
"file:///deno_global.js" => Ok(ModuleSource {
module_url_specified: "file:///deno_global.js".to_string(),
module_url_found: "file:///deno_global.js".to_string(),
code: "const old_deno = Deno; Deno = null; export default old_deno"
.as_bytes()
.to_vec()
.into_boxed_slice(),
module_type: ModuleType::JavaScript,
}),
"file:///loadModule.js" => Ok(ModuleSource {
module_url_specified: "file:///loadModule.js".to_string(),
module_url_found: "file:///loadModule.js".to_string(),
code: include_str!("./artifacts/loadModule.js")
.as_bytes()
.to_vec()
.into_boxed_slice(),
module_type: ModuleType::JavaScript,
}),
"file:///embassy.js" => Ok(ModuleSource {
module_url_specified: "file:///embassy.js".to_string(),
module_url_found: "file:///embassy.js".to_string(),
code: self.code.0.as_bytes().to_vec().into_boxed_slice(),
module_type: ModuleType::JavaScript,
}),
"file:///deno_global.js" => Ok(ModuleSource::new(
ModuleType::JavaScript,
FastString::Static("const old_deno = Deno; Deno = null; export default old_deno"),
&*DENO_GLOBAL_JS,
)),
"file:///loadModule.js" => Ok(ModuleSource::new(
ModuleType::JavaScript,
FastString::Static(include_str!("./artifacts/loadModule.js")),
&*LOAD_MODULE_JS,
)),
"file:///embassy.js" => Ok(ModuleSource::new(
ModuleType::JavaScript,
self.code.0.clone().into(),
&*EMBASSY_JS,
)),
x => Err(anyhow!("Not allowed to import: {}", x)),
};
Box::pin(async move {
let module = module.and_then(|m| {
if is_dyn_import {
bail!("Will not import dynamic");
}
match &maybe_referrer {
Some(x) if x.as_str() == "file:///embassy.js" => {
bail!("Embassy is not allowed to import")
bail!("StartJS is not allowed to import")
}
_ => (),
}
module
})
Ok(m)
});
Box::pin(async move { module })
}
}
@@ -234,7 +234,7 @@ impl JsExecutionEnvironment {
format!("The file reading created error: {}", err),
));
};
buffer
buffer.into()
});
Ok(JsExecutionEnvironment {
os,
@@ -364,12 +364,11 @@ impl JsExecutionEnvironment {
callback_sender,
rsyncs: Default::default(),
};
let ext = Extension::builder()
let ext = Extension::builder("embassy")
.ops(Self::declarations())
.state(move |state| {
state.put(ext_answer_state.clone());
state.put(js_ctx.clone());
Ok(())
})
.build();
let loader = std::rc::Rc::new(self.module_loader.clone());

View File

@@ -7,5 +7,5 @@ edition = "2021"
[dependencies]
dashmap = "5.3.4"
deno_core = "=0.136.0"
deno_ast = { version = "=0.15.0", features = ["transpiling"] }
deno_core = "0.195.0"
deno_ast = { version = "0.27.2", features = ["transpiling"] }

View File

@@ -1,10 +1,7 @@
use deno_core::{JsRuntime, RuntimeOptions};
use deno_core::JsRuntimeForSnapshot;
fn main() {
let mut runtime = JsRuntime::new(RuntimeOptions {
will_snapshot: true,
..Default::default()
});
let runtime = JsRuntimeForSnapshot::new(Default::default(), Default::default());
let snapshot = runtime.snapshot();
let snapshot_slice: &[u8] = &*snapshot;

View File

@@ -487,6 +487,15 @@ dependencies = [
"winapi",
]
[[package]]
name = "chumsky"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23170228b96236b5a7299057ac284a321457700bc8c41a4476052f0f4ba5349d"
dependencies = [
"hashbrown",
]
[[package]]
name = "ciborium"
version = "0.2.0"
@@ -607,7 +616,6 @@ dependencies = [
"beau_collector",
"clap 2.34.0",
"dashmap",
"embassy-os",
"emver",
"failure",
"indexmap",
@@ -624,6 +632,7 @@ dependencies = [
"serde",
"serde_json",
"serde_yaml 0.8.26",
"start-os",
]
[[package]]
@@ -1098,6 +1107,12 @@ version = "0.15.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03d8c417d7a8cb362e0c37e5d815f5eb7c37f79ff93707329d5a194e42e54ca0"
[[package]]
name = "dyn-clone"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "304e6508efa593091e97a9abbc10f90aa7ca635b6d2784feff3c89d41dd12272"
[[package]]
name = "ecdsa"
version = "0.14.8"
@@ -1178,114 +1193,6 @@ dependencies = [
"zeroize",
]
[[package]]
name = "embassy-os"
version = "0.3.4-rev.3"
dependencies = [
"aes",
"async-compression",
"async-stream",
"async-trait",
"base32",
"base64 0.13.1",
"base64ct",
"basic-cookies",
"bimap",
"bollard",
"bytes",
"chrono",
"ciborium",
"clap 3.2.23",
"color-eyre",
"cookie",
"cookie_store 0.19.0",
"current_platform",
"digest 0.10.6",
"digest 0.9.0",
"divrem",
"ed25519",
"ed25519-dalek",
"embassy_container_init",
"emver",
"fd-lock-rs",
"futures",
"git-version",
"gpt",
"helpers",
"hex",
"hmac 0.12.1",
"http",
"hyper",
"hyper-ws-listener",
"id-pool",
"imbl 2.0.0",
"indexmap",
"ipnet",
"iprange",
"isocountry",
"itertools 0.10.5",
"josekit",
"jsonpath_lib",
"lazy_static",
"libc",
"log",
"mbrman",
"models",
"nix 0.25.1",
"nom",
"num",
"num_enum",
"openssh-keys",
"openssl",
"p256 0.12.0",
"patch-db",
"pbkdf2",
"pin-project",
"pkcs8",
"prettytable-rs",
"proptest",
"proptest-derive",
"rand 0.7.3",
"rand 0.8.5",
"regex",
"reqwest",
"reqwest_cookie_store",
"rpassword",
"rpc-toolkit",
"rust-argon2",
"scopeguard",
"serde",
"serde_json",
"serde_with 2.2.0",
"serde_yaml 0.9.16",
"sha2 0.10.6",
"sha2 0.9.9",
"simple-logging",
"sqlx",
"ssh-key",
"stderrlog",
"tar",
"thiserror",
"tokio",
"tokio-rustls",
"tokio-socks",
"tokio-stream",
"tokio-tar",
"tokio-tungstenite",
"tokio-util",
"toml",
"torut",
"tracing",
"tracing-error 0.2.0",
"tracing-futures",
"tracing-subscriber 0.3.16",
"trust-dns-server",
"typed-builder",
"url",
"uuid 1.2.2",
"zeroize",
]
[[package]]
name = "embassy_container_init"
version = "0.1.0"
@@ -1826,6 +1733,12 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hifijson"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85ef6b41c333e6dd2a4aaa59125a19b633cd17e7aaf372b2260809777bcdef4a"
[[package]]
name = "hkdf"
version = "0.12.3"
@@ -2057,6 +1970,25 @@ dependencies = [
"bitmaps 3.2.0",
]
[[package]]
name = "include_dir"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18762faeff7122e89e0857b02f7ce6fcc0d101d5e9ad2ad7846cc01d61b7f19e"
dependencies = [
"include_dir_macros",
]
[[package]]
name = "include_dir_macros"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b139284b5cf57ecfa712bcc66950bb635b31aff41c188e8a4cfc758eca374a3f"
dependencies = [
"proc-macro2 1.0.50",
"quote 1.0.23",
]
[[package]]
name = "indenter"
version = "0.3.3"
@@ -2187,6 +2119,44 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440"
[[package]]
name = "jaq-core"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb52eeac20f256459e909bd4a03bb8c4fab6a1fdbb8ed52d00f644152df48ece"
dependencies = [
"ahash",
"dyn-clone",
"hifijson",
"indexmap",
"itertools 0.10.5",
"jaq-parse",
"log",
"once_cell",
"regex",
"serde_json",
]
[[package]]
name = "jaq-parse"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f0f97f01eb9e87af3cbcc843b0dfe693fc6b0a2b9093dc8980dd9fc682826b0"
dependencies = [
"chumsky",
"serde",
]
[[package]]
name = "jaq-std"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b261109851c8687bc55eab26e6d81e96f3fdab367e2d3d5706947c218ddaf22"
dependencies = [
"bincode",
"jaq-parse",
]
[[package]]
name = "josekit"
version = "0.8.1"
@@ -2513,6 +2483,16 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54"
[[package]]
name = "new_mime_guess"
version = "4.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2d684d1b59e0dc07b37e2203ef576987473288f530082512aff850585c61b1f"
dependencies = [
"mime",
"unicase",
]
[[package]]
name = "nibble_vec"
version = "0.1.0"
@@ -4229,6 +4209,116 @@ dependencies = [
"zeroize",
]
[[package]]
name = "start-os"
version = "0.3.4-rev.4"
dependencies = [
"aes",
"async-compression",
"async-stream",
"async-trait",
"base32",
"base64 0.13.1",
"base64ct",
"basic-cookies",
"bollard",
"bytes",
"chrono",
"ciborium",
"clap 3.2.23",
"color-eyre",
"cookie",
"cookie_store 0.19.0",
"current_platform",
"digest 0.10.6",
"digest 0.9.0",
"divrem",
"ed25519",
"ed25519-dalek",
"embassy_container_init",
"emver",
"fd-lock-rs",
"futures",
"git-version",
"gpt",
"helpers",
"hex",
"hmac 0.12.1",
"http",
"hyper",
"hyper-ws-listener",
"imbl 2.0.0",
"include_dir",
"indexmap",
"ipnet",
"iprange",
"isocountry",
"itertools 0.10.5",
"jaq-core",
"jaq-std",
"josekit",
"jsonpath_lib",
"lazy_static",
"libc",
"log",
"mbrman",
"models",
"new_mime_guess",
"nix 0.25.1",
"nom",
"num",
"num_enum",
"openssh-keys",
"openssl",
"p256 0.12.0",
"patch-db",
"pbkdf2",
"pin-project",
"pkcs8",
"prettytable-rs",
"proptest",
"proptest-derive",
"rand 0.7.3",
"rand 0.8.5",
"regex",
"reqwest",
"reqwest_cookie_store",
"rpassword",
"rpc-toolkit",
"rust-argon2",
"scopeguard",
"serde",
"serde_json",
"serde_with 2.2.0",
"serde_yaml 0.9.16",
"sha2 0.10.6",
"sha2 0.9.9",
"simple-logging",
"sqlx",
"ssh-key",
"stderrlog",
"tar",
"thiserror",
"tokio",
"tokio-rustls",
"tokio-socks",
"tokio-stream",
"tokio-tar",
"tokio-tungstenite",
"tokio-util",
"toml",
"torut",
"tracing",
"tracing-error 0.2.0",
"tracing-futures",
"tracing-subscriber 0.3.16",
"trust-dns-server",
"typed-builder",
"url",
"uuid 1.2.2",
"zeroize",
]
[[package]]
name = "stderrlog"
version = "0.5.4"
@@ -4866,6 +4956,15 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81"
[[package]]
name = "unicase"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
dependencies = [
"version_check",
]
[[package]]
name = "unicode-bidi"
version = "0.3.8"

View File

@@ -11,7 +11,7 @@ anyhow = { version = "1.0.40", features = ["backtrace"] }
beau_collector = "0.2.1"
clap = "2.33.3"
dashmap = "5.3.2"
embassy-os = { path = "../../backend", default-features = false }
start-os = { path = "../../backend", default-features = false }
emver = { version = "0.1.7", git = "https://github.com/Start9Labs/emver-rs.git", features = [
"serde",
] }

View File

@@ -1,6 +1,6 @@
use std::{path::Path, process::Stdio};
use embassy::disk::main::DEFAULT_PASSWORD;
use startos::disk::main::DEFAULT_PASSWORD;
pub fn create_backup(
mountpoint: impl AsRef<Path>,
@@ -19,17 +19,21 @@ pub fn create_backup(
let mut data_cmd = std::process::Command::new("duplicity");
for exclude in exclude.lines().map(|s| s.trim()).filter(|s| !s.is_empty()) {
if exclude.to_string().starts_with('!') {
data_cmd.arg(format!(
"--include={}",
data_path
.join(exclude.to_string().trim_start_matches('!'))
.display()
)).arg("--allow-source-mismatch");
data_cmd
.arg(format!(
"--include={}",
data_path
.join(exclude.to_string().trim_start_matches('!'))
.display()
))
.arg("--allow-source-mismatch");
} else {
data_cmd.arg(format!(
"--exclude={}",
data_path.join(exclude.to_string()).display()
)).arg("--allow-source-mismatch");
data_cmd
.arg(format!(
"--exclude={}",
data_path.join(exclude.to_string()).display()
))
.arg("--allow-source-mismatch");
}
}
let data_output = data_cmd

View File

@@ -3,11 +3,11 @@ use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::path::Path;
use beau_collector::BeauCollector;
use embassy::config::action::SetResult;
use embassy::config::{spec, Config};
use embassy::s9pk::manifest::PackageId;
use embassy::status::health_check::HealthCheckId;
use linear_map::LinearMap;
use startos::config::action::SetResult;
use startos::config::{spec, Config};
use startos::s9pk::manifest::PackageId;
use startos::status::health_check::HealthCheckId;
pub mod rules;

View File

@@ -7,8 +7,8 @@ use pest::Parser;
use rand::SeedableRng;
use serde_json::Value;
use embassy::config::util::STATIC_NULL;
use embassy::config::Config;
use startos::config::util::STATIC_NULL;
use startos::config::Config;
#[derive(Parser)]
#[grammar = "config/rule_parser.pest"]

View File

@@ -19,8 +19,8 @@ use clap::{App, Arg, SubCommand};
use config::{
apply_dependency_configuration, validate_configuration, validate_dependency_configuration,
};
use embassy::config::action::ConfigRes;
use serde_json::json;
use startos::config::action::ConfigRes;
const PROPERTIES_FALLBACK_MESSAGE: &str =
"Could not find properties. The service might still be starting";