Compare commits

..

82 Commits

Author SHA1 Message Date
Aiden McClelland
2191707b94 wip: iroh 2025-08-31 15:43:34 -06:00
Aiden McClelland
63a4bba19a fix gha sccache 2025-08-29 13:32:12 -06:00
Aiden McClelland
d64b80987c support for sccache 2025-08-29 13:07:29 -06:00
Aiden McClelland
fbea3c56e6 clean up logs 2025-08-29 11:59:36 -06:00
Aiden McClelland
58b6b5c4ea Merge branch 'feature/proxies' of github.com:Start9Labs/start-os into feature/proxies 2025-08-29 11:48:31 -06:00
Aiden McClelland
369e559518 fix file_stream and remove non-terminating test 2025-08-29 11:48:30 -06:00
Alex Inkin
ca39ffb9eb refactor: fix multiple comments (#3013)
* refactor: fix multiple comments

* styling changes, add documentation to sidebar

* translations for dns page

* refactor: subtle colors

* rearrange service page

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
2025-08-29 11:37:34 -06:00
Aiden McClelland
8163db7ac3 socks5 proxy working 2025-08-29 11:19:30 -06:00
Aiden McClelland
b3b031ed47 wip: debugging tor 2025-08-27 15:10:54 -06:00
Matt Hill
c5fa09c4d4 handle wh file uploads 2025-08-27 10:06:39 -06:00
Alex Inkin
b7438ef155 refactor: refactor forms components and remove legacy Taiga UI package (#3012) 2025-08-27 09:57:49 -06:00
Matt Hill
2a27716e29 remove unnecessary truthy check 2025-08-26 22:16:57 -06:00
Matt Hill
7a94086d45 move status column in service list 2025-08-26 13:59:16 -06:00
Matt Hill
ec72fb4bfd fix showing dns records 2025-08-26 13:08:24 -06:00
Matt Hill
9eaaa85625 implement toggling gateways for service interface 2025-08-26 12:29:14 -06:00
Aiden McClelland
f876cd796e Merge branch 'feature/proxies' of github.com:Start9Labs/start-os into feature/proxies 2025-08-26 12:13:41 -06:00
Aiden McClelland
9fe9608560 misc fixes 2025-08-26 12:13:39 -06:00
Matt Hill
303f6a55ac Merge branch 'feature/proxies' of github.com:Start9Labs/start-os into feature/proxies 2025-08-26 11:37:12 -06:00
Aiden McClelland
ff686d3c52 Merge branch 'feature/proxies' of github.com:Start9Labs/start-os into feature/proxies 2025-08-25 19:29:52 -06:00
Aiden McClelland
f4cf94acd2 fix dns 2025-08-25 19:29:39 -06:00
Matt Hill
0709a5c242 reason instead of description 2025-08-24 10:24:48 -06:00
Matt Hill
701db35ca3 remove logs 2025-08-24 09:41:58 -06:00
Matt Hill
57bdc400b4 honor hidden form values 2025-08-24 09:40:24 -06:00
Matt Hill
611e19da26 placeholder for empty service interfaces table 2025-08-24 08:54:44 -06:00
Matt Hill
0e9b9fce3e simple renaming 2025-08-24 08:46:12 -06:00
Aiden McClelland
d6d91822cc coukd work 2025-08-22 08:53:38 -06:00
Aiden McClelland
5bee2cef96 fix deadlock 2025-08-21 18:40:53 -06:00
Aiden McClelland
359146f02c wip 2025-08-20 14:46:15 -06:00
Matt Hill
d564471825 more translations 2025-08-20 11:45:17 -06:00
Alex Inkin
931505ff08 fix: refactor legacy components (#3010)
* fix: comments

* fix: refactor legacy components

* remove default again

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
2025-08-19 08:13:36 -06:00
Alex Inkin
0709ea65d7 fix: comments (#3009)
* fix: comments

* undo default

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
2025-08-19 08:10:14 -06:00
Aiden McClelland
75a20ae5c5 it builds 2025-08-18 18:12:03 -06:00
Matt Hill
aaf2361909 add missing translations 2025-08-18 15:16:43 -06:00
Matt Hill
17c4f3a1e8 fix dns form 2025-08-17 09:01:09 -06:00
Matt Hill
a0a2c20b08 fix all types 2025-08-16 23:14:19 -06:00
Aiden McClelland
f7f0b7dc1a revert to ts-rs v9 2025-08-16 22:33:53 -06:00
Aiden McClelland
d06c443c7d clean up tech debt, bump dependencies 2025-08-15 18:32:27 -06:00
Aiden McClelland
7094d1d939 update types 2025-08-15 18:05:52 -06:00
Aiden McClelland
8f573386c6 with todos 2025-08-15 16:07:23 -06:00
Matt Hill
bfc88a2225 fix sort functions for public and private domains 2025-08-13 14:28:53 -06:00
Matt Hill
d5bb537368 dns 2025-08-13 13:27:05 -06:00
Matt Hill
3abae65b22 better icon for restart tor 2025-08-13 10:54:07 -06:00
Matt Hill
3848e8f2df restart tor instead of reset 2025-08-13 10:53:46 -06:00
Matt Hill
63323faa97 nix StartOS domains, implement public and private domains at interface scope 2025-08-11 23:01:31 -06:00
Matt Hill
e8b7a35d43 public domain, max width, descriptions for dns 2025-08-11 10:03:35 -06:00
waterplea
da9a1b99d9 fix: dns testing 2025-08-11 13:50:58 +07:00
Matt Hill
68780ccbdd forms for adding domain, rework things based on new ideas 2025-08-10 23:33:05 -06:00
Aiden McClelland
022f7134be wip: start-tunnel & fix build 2025-08-09 21:57:32 -06:00
Matt Hill
b4491a3f39 only translations left 2025-08-09 09:29:47 -06:00
waterplea
29ddfad9d7 fix: address comments 2025-08-09 17:45:31 +07:00
Matt Hill
86a24ec067 domains preferred 2025-08-08 21:00:32 -06:00
Matt Hill
35ace3997b MVP of service interface page 2025-08-08 20:57:16 -06:00
Aiden McClelland
4f24658d33 fix unnecessary export 2025-08-08 11:12:11 -06:00
Aiden McClelland
3a84cc97fe comments 2025-08-07 17:21:09 -06:00
Aiden McClelland
3845550e90 best address logic 2025-08-07 17:15:23 -06:00
Matt Hill
4d5ff1a97b start sorting addresses 2025-08-07 13:47:27 -06:00
Matt Hill
b864816033 better placeholder for no addresses 2025-08-07 09:08:41 -06:00
Matt Hill
2762076683 minor 2025-08-07 09:03:54 -06:00
Matt Hill
8796e41ea0 merge 2025-08-07 08:18:47 -06:00
waterplea
8edb7429f5 refactor: styles for interfaces page 2025-08-07 18:53:35 +07:00
Matt Hill
5109efcee2 different options for clearnet domains 2025-08-06 18:45:41 -06:00
Matt Hill
177232ab28 start service interface page, WIP 2025-08-06 17:55:21 -06:00
Aiden McClelland
d6dfaf8feb domains api + migration 2025-08-06 14:29:35 -06:00
Aiden McClelland
ea12251a7e add ip util to sdk 2025-08-06 11:14:41 -06:00
waterplea
b35a89da29 refactor: add file control to form service 2025-08-06 19:07:21 +07:00
Matt Hill
d8d1009417 domains mostly finished 2025-08-05 17:29:48 -06:00
Aiden McClelland
3835562200 fix fe types 2025-08-05 17:14:17 -06:00
Aiden McClelland
0d227e62dc Merge branch 'feature/proxies' of github.com:Start9Labs/start-os into feature/proxies 2025-08-05 17:07:27 -06:00
Aiden McClelland
10af26116d refactor public/private gateways 2025-08-05 17:07:25 -06:00
Matt Hill
f8b03ea917 certificate authorities 2025-08-05 13:03:04 -06:00
Matt Hill
4a2777c52f domains and acme refactor 2025-08-05 09:29:04 -06:00
waterplea
86dbf26253 refactor: gateways page 2025-08-05 17:39:48 +07:00
waterplea
32999fc55f refactor: domains page 2025-08-04 19:34:57 +07:00
Matt Hill
ea2b1f5920 edit instead of chnage acme and change gateway 2025-08-01 22:59:10 -06:00
Matt Hill
716ed64aa8 show and test dns 2025-07-31 19:57:04 -06:00
Matt Hill
f23659f4ea dont show hidden actions 2025-07-31 13:42:43 -06:00
Matt Hill
daf584b33e add domains and gateways, remove routers, fix docs links 2025-07-30 15:33:13 -06:00
Aiden McClelland
e6b7390a61 wip start-tunneld 2025-07-24 18:33:55 -06:00
Aiden McClelland
84f554269f proxy -> tunnel, implement backend apis 2025-07-23 15:44:57 -06:00
Matt Hill
21adce5c5d fix file type 2025-07-22 17:07:20 -06:00
Aiden McClelland
d3e7e37f59 backend changes 2025-07-22 16:48:16 -06:00
Matt Hill
4d9709eb1c add support for inbound proxies 2025-07-22 16:40:31 -06:00
137 changed files with 6392 additions and 4724 deletions

View File

@@ -264,7 +264,7 @@ container-runtime/dist/node_modules/.package-lock.json container-runtime/dist/pa
container-runtime/rootfs.$(ARCH).squashfs: container-runtime/debian.$(ARCH).squashfs container-runtime/container-runtime.service container-runtime/update-image.sh container-runtime/deb-install.sh container-runtime/dist/index.js container-runtime/dist/node_modules/.package-lock.json core/target/$(ARCH)-unknown-linux-musl/release/containerbox container-runtime/rootfs.$(ARCH).squashfs: container-runtime/debian.$(ARCH).squashfs container-runtime/container-runtime.service container-runtime/update-image.sh container-runtime/deb-install.sh container-runtime/dist/index.js container-runtime/dist/node_modules/.package-lock.json core/target/$(ARCH)-unknown-linux-musl/release/containerbox
ARCH=$(ARCH) REQUIRES=linux ./build/os-compat/run-compat.sh ./container-runtime/update-image.sh ARCH=$(ARCH) REQUIRES=linux ./build/os-compat/run-compat.sh ./container-runtime/update-image.sh
build/lib/depends build/lib/conflicts: $(ENVIRONMENT_FILE) build/dpkg-deps/* build/lib/depends build/lib/conflicts: build/dpkg-deps/*
build/dpkg-deps/generate.sh build/dpkg-deps/generate.sh
$(FIRMWARE_ROMS): build/lib/firmware.json download-firmware.sh $(PLATFORM_FILE) $(FIRMWARE_ROMS): build/lib/firmware.json download-firmware.sh $(PLATFORM_FILE)

View File

@@ -1,123 +1,34 @@
#!/bin/sh #!/bin/sh
printf "\n"
printf "Welcome to\n"
cat << "ASCII"
parse_essential_db_info() { ███████
DB_DUMP="/tmp/startos_db.json" █ █ █
█ █ █ █
█ █ █ █
█ █ █ █
█ █ █ █
█ █
███████
if command -v start-cli >/dev/null 2>&1; then _____ __ ___ __ __
start-cli db dump > "$DB_DUMP" 2>/dev/null || return 1 (_ | /\ |__) | / \(_
else __) | / \| \ | \__/__)
return 1 ASCII
fi printf " v$(cat /usr/lib/startos/VERSION.txt)\n\n"
printf " %s (%s %s)\n" "$(uname -o)" "$(uname -r)" "$(uname -m)"
if command -v jq >/dev/null 2>&1 && [ -f "$DB_DUMP" ]; then printf " Git Hash: $(cat /usr/lib/startos/GIT_HASH.txt)"
HOSTNAME=$(jq -r '.value.serverInfo.hostname // "unknown"' "$DB_DUMP" 2>/dev/null) if [ -n "$(cat /usr/lib/startos/ENVIRONMENT.txt)" ]; then
VERSION=$(jq -r '.value.serverInfo.version // "unknown"' "$DB_DUMP" 2>/dev/null) printf " ~ $(cat /usr/lib/startos/ENVIRONMENT.txt)\n"
RAM_BYTES=$(jq -r '.value.serverInfo.ram // 0' "$DB_DUMP" 2>/dev/null)
WAN_IP=$(jq -r '.value.serverInfo.network.gateways[].ipInfo.wanIp // "unknown"' "$DB_DUMP" 2>/dev/null | head -1)
NTP_SYNCED=$(jq -r '.value.serverInfo.ntpSynced // false' "$DB_DUMP" 2>/dev/null)
if [ "$RAM_BYTES" != "0" ] && [ "$RAM_BYTES" != "null" ]; then
RAM_GB=$(echo "scale=1; $RAM_BYTES / 1073741824" | bc 2>/dev/null || echo "unknown")
else
RAM_GB="unknown"
fi
RUNNING_SERVICES=$(jq -r '[.value.packageData[] | select(.status.main == "running")] | length' "$DB_DUMP" 2>/dev/null)
TOTAL_SERVICES=$(jq -r '.value.packageData | length' "$DB_DUMP" 2>/dev/null)
rm -f "$DB_DUMP"
return 0
else
rm -f "$DB_DUMP" 2>/dev/null
return 1
fi
}
DB_INFO_AVAILABLE=0
if parse_essential_db_info; then
DB_INFO_AVAILABLE=1
fi
if [ "$DB_INFO_AVAILABLE" -eq 1 ] && [ "$VERSION" != "unknown" ]; then
version_display="v$VERSION"
else else
version_display="v$(cat /usr/lib/startos/VERSION.txt 2>/dev/null || echo 'unknown')" printf "\n"
fi fi
printf "\n\033[1;37m ▄▄▀▀▀▀▀▄▄\033[0m\n" printf "\n"
printf "\033[1;37m ▄▀ ▄ ▀▄ ▄▄▄▄▄ ▄▄▄▄▄▄▄ ▄ ▄▄▄▄▄ ▄▄▄▄▄▄▄ \033[1;31m▄██████▄ ▄██████\033[0m\n" printf " * Documentation: https://docs.start9.com\n"
printf "\033[1;37m █ █ █ █ █ █ █ █ █ ▀▄ █ \033[1;31m██ ██ ██ \033[0m\n" printf " * Management: https://%s.local\n" "$(hostname)"
printf "\033[1;37m█ █ █ █ ▀▄▄▄▄ █ █ █ █ ▄▄▄▀ █ \033[1;31m██ ██ ▀█████▄\033[0m\n" printf " * Support: https://start9.com/contact\n"
printf "\033[1;37m█ █ █ █ █ █ █ █ █ ▀▄ █ \033[1;31m██ ██ ██\033[0m\n" printf " * Source Code: https://github.com/Start9Labs/start-os\n"
printf "\033[1;37m █ █ █ █ ▄▄▄▄▄▀ █ █ █ █ ▀▄ █ \033[1;31m▀██████▀ ██████▀\033[0m\n" printf " * License: MIT\n"
printf "\033[1;37m █ █\033[0m\n" printf "\n"
printf "\033[1;37m ▀▀▄▄▄▀▀ $version_display\033[0m\n\n"
uptime_str=$(uptime | awk -F'up ' '{print $2}' | awk -F',' '{print $1}' | sed 's/^ *//')
if [ "$DB_INFO_AVAILABLE" -eq 1 ] && [ "$RAM_GB" != "unknown" ]; then
memory_used=$(free -m | awk 'NR==2{printf "%.0fMB", $3}')
memory_display="$memory_used / ${RAM_GB}GB"
else
memory_display=$(free -m | awk 'NR==2{printf "%.0fMB / %.0fMB", $3, $2}')
fi
root_usage=$(df -h / | awk 'NR==2{printf "%s (%s free)", $5, $4}')
if [ -d "/media/startos/data/package-data" ]; then
data_usage=$(df -h /media/startos/data/package-data | awk 'NR==2{printf "%s (%s free)", $5, $4}')
else
data_usage="N/A"
fi
if [ "$DB_INFO_AVAILABLE" -eq 1 ]; then
services_text="$RUNNING_SERVICES/$TOTAL_SERVICES running"
else
services_text="Unknown"
fi
local_ip=$(ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="src") print $(i+1)}' | head -1)
if [ -z "$local_ip" ]; then local_ip="N/A"; fi
if [ "$DB_INFO_AVAILABLE" -eq 1 ] && [ "$WAN_IP" != "unknown" ]; then
wan_ip="$WAN_IP"
else
wan_ip="N/A"
fi
printf " \033[1;37m┌─ SYSTEM STATUS ───────────────────────────────────────────────────┐\033[0m\n"
printf " \033[1;37m│\033[0m %-8s \033[0;33m%-22s\033[0m %-8s \033[0;33m%-23s\033[0m \033[1;37m│\033[0m\n" "Uptime:" "$uptime_str" "Memory:" "$memory_display"
printf " \033[1;37m│\033[0m %-8s \033[0;33m%-22s\033[0m %-8s \033[0;33m%-23s\033[0m \033[1;37m│\033[0m\n" "Root:" "$root_usage" "Data:" "$data_usage"
if [ "$DB_INFO_AVAILABLE" -eq 1 ]; then
if [ "$RUNNING_SERVICES" -eq "$TOTAL_SERVICES" ] && [ "$TOTAL_SERVICES" -gt 0 ]; then
printf " \033[1;37m│\033[0m %-8s \033[0;32m%-22s\033[0m %-8s \033[0;33m%-23s\033[0m \033[1;37m│\033[0m\n" "Services:" "$services_text" "WAN:" "$wan_ip"
elif [ "$RUNNING_SERVICES" -gt 0 ]; then
printf " \033[1;37m│\033[0m %-8s \033[0;33m%-22s\033[0m %-8s \033[0;33m%-23s\033[0m \033[1;37m│\033[0m\n" "Services:" "$services_text" "WAN:" "$wan_ip"
else
printf " \033[1;37m│\033[0m %-8s \033[0;31m%-22s\033[0m %-8s \033[0;33m%-23s\033[0m \033[1;37m│\033[0m\n" "Services:" "$services_text" "WAN:" "$wan_ip"
fi
else
printf " \033[1;37m│\033[0m %-8s \033[0;37m%-22s\033[0m %-8s \033[0;33m%-23s\033[0m \033[1;37m│\033[0m\n" "Services:" "$services_text" "WAN:" "$wan_ip"
fi
if [ "$DB_INFO_AVAILABLE" -eq 1 ] && [ "$NTP_SYNCED" = "true" ]; then
printf " \033[1;37m│\033[0m %-8s \033[0;33m%-22s\033[0m %-8s \033[0;32m%-23s\033[0m \033[1;37m│\033[0m\n" "Local:" "$local_ip" "NTP:" "Synced"
elif [ "$DB_INFO_AVAILABLE" -eq 1 ] && [ "$NTP_SYNCED" = "false" ]; then
printf " \033[1;37m│\033[0m %-8s \033[0;33m%-22s\033[0m %-8s \033[0;31m%-23s\033[0m \033[1;37m│\033[0m\n" "Local:" "$local_ip" "NTP:" "Not Synced"
else
printf " \033[1;37m│\033[0m %-8s \033[0;33m%-22s\033[0m %-8s \033[0;37m%-23s\033[0m \033[1;37m│\033[0m\n" "Local:" "$local_ip" "NTP:" "Unknown"
fi
printf " \033[1;37m└───────────────────────────────────────────────────────────────────┘\033[0m"
if [ "$DB_INFO_AVAILABLE" -eq 1 ] && [ "$HOSTNAME" != "unknown" ]; then
web_url="https://$HOSTNAME.local"
else
web_url="https://$(hostname).local"
fi
printf "\n \033[1;37m┌──────────────────────────────────────────────────── QUICK ACCESS ─┐\033[0m\n"
printf " \033[1;37m│\033[0m Web Interface: \033[0;36m%-50s\033[0m \033[1;37m│\033[0m\n" "$web_url"
printf " \033[1;37m│\033[0m Documentation: \033[0;36m%-50s\033[0m \033[1;37m│\033[0m\n" "https://staging.docs.start9.com"
printf " \033[1;37m│\033[0m Support: \033[0;36m%-50s\033[0m \033[1;37m│\033[0m\n" "https://start9.com/contact"
printf " \033[1;37m└───────────────────────────────────────────────────────────────────┘\033[0m\n\n"

View File

@@ -2,7 +2,6 @@
set -e set -e
mkdir -p /run/systemd/resolve mkdir -p /run/systemd/resolve
echo "nameserver 8.8.8.8" > /run/systemd/resolve/stub-resolv.conf echo "nameserver 8.8.8.8" > /run/systemd/resolve/stub-resolv.conf
@@ -14,7 +13,6 @@ source ~/.bashrc
nvm install 22 nvm install 22
ln -s $(which node) /usr/bin/node ln -s $(which node) /usr/bin/node
sed -i '/\(^\|#\)DNSStubListener=/c\DNSStubListener=no' /etc/systemd/resolved.conf
sed -i '/\(^\|#\)Storage=/c\Storage=persistent' /etc/systemd/journald.conf sed -i '/\(^\|#\)Storage=/c\Storage=persistent' /etc/systemd/journald.conf
sed -i '/\(^\|#\)Compress=/c\Compress=yes' /etc/systemd/journald.conf sed -i '/\(^\|#\)Compress=/c\Compress=yes' /etc/systemd/journald.conf
sed -i '/\(^\|#\)SystemMaxUse=/c\SystemMaxUse=1G' /etc/systemd/journald.conf sed -i '/\(^\|#\)SystemMaxUse=/c\SystemMaxUse=1G' /etc/systemd/journald.conf
@@ -22,7 +20,4 @@ sed -i '/\(^\|#\)ForwardToSyslog=/c\ForwardToSyslog=no' /etc/systemd/journald.co
systemctl enable container-runtime.service systemctl enable container-runtime.service
rm -rf /run/systemd rm -rf /run/systemd
rm /etc/resolv.conf
echo "nameserver 10.0.3.1" > /etc/resolv.conf

View File

@@ -38,7 +38,7 @@
}, },
"../sdk/dist": { "../sdk/dist": {
"name": "@start9labs/start-sdk", "name": "@start9labs/start-sdk",
"version": "0.4.0-beta.38", "version": "0.4.0-beta.36",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@iarna/toml": "^3.0.0", "@iarna/toml": "^3.0.0",

View File

@@ -40,7 +40,7 @@ sudo chown 0:0 tmp/combined/lib/systemd/system/container-runtime.service
sudo cp container-runtime-failure.service tmp/combined/lib/systemd/system/container-runtime-failure.service sudo cp container-runtime-failure.service tmp/combined/lib/systemd/system/container-runtime-failure.service
sudo chown 0:0 tmp/combined/lib/systemd/system/container-runtime-failure.service sudo chown 0:0 tmp/combined/lib/systemd/system/container-runtime-failure.service
sudo cp ../core/target/$ARCH-unknown-linux-musl/release/containerbox tmp/combined/usr/bin/start-container sudo cp ../core/target/$ARCH-unknown-linux-musl/release/containerbox tmp/combined/usr/bin/start-container
echo -e '#!/bin/bash\nexec start-container "$@"' | sudo tee tmp/combined/usr/bin/start-cli # TODO: remove echo -e '#!/bin/bash\nexec start-container $@' | sudo tee tmp/combined/usr/bin/start-cli # TODO: remove
sudo chmod +x tmp/combined/usr/bin/start-cli sudo chmod +x tmp/combined/usr/bin/start-cli
sudo chown 0:0 tmp/combined/usr/bin/start-container sudo chown 0:0 tmp/combined/usr/bin/start-container
echo container-runtime | sha256sum | head -c 32 | cat - <(echo) | sudo tee tmp/combined/etc/machine-id echo container-runtime | sha256sum | head -c 32 | cat - <(echo) | sudo tee tmp/combined/etc/machine-id

2095
core/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,60 +5,50 @@ cd "$(dirname "${BASH_SOURCE[0]}")"
set -ea set -ea
shopt -s expand_aliases shopt -s expand_aliases
if [ -z "${ARCH:-}" ]; then if [ -z "$ARCH" ]; then
ARCH=$(uname -m) ARCH=$(uname -m)
fi fi
if [ "$ARCH" = "arm64" ]; then if [ "$ARCH" = "arm64" ]; then
ARCH="aarch64" ARCH="aarch64"
fi fi
if [ -z "${KERNEL_NAME:-}" ]; then if [ -z "$KERNEL_NAME" ]; then
KERNEL_NAME=$(uname -s) KERNEL_NAME=$(uname -s)
fi fi
if [ -z "${TARGET:-}" ]; then if [ -z "$TARGET" ]; then
if [ "$KERNEL_NAME" = "Linux" ]; then if [ "$KERNEL_NAME" = "Linux" ]; then
TARGET="$ARCH-unknown-linux-musl" TARGET="$ARCH-unknown-linux-musl"
elif [ "$KERNEL_NAME" = "Darwin" ]; then elif [ "$KERNEL_NAME" = "Darwin" ]; then
TARGET="$ARCH-apple-darwin" TARGET="$ARCH-apple-darwin"
else else
>&2 echo "unknown kernel $KERNEL_NAME" >&2 echo "unknown kernel $KERNEL_NAME"
exit 1 exit 1
fi fi
fi fi
USE_TTY= USE_TTY=
if tty -s; then if tty -s; then
USE_TTY="-it" USE_TTY="-it"
fi fi
cd .. cd ..
FEATURES="$(echo $ENVIRONMENT | sed 's/-/,/g')"
# Ensure GIT_HASH.txt exists if not created by higher-level build steps
if [ ! -f GIT_HASH.txt ] && command -v git >/dev/null 2>&1; then
git rev-parse HEAD > GIT_HASH.txt || true
fi
FEATURES="$(echo "${ENVIRONMENT:-}" | sed 's/-/,/g')"
FEATURE_ARGS="cli"
if [ -n "$FEATURES" ]; then
FEATURE_ARGS="$FEATURE_ARGS,$FEATURES"
fi
RUSTFLAGS="" RUSTFLAGS=""
if [[ "${ENVIRONMENT:-}" =~ (^|-)unstable($|-) ]]; then
RUSTFLAGS="--cfg tokio_unstable" if [[ "${ENVIRONMENT}" =~ (^|-)unstable($|-) ]]; then
RUSTFLAGS="--cfg tokio_unstable"
fi fi
if command -v zig >/dev/null 2>&1 && [ "${ENFORCE_USE_DOCKER:-0}" != "1" ]; then if which zig > /dev/null && [ "$ENFORCE_USE_DOCKER" != 1 ]; do
echo "FEATURES=\"$FEATURES\"" echo "FEATURES=\"$FEATURES\""
echo "RUSTFLAGS=\"$RUSTFLAGS\"" echo "RUSTFLAGS=\"$RUSTFLAGS\""
RUSTFLAGS=$RUSTFLAGS sh -c "cd core && cargo zigbuild --release --no-default-features --features $FEATURE_ARGS --locked --bin start-cli --target=$TARGET" RUSTFLAGS=$RUSTFLAGS sh -c "cd core && cargo zigbuild --release --no-default-features --features cli,$FEATURES --locked --bin start-cli --target=$TARGET"
else else
alias 'rust-zig-builder'='docker run '"$USE_TTY"' --rm -e "RUSTFLAGS=$RUSTFLAGS" -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$HOME/.cargo/git":/root/.cargo/git -v "$(pwd)":/home/rust/src -w /home/rust/src -P messense/cargo-zigbuild' alias 'rust-zig-builder'='docker run $USE_TTY --rm -e "RUSTFLAGS=$RUSTFLAGS" -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$HOME/.cargo/git":/root/.cargo/git -v "$(pwd)":/home/rust/src -w /home/rust/src -P messense/cargo-zigbuild'
RUSTFLAGS=$RUSTFLAGS rust-zig-builder sh -c "cd core && cargo zigbuild --release --no-default-features --features $FEATURE_ARGS --locked --bin start-cli --target=$TARGET" RUSTFLAGS=$RUSTFLAGS rust-zig-builder sh -c "cd core && cargo zigbuild --release --no-default-features --features cli,$FEATURES --locked --bin start-cli --target=$TARGET"
if [ "$(ls -nd "core/target/$TARGET/release/start-cli" | awk '{ print $3 }')" != "$UID" ]; then if [ "$(ls -nd core/target/$TARGET/release/start-cli | awk '{ print $3 }')" != "$UID" ]; then
rust-zig-builder sh -c "cd core && chown -R $UID:$UID target && chown -R $UID:$UID /root/.cargo" rust-zig-builder sh -c "cd core && chown -R $UID:$UID target && chown -R $UID:$UID /root/.cargo"
fi fi
fi fi

View File

@@ -13,6 +13,7 @@ color-eyre = "0.6.2"
ed25519-dalek = { version = "2.0.0", features = ["serde"] } ed25519-dalek = { version = "2.0.0", features = ["serde"] }
gpt = "4.1.0" gpt = "4.1.0"
lazy_static = "1.4" lazy_static = "1.4"
lettre = { version = "0.11", default-features = false }
mbrman = "0.6.0" mbrman = "0.6.0"
exver = { version = "0.2.0", git = "https://github.com/Start9Labs/exver-rs.git", features = [ exver = { version = "0.2.0", git = "https://github.com/Start9Labs/exver-rs.git", features = [
"serde", "serde",

View File

@@ -1,6 +1,5 @@
use std::borrow::Cow; use std::borrow::Cow;
use std::path::Path; use std::path::Path;
use std::str::FromStr;
use base64::Engine; use base64::Engine;
use color_eyre::eyre::eyre; use color_eyre::eyre::eyre;
@@ -15,26 +14,28 @@ use crate::{mime, Error, ErrorKind, ResultExt};
#[derive(Clone, TS)] #[derive(Clone, TS)]
#[ts(type = "string")] #[ts(type = "string")]
pub struct DataUrl<'a> { pub struct DataUrl<'a> {
pub mime: InternedString, mime: InternedString,
pub data: Cow<'a, [u8]>, data: Cow<'a, [u8]>,
} }
impl<'a> DataUrl<'a> { impl<'a> DataUrl<'a> {
pub const DEFAULT_MIME: &'static str = "application/octet-stream"; pub const DEFAULT_MIME: &'static str = "application/octet-stream";
pub const MAX_SIZE: u64 = 100 * 1024; pub const MAX_SIZE: u64 = 100 * 1024;
fn to_string(&self) -> String { // data:{mime};base64,{data}
pub fn to_string(&self) -> String {
use std::fmt::Write; use std::fmt::Write;
let mut res = String::with_capacity(self.len()); let mut res = String::with_capacity(self.data_url_len_without_mime() + self.mime.len());
write!(&mut res, "{self}").unwrap(); let _ = write!(res, "data:{};base64,", self.mime);
base64::engine::general_purpose::STANDARD.encode_string(&self.data, &mut res);
res res
} }
fn len_without_mime(&self) -> usize { fn data_url_len_without_mime(&self) -> usize {
5 + 8 + (4 * self.data.len() / 3) + 3 5 + 8 + (4 * self.data.len() / 3) + 3
} }
pub fn len(&self) -> usize { pub fn data_url_len(&self) -> usize {
self.len_without_mime() + self.mime.len() self.data_url_len_without_mime() + self.mime.len()
} }
pub fn from_slice(mime: &str, data: &'a [u8]) -> Self { pub fn from_slice(mime: &str, data: &'a [u8]) -> Self {
@@ -43,10 +44,6 @@ impl<'a> DataUrl<'a> {
data: Cow::Borrowed(data), data: Cow::Borrowed(data),
} }
} }
pub fn canonical_ext(&self) -> Option<&'static str> {
mime::unmime(&self.mime)
}
} }
impl DataUrl<'static> { impl DataUrl<'static> {
pub async fn from_reader( pub async fn from_reader(
@@ -112,57 +109,12 @@ impl DataUrl<'static> {
} }
} }
impl<'a> std::fmt::Display for DataUrl<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"data:{};base64,{}",
self.mime,
base64::display::Base64Display::new(
&*self.data,
&base64::engine::general_purpose::STANDARD
)
)
}
}
impl<'a> std::fmt::Debug for DataUrl<'a> { impl<'a> std::fmt::Debug for DataUrl<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(self, f) f.write_str(&self.to_string())
} }
} }
#[derive(Debug)]
pub struct DataUrlParseError;
impl std::fmt::Display for DataUrlParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "invalid base64 url")
}
}
impl std::error::Error for DataUrlParseError {}
impl From<DataUrlParseError> for Error {
fn from(e: DataUrlParseError) -> Self {
Error::new(e, ErrorKind::ParseUrl)
}
}
impl FromStr for DataUrl<'static> {
type Err = DataUrlParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
s.strip_prefix("data:")
.and_then(|v| v.split_once(";base64,"))
.and_then(|(mime, data)| {
Some(DataUrl {
mime: InternedString::intern(mime),
data: Cow::Owned(
base64::engine::general_purpose::STANDARD
.decode(data)
.ok()?,
),
})
})
.ok_or(DataUrlParseError)
}
}
impl<'de> Deserialize<'de> for DataUrl<'static> { impl<'de> Deserialize<'de> for DataUrl<'static> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where where
@@ -178,9 +130,21 @@ impl<'de> Deserialize<'de> for DataUrl<'static> {
where where
E: serde::de::Error, E: serde::de::Error,
{ {
v.parse().map_err(|_| { v.strip_prefix("data:")
E::invalid_value(serde::de::Unexpected::Str(v), &"a valid base64 data url") .and_then(|v| v.split_once(";base64,"))
}) .and_then(|(mime, data)| {
Some(DataUrl {
mime: InternedString::intern(mime),
data: Cow::Owned(
base64::engine::general_purpose::STANDARD
.decode(data)
.ok()?,
),
})
})
.ok_or_else(|| {
E::invalid_value(serde::de::Unexpected::Str(v), &"a valid base64 data url")
})
} }
} }
deserializer.deserialize_any(Visitor) deserializer.deserialize_any(Visitor)
@@ -204,6 +168,6 @@ fn doesnt_reallocate() {
mime: InternedString::intern("png"), mime: InternedString::intern("png"),
data: Cow::Borrowed(&random[..i]), data: Cow::Borrowed(&random[..i]),
}; };
assert_eq!(icon.to_string().capacity(), icon.len()); assert_eq!(icon.to_string().capacity(), icon.data_url_len());
} }
} }

View File

@@ -94,6 +94,7 @@ pub enum ErrorKind {
DBus = 75, DBus = 75,
InstallFailed = 76, InstallFailed = 76,
UpdateFailed = 77, UpdateFailed = 77,
Smtp = 78,
} }
impl ErrorKind { impl ErrorKind {
pub fn as_str(&self) -> &'static str { pub fn as_str(&self) -> &'static str {
@@ -176,6 +177,7 @@ impl ErrorKind {
DBus => "DBus Error", DBus => "DBus Error",
InstallFailed => "Install Failed", InstallFailed => "Install Failed",
UpdateFailed => "Update Failed", UpdateFailed => "Update Failed",
Smtp => "SMTP Error",
} }
} }
} }
@@ -370,6 +372,21 @@ impl From<patch_db::value::Error> for Error {
} }
} }
} }
impl From<lettre::error::Error> for Error {
fn from(e: lettre::error::Error) -> Self {
Error::new(e, ErrorKind::Smtp)
}
}
impl From<lettre::transport::smtp::Error> for Error {
fn from(e: lettre::transport::smtp::Error) -> Self {
Error::new(e, ErrorKind::Smtp)
}
}
impl From<lettre::address::AddressError> for Error {
fn from(e: lettre::address::AddressError) -> Self {
Error::new(e, ErrorKind::Smtp)
}
}
#[derive(Clone, Deserialize, Serialize)] #[derive(Clone, Deserialize, Serialize)]
pub struct ErrorData { pub struct ErrorData {

View File

@@ -14,14 +14,12 @@ impl GatewayId {
&*self.0 &*self.0
} }
} }
impl From<InternedString> for GatewayId { impl<T> From<T> for GatewayId
fn from(value: InternedString) -> Self { where
Self(value) T: Into<InternedString>,
} {
} fn from(value: T) -> Self {
impl From<GatewayId> for InternedString { Self(value.into())
fn from(value: GatewayId) -> Self {
value.0
} }
} }
impl FromStr for GatewayId { impl FromStr for GatewayId {

View File

@@ -51,7 +51,7 @@ default = ["cli", "startd", "registry", "cli-container", "tunnel"]
dev = [] dev = []
docker = [] docker = []
registry = [] registry = []
startd = ["mail-send"] startd = []
test = [] test = []
tunnel = [] tunnel = []
unstable = ["console-subscriber", "tokio/tracing"] unstable = ["console-subscriber", "tokio/tracing"]
@@ -81,25 +81,26 @@ async-stream = "0.3.5"
async-trait = "0.1.74" async-trait = "0.1.74"
axum = { version = "0.8.4", features = ["ws"] } axum = { version = "0.8.4", features = ["ws"] }
barrage = "0.2.3" barrage = "0.2.3"
backhand = "0.21.0" backhand = "0.23.0"
base32 = "0.5.0" base32 = "0.5.0"
base64 = "0.22.1" base64 = "0.22.1"
base64ct = "1.6.0" base64ct = "1.6.0"
basic-cookies = "0.1.4" basic-cookies = "0.1.4"
bech32 = "0.11.0"
blake3 = { version = "1.5.0", features = ["mmap", "rayon"] } blake3 = { version = "1.5.0", features = ["mmap", "rayon"] }
bytes = "1" bytes = "1"
chrono = { version = "0.4.31", features = ["serde"] } chrono = { version = "0.4.31", features = ["serde"] }
clap = { version = "4.4.12", features = ["string"] } clap = { version = "4.4.12", features = ["string"] }
color-eyre = "0.6.2" color-eyre = "0.6.2"
console = "0.15.7" console = "0.16.0"
console-subscriber = { version = "0.4.1", optional = true } console-subscriber = { version = "0.4.1", optional = true }
const_format = "0.2.34" const_format = "0.2.34"
cookie = "0.18.0" cookie = "0.18.0"
cookie_store = "0.21.0" cookie_store = "0.22.0"
der = { version = "0.7.9", features = ["derive", "pem"] } der = { version = "0.7.9", features = ["derive", "pem"] }
digest = "0.10.7" digest = "0.10.7"
divrem = "1.0.0" divrem = "1.0.0"
dns-lookup = "2.1.0" dns-lookup = "3.0.0"
ed25519 = { version = "2.2.3", features = ["pkcs8", "pem", "alloc"] } ed25519 = { version = "2.2.3", features = ["pkcs8", "pem", "alloc"] }
ed25519-dalek = { version = "2.2.0", features = [ ed25519-dalek = { version = "2.2.0", features = [
"serde", "serde",
@@ -141,10 +142,11 @@ imbl = { version = "6", features = ["serde", "small-chunks"] }
imbl-value = { version = "0.4.3", features = ["ts-rs"] } imbl-value = { version = "0.4.3", features = ["ts-rs"] }
include_dir = { version = "0.7.3", features = ["metadata"] } include_dir = { version = "0.7.3", features = ["metadata"] }
indexmap = { version = "2.0.2", features = ["serde"] } indexmap = { version = "2.0.2", features = ["serde"] }
indicatif = { version = "0.17.7", features = ["tokio"] } indicatif = { version = "0.18.0", features = ["tokio"] }
inotify = "0.11.0" inotify = "0.11.0"
integer-encoding = { version = "4.0.0", features = ["tokio_async"] } integer-encoding = { version = "4.0.0", features = ["tokio_async"] }
ipnet = { version = "2.8.0", features = ["serde"] } ipnet = { version = "2.8.0", features = ["serde"] }
iroh = { version = "0.91.2", features = ["discovery-pkarr-dht"] }
isocountry = "0.3.2" isocountry = "0.3.2"
itertools = "0.14.0" itertools = "0.14.0"
jaq-core = "0.10.1" jaq-core = "0.10.1"
@@ -153,7 +155,16 @@ josekit = "0.10.3"
jsonpath_lib = { git = "https://github.com/Start9Labs/jsonpath.git" } jsonpath_lib = { git = "https://github.com/Start9Labs/jsonpath.git" }
lazy_async_pool = "0.3.3" lazy_async_pool = "0.3.3"
lazy_format = "2.0" lazy_format = "2.0"
lazy_static = "1.4.0" lazy_static = "1.5.0"
lettre = { version = "0.11.18", default-features = false, features = [
"smtp-transport",
"pool",
"hostname",
"builder",
"tokio1-rustls",
"rustls-platform-verifier",
"aws-lc-rs",
] }
libc = "0.2.149" libc = "0.2.149"
log = "0.4.20" log = "0.4.20"
mio = "1" mio = "1"
@@ -186,23 +197,23 @@ pkcs8 = { version = "0.10.2", features = ["std"] }
prettytable-rs = "0.10.0" prettytable-rs = "0.10.0"
procfs = { version = "0.17.0", optional = true } procfs = { version = "0.17.0", optional = true }
proptest = "1.3.1" proptest = "1.3.1"
proptest-derive = "0.5.0" proptest-derive = "0.6.0"
pty-process = { version = "0.5.1", optional = true } pty-process = { version = "0.5.1", optional = true }
qrcode = "0.14.1" qrcode = "0.14.1"
rand = "0.9.2" rand = "0.9.2"
regex = "1.10.2" regex = "1.10.2"
reqwest = { version = "0.12.4", features = ["stream", "json", "socks"] } reqwest = { version = "0.12.4", features = ["stream", "json", "socks"] }
reqwest_cookie_store = "0.8.0" reqwest_cookie_store = "0.9.0"
rpassword = "7.2.0" rpassword = "7.2.0"
rpc-toolkit = { git = "https://github.com/Start9Labs/rpc-toolkit.git", branch = "master" } rpc-toolkit = { git = "https://github.com/Start9Labs/rpc-toolkit.git", branch = "master" }
rust-argon2 = "2.0.0" rust-argon2 = "3.0.0"
rustyline-async = "0.4.1" rustyline-async = "0.4.1"
safelog = { version = "0.4.8", git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit" } safelog = { version = "0.4.8", git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit" }
semver = { version = "1.0.20", features = ["serde"] } semver = { version = "1.0.20", features = ["serde"] }
serde = { version = "1.0", features = ["derive", "rc"] } serde = { version = "1.0", features = ["derive", "rc"] }
serde_cbor = { package = "ciborium", version = "0.2.1" } serde_cbor = { package = "ciborium", version = "0.2.1" }
serde_json = "1.0" serde_json = "1.0"
serde_toml = { package = "toml", version = "0.8.2" } serde_toml = { package = "toml", version = "0.9.5" }
serde_urlencoded = "0.7" serde_urlencoded = "0.7"
serde_with = { version = "3.4.0", features = ["macros", "json"] } serde_with = { version = "3.4.0", features = ["macros", "json"] }
serde_yaml = { package = "serde_yml", version = "0.0.12" } serde_yaml = { package = "serde_yml", version = "0.0.12" }
@@ -227,7 +238,7 @@ tokio = { version = "1.38.1", features = ["full"] }
tokio-rustls = "0.26.0" tokio-rustls = "0.26.0"
tokio-stream = { version = "0.1.14", features = ["io-util", "sync", "net"] } tokio-stream = { version = "0.1.14", features = ["io-util", "sync", "net"] }
tokio-tar = { git = "https://github.com/dr-bonez/tokio-tar.git" } tokio-tar = { git = "https://github.com/dr-bonez/tokio-tar.git" }
tokio-tungstenite = { version = "0.26.2", features = ["native-tls", "url"] } tokio-tungstenite = { version = "0.27.0", features = ["native-tls", "url"] }
tokio-util = { version = "0.7.9", features = ["io"] } tokio-util = { version = "0.7.9", features = ["io"] }
tor-cell = { version = "0.33", git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit" } tor-cell = { version = "0.33", git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit" }
tor-hscrypto = { version = "0.33", features = [ tor-hscrypto = { version = "0.33", features = [
@@ -259,7 +270,6 @@ urlencoding = "2.1.3"
uuid = { version = "1.4.1", features = ["v4"] } uuid = { version = "1.4.1", features = ["v4"] }
zbus = "5.1.1" zbus = "5.1.1"
zeroize = "1.6.0" zeroize = "1.6.0"
mail-send = { git = "https://github.com/dr-bonez/mail-send.git", branch = "main", optional = true }
rustls = "0.23.20" rustls = "0.23.20"
rustls-pki-types = { version = "1.10.1", features = ["alloc"] } rustls-pki-types = { version = "1.10.1", features = ["alloc"] }

View File

@@ -4,7 +4,7 @@ use clap::{CommandFactory, FromArgMatches, Parser};
pub use models::ActionId; pub use models::ActionId;
use models::{PackageId, ReplayId}; use models::{PackageId, ReplayId};
use qrcode::QrCode; use qrcode::QrCode;
use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler}; use rpc_toolkit::{Context, HandlerExt, ParentHandler, from_fn_async};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tracing::instrument; use tracing::instrument;
use ts_rs::TS; use ts_rs::TS;
@@ -14,7 +14,7 @@ use crate::db::model::package::TaskSeverity;
use crate::prelude::*; use crate::prelude::*;
use crate::rpc_continuations::Guid; use crate::rpc_continuations::Guid;
use crate::util::serde::{ use crate::util::serde::{
display_serializable, HandlerExtSerde, StdinDeserializable, WithIoFormat, HandlerExtSerde, StdinDeserializable, WithIoFormat, display_serializable,
}; };
pub fn action_api<C: Context>() -> ParentHandler<C> { pub fn action_api<C: Context>() -> ParentHandler<C> {
@@ -52,7 +52,6 @@ pub fn action_api<C: Context>() -> ParentHandler<C> {
#[ts(export)] #[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ActionInput { pub struct ActionInput {
#[serde(default)]
pub event_id: Guid, pub event_id: Guid,
#[ts(type = "Record<string, unknown>")] #[ts(type = "Record<string, unknown>")]
pub spec: Value, pub spec: Value,

View File

@@ -6,10 +6,10 @@ use tokio::sync::broadcast::Sender;
use tokio::sync::watch; use tokio::sync::watch;
use tracing::instrument; use tracing::instrument;
use crate::Error;
use crate::context::config::ServerConfig; use crate::context::config::ServerConfig;
use crate::progress::FullProgressTracker; use crate::progress::FullProgressTracker;
use crate::rpc_continuations::RpcContinuations; use crate::rpc_continuations::RpcContinuations;
use crate::Error;
pub struct InitContextSeed { pub struct InitContextSeed {
pub config: ServerConfig, pub config: ServerConfig,
@@ -25,12 +25,10 @@ impl InitContext {
#[instrument(skip_all)] #[instrument(skip_all)]
pub async fn init(cfg: &ServerConfig) -> Result<Self, Error> { pub async fn init(cfg: &ServerConfig) -> Result<Self, Error> {
let (shutdown, _) = tokio::sync::broadcast::channel(1); let (shutdown, _) = tokio::sync::broadcast::channel(1);
let mut progress = FullProgressTracker::new();
progress.enable_logging(true);
Ok(Self(Arc::new(InitContextSeed { Ok(Self(Arc::new(InitContextSeed {
config: cfg.clone(), config: cfg.clone(),
error: watch::channel(None).0, error: watch::channel(None).0,
progress, progress: FullProgressTracker::new(),
shutdown, shutdown,
rpc_continuations: RpcContinuations::new(), rpc_continuations: RpcContinuations::new(),
}))) })))

View File

@@ -10,14 +10,15 @@ use josekit::jwk::Jwk;
use patch_db::PatchDb; use patch_db::PatchDb;
use rpc_toolkit::Context; use rpc_toolkit::Context;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::sync::broadcast::Sender;
use tokio::sync::OnceCell; use tokio::sync::OnceCell;
use tokio::sync::broadcast::Sender;
use tracing::instrument; use tracing::instrument;
use ts_rs::TS; use ts_rs::TS;
use crate::MAIN_DATA;
use crate::account::AccountInfo; use crate::account::AccountInfo;
use crate::context::config::ServerConfig;
use crate::context::RpcContext; use crate::context::RpcContext;
use crate::context::config::ServerConfig;
use crate::disk::OsPartitionInfo; use crate::disk::OsPartitionInfo;
use crate::hostname::Hostname; use crate::hostname::Hostname;
use crate::net::web_server::{UpgradableListener, WebServer, WebServerAcceptorSetter}; use crate::net::web_server::{UpgradableListener, WebServer, WebServerAcceptorSetter};
@@ -27,7 +28,6 @@ use crate::rpc_continuations::{Guid, RpcContinuation, RpcContinuations};
use crate::setup::SetupProgress; use crate::setup::SetupProgress;
use crate::shutdown::Shutdown; use crate::shutdown::Shutdown;
use crate::util::net::WebSocketExt; use crate::util::net::WebSocketExt;
use crate::MAIN_DATA;
lazy_static::lazy_static! { lazy_static::lazy_static! {
pub static ref CURRENT_SECRET: Jwk = Jwk::generate_ec_key(josekit::jwk::alg::ec::EcCurve::P256).unwrap_or_else(|e| { pub static ref CURRENT_SECRET: Jwk = Jwk::generate_ec_key(josekit::jwk::alg::ec::EcCurve::P256).unwrap_or_else(|e| {
@@ -86,8 +86,6 @@ impl SetupContext {
config: &ServerConfig, config: &ServerConfig,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
let (shutdown, _) = tokio::sync::broadcast::channel(1); let (shutdown, _) = tokio::sync::broadcast::channel(1);
let mut progress = FullProgressTracker::new();
progress.enable_logging(true);
Ok(Self(Arc::new(SetupContextSeed { Ok(Self(Arc::new(SetupContextSeed {
webserver: webserver.acceptor_setter(), webserver: webserver.acceptor_setter(),
config: config.clone(), config: config.clone(),
@@ -98,7 +96,7 @@ impl SetupContext {
) )
})?, })?,
disable_encryption: config.disable_encryption.unwrap_or(false), disable_encryption: config.disable_encryption.unwrap_or(false),
progress, progress: FullProgressTracker::new(),
task: OnceCell::new(), task: OnceCell::new(),
result: OnceCell::new(), result: OnceCell::new(),
disk_guid: OnceCell::new(), disk_guid: OnceCell::new(),

View File

@@ -19,8 +19,8 @@ use crate::account::AccountInfo;
use crate::db::model::package::AllPackageData; use crate::db::model::package::AllPackageData;
use crate::net::acme::AcmeProvider; use crate::net::acme::AcmeProvider;
use crate::net::forward::START9_BRIDGE_IFACE; use crate::net::forward::START9_BRIDGE_IFACE;
use crate::net::host::binding::{AddSslOptions, BindInfo, BindOptions, NetInfo};
use crate::net::host::Host; use crate::net::host::Host;
use crate::net::host::binding::{AddSslOptions, BindInfo, BindOptions, NetInfo};
use crate::net::utils::ipv6_is_local; use crate::net::utils::ipv6_is_local;
use crate::net::vhost::AlpnInfo; use crate::net::vhost::AlpnInfo;
use crate::prelude::*; use crate::prelude::*;
@@ -211,7 +211,6 @@ pub struct DnsSettings {
#[model = "Model<Self>"] #[model = "Model<Self>"]
#[ts(export)] #[ts(export)]
pub struct NetworkInterfaceInfo { pub struct NetworkInterfaceInfo {
pub name: Option<InternedString>,
pub public: Option<bool>, pub public: Option<bool>,
pub secure: Option<bool>, pub secure: Option<bool>,
pub ip_info: Option<IpInfo>, pub ip_info: Option<IpInfo>,
@@ -219,9 +218,8 @@ pub struct NetworkInterfaceInfo {
impl NetworkInterfaceInfo { impl NetworkInterfaceInfo {
pub fn loopback() -> (&'static GatewayId, &'static Self) { pub fn loopback() -> (&'static GatewayId, &'static Self) {
lazy_static! { lazy_static! {
static ref LO: GatewayId = GatewayId::from(InternedString::intern("lo")); static ref LO: GatewayId = GatewayId::from("lo");
static ref LOOPBACK: NetworkInterfaceInfo = NetworkInterfaceInfo { static ref LOOPBACK: NetworkInterfaceInfo = NetworkInterfaceInfo {
name: Some(InternedString::from_static("Loopback")),
public: Some(false), public: Some(false),
secure: Some(true), secure: Some(true),
ip_info: Some(IpInfo { ip_info: Some(IpInfo {
@@ -250,10 +248,8 @@ impl NetworkInterfaceInfo {
} }
pub fn lxc_bridge() -> (&'static GatewayId, &'static Self) { pub fn lxc_bridge() -> (&'static GatewayId, &'static Self) {
lazy_static! { lazy_static! {
static ref LXCBR0: GatewayId = static ref LXCBR0: GatewayId = GatewayId::from(START9_BRIDGE_IFACE);
GatewayId::from(InternedString::intern(START9_BRIDGE_IFACE));
static ref LXC_BRIDGE: NetworkInterfaceInfo = NetworkInterfaceInfo { static ref LXC_BRIDGE: NetworkInterfaceInfo = NetworkInterfaceInfo {
name: Some(InternedString::from_static("LXC Bridge Interface")),
public: Some(false), public: Some(false),
secure: Some(true), secure: Some(true),
ip_info: Some(IpInfo { ip_info: Some(IpInfo {

View File

@@ -1,14 +1,13 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::path::Path;
use imbl_value::InternedString; use imbl_value::InternedString;
use models::PackageId; use models::PackageId;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use ts_rs::TS; use ts_rs::TS;
use crate::Error;
use crate::prelude::*; use crate::prelude::*;
use crate::util::PathOrUrl; use crate::util::PathOrUrl;
use crate::Error;
#[derive(Clone, Debug, Default, Deserialize, Serialize, HasModel, TS)] #[derive(Clone, Debug, Default, Deserialize, Serialize, HasModel, TS)]
#[model = "Model<Self>"] #[model = "Model<Self>"]
@@ -25,57 +24,14 @@ impl Map for Dependencies {
} }
} }
#[derive(Clone, Debug, Deserialize, Serialize, HasModel)] #[derive(Clone, Debug, Deserialize, Serialize, HasModel, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[model = "Model<Self>"] #[model = "Model<Self>"]
#[ts(export)]
pub struct DepInfo { pub struct DepInfo {
pub description: Option<String>, pub description: Option<String>,
pub optional: bool, pub optional: bool,
#[serde(flatten)] pub s9pk: Option<PathOrUrl>,
pub metadata: Option<MetadataSrc>,
}
impl TS for DepInfo {
type WithoutGenerics = Self;
fn decl() -> String {
format!("type {} = {}", Self::name(), Self::inline())
}
fn decl_concrete() -> String {
Self::decl()
}
fn name() -> String {
"DepInfo".into()
}
fn inline() -> String {
"{ description: string | null, optional: boolean } & MetadataSrc".into()
}
fn inline_flattened() -> String {
Self::inline()
}
fn visit_dependencies(v: &mut impl ts_rs::TypeVisitor)
where
Self: 'static,
{
v.visit::<MetadataSrc>()
}
fn output_path() -> Option<&'static std::path::Path> {
Some(Path::new("DepInfo.ts"))
}
}
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub enum MetadataSrc {
Metadata(Metadata),
S9pk(Option<PathOrUrl>), // backwards compatibility
}
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct Metadata {
pub title: InternedString,
pub icon: PathOrUrl,
} }
#[derive(Clone, Debug, Deserialize, Serialize, HasModel, TS)] #[derive(Clone, Debug, Deserialize, Serialize, HasModel, TS)]

View File

@@ -247,8 +247,7 @@ pub async fn init(
Command::new("killall") Command::new("killall")
.arg("journalctl") .arg("journalctl")
.invoke(crate::ErrorKind::Journald) .invoke(crate::ErrorKind::Journald)
.await .await?;
.log_err();
mount_logs.complete(); mount_logs.complete();
tokio::io::copy( tokio::io::copy(
&mut open_file("/run/startos/init.log").await?, &mut open_file("/run/startos/init.log").await?,
@@ -496,7 +495,14 @@ pub async fn init_progress(ctx: InitContext) -> Result<InitProgressRes, Error> {
} }
); );
if let Err(e) = ws.close_result(res.map(|_| "complete")).await { if let Err(e) = ws
.close_result(res.map(|_| "complete").map_err(|e| {
tracing::error!("error in init progress websocket: {e}");
tracing::debug!("{e:?}");
e
}))
.await
{
tracing::error!("error closing init progress websocket: {e}"); tracing::error!("error closing init progress websocket: {e}");
tracing::debug!("{e:?}"); tracing::debug!("{e:?}");
} }

View File

@@ -15,7 +15,7 @@ use itertools::Itertools;
use models::{FromStrParser, PackageId}; use models::{FromStrParser, PackageId};
use rpc_toolkit::yajrc::RpcError; use rpc_toolkit::yajrc::RpcError;
use rpc_toolkit::{ use rpc_toolkit::{
from_fn_async, CallRemote, Context, Empty, HandlerArgs, HandlerExt, HandlerFor, ParentHandler, CallRemote, Context, Empty, HandlerArgs, HandlerExt, HandlerFor, ParentHandler, from_fn_async,
}; };
use serde::de::{self, DeserializeOwned}; use serde::de::{self, DeserializeOwned};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -30,9 +30,9 @@ use crate::error::ResultExt;
use crate::lxc::ContainerId; use crate::lxc::ContainerId;
use crate::prelude::*; use crate::prelude::*;
use crate::rpc_continuations::{Guid, RpcContinuation, RpcContinuations}; use crate::rpc_continuations::{Guid, RpcContinuation, RpcContinuations};
use crate::util::Invoke;
use crate::util::net::WebSocketExt; use crate::util::net::WebSocketExt;
use crate::util::serde::Reversible; use crate::util::serde::Reversible;
use crate::util::Invoke;
#[pin_project::pin_project] #[pin_project::pin_project]
pub struct LogStream { pub struct LogStream {
@@ -551,8 +551,8 @@ pub async fn journalctl(
let deserialized_entries = String::from_utf8(cmd.invoke(ErrorKind::Journald).await?)? let deserialized_entries = String::from_utf8(cmd.invoke(ErrorKind::Journald).await?)?
.lines() .lines()
.map(serde_json::from_str::<JournalctlEntry>) .map(serde_json::from_str::<JournalctlEntry>)
.filter_map(|e| e.ok()) .collect::<Result<Vec<_>, _>>()
.collect::<Vec<_>>(); .with_kind(ErrorKind::Deserialization)?;
if follow { if follow {
let mut follow_cmd = gen_journalctl_command(&id); let mut follow_cmd = gen_journalctl_command(&id);
@@ -573,8 +573,11 @@ pub async fn journalctl(
let follow_deserialized_entries = journalctl_entries let follow_deserialized_entries = journalctl_entries
.map_err(|e| Error::new(e, crate::ErrorKind::Journald)) .map_err(|e| Error::new(e, crate::ErrorKind::Journald))
.try_filter_map(|s| { .and_then(|s| {
futures::future::ready(Ok(serde_json::from_str::<JournalctlEntry>(&s).ok())) futures::future::ready(
serde_json::from_str::<JournalctlEntry>(&s)
.with_kind(crate::ErrorKind::Deserialization),
)
}); });
let entries = futures::stream::iter(deserialized_entries) let entries = futures::stream::iter(deserialized_entries)

View File

@@ -10,34 +10,34 @@ use futures::future::BoxFuture;
use futures::{FutureExt, StreamExt, TryStreamExt}; use futures::{FutureExt, StreamExt, TryStreamExt};
use helpers::NonDetachingJoinHandle; use helpers::NonDetachingJoinHandle;
use hickory_client::client::Client; use hickory_client::client::Client;
use hickory_client::proto::DnsHandle;
use hickory_client::proto::runtime::TokioRuntimeProvider; use hickory_client::proto::runtime::TokioRuntimeProvider;
use hickory_client::proto::tcp::TcpClientStream; use hickory_client::proto::tcp::TcpClientStream;
use hickory_client::proto::udp::UdpClientStream; use hickory_client::proto::udp::UdpClientStream;
use hickory_client::proto::xfer::{DnsExchangeBackground, DnsRequestOptions}; use hickory_client::proto::xfer::{DnsExchangeBackground, DnsRequestOptions};
use hickory_client::proto::DnsHandle; use hickory_server::ServerFuture;
use hickory_server::authority::MessageResponseBuilder; use hickory_server::authority::MessageResponseBuilder;
use hickory_server::proto::op::{Header, ResponseCode}; use hickory_server::proto::op::{Header, ResponseCode};
use hickory_server::proto::rr::{Name, Record, RecordType}; use hickory_server::proto::rr::{Name, Record, RecordType};
use hickory_server::server::{Request, RequestHandler, ResponseHandler, ResponseInfo}; use hickory_server::server::{Request, RequestHandler, ResponseHandler, ResponseInfo};
use hickory_server::ServerFuture;
use imbl::OrdMap; use imbl::OrdMap;
use imbl_value::InternedString; use imbl_value::InternedString;
use itertools::Itertools; use itertools::Itertools;
use models::{GatewayId, OptionExt, PackageId}; use models::{GatewayId, OptionExt, PackageId};
use rpc_toolkit::{ use rpc_toolkit::{
from_fn_async, from_fn_blocking, Context, HandlerArgs, HandlerExt, ParentHandler, Context, HandlerArgs, HandlerExt, ParentHandler, from_fn_async, from_fn_blocking,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::net::{TcpListener, UdpSocket}; use tokio::net::{TcpListener, UdpSocket};
use tracing::instrument; use tracing::instrument;
use crate::context::RpcContext; use crate::context::RpcContext;
use crate::db::model::public::NetworkInterfaceInfo;
use crate::db::model::Database; use crate::db::model::Database;
use crate::db::model::public::NetworkInterfaceInfo;
use crate::net::gateway::NetworkInterfaceWatcher; use crate::net::gateway::NetworkInterfaceWatcher;
use crate::prelude::*; use crate::prelude::*;
use crate::util::io::file_string_stream; use crate::util::io::file_string_stream;
use crate::util::serde::{display_serializable, HandlerExtSerde}; use crate::util::serde::{HandlerExtSerde, display_serializable};
use crate::util::sync::{SyncRwLock, Watch}; use crate::util::sync::{SyncRwLock, Watch};
pub fn dns_api<C: Context>() -> ParentHandler<C> { pub fn dns_api<C: Context>() -> ParentHandler<C> {
@@ -343,19 +343,13 @@ impl RequestHandler for Resolver {
if let Some(ip) = self.resolve(query.name().borrow(), req.src.ip()) { if let Some(ip) = self.resolve(query.name().borrow(), req.src.ip()) {
match query.query_type() { match query.query_type() {
RecordType::A => { RecordType::A => {
let mut header = Header::response_from_request(request.header());
header.set_recursion_available(true);
response_handle response_handle
.send_response( .send_response(
MessageResponseBuilder::from_message_request(&*request).build( MessageResponseBuilder::from_message_request(&*request).build(
header, Header::response_from_request(request.header()),
&ip.into_iter() &ip.into_iter()
.filter_map(|a| { .filter_map(|a| {
if let IpAddr::V4(a) = a { if let IpAddr::V4(a) = a { Some(a) } else { None }
Some(a)
} else {
None
}
}) })
.map(|ip| { .map(|ip| {
Record::from_rdata( Record::from_rdata(
@@ -373,19 +367,13 @@ impl RequestHandler for Resolver {
.await .await
} }
RecordType::AAAA => { RecordType::AAAA => {
let mut header = Header::response_from_request(request.header());
header.set_recursion_available(true);
response_handle response_handle
.send_response( .send_response(
MessageResponseBuilder::from_message_request(&*request).build( MessageResponseBuilder::from_message_request(&*request).build(
header, Header::response_from_request(request.header()),
&ip.into_iter() &ip.into_iter()
.filter_map(|a| { .filter_map(|a| {
if let IpAddr::V6(a) = a { if let IpAddr::V6(a) = a { Some(a) } else { None }
Some(a)
} else {
None
}
}) })
.map(|ip| { .map(|ip| {
Record::from_rdata( Record::from_rdata(
@@ -403,12 +391,11 @@ impl RequestHandler for Resolver {
.await .await
} }
_ => { _ => {
let mut header = Header::response_from_request(request.header()); let res = Header::response_from_request(request.header());
header.set_recursion_available(true);
response_handle response_handle
.send_response( .send_response(
MessageResponseBuilder::from_message_request(&*request).build( MessageResponseBuilder::from_message_request(&*request).build(
header.into(), res.into(),
[], [],
[], [],
[], [],
@@ -445,13 +432,12 @@ impl RequestHandler for Resolver {
tracing::error!("{e}"); tracing::error!("{e}");
tracing::debug!("{e:?}"); tracing::debug!("{e:?}");
} }
let mut header = Header::response_from_request(request.header()); let mut res = Header::response_from_request(request.header());
header.set_recursion_available(true); res.set_response_code(ResponseCode::ServFail);
header.set_response_code(ResponseCode::ServFail);
response_handle response_handle
.send_response( .send_response(
MessageResponseBuilder::from_message_request(&*request).build( MessageResponseBuilder::from_message_request(&*request).build(
header, res,
[], [],
[], [],
[], [],
@@ -467,13 +453,12 @@ impl RequestHandler for Resolver {
Err(e) => { Err(e) => {
tracing::error!("{}", e); tracing::error!("{}", e);
tracing::debug!("{:?}", e); tracing::debug!("{:?}", e);
let mut header = Header::response_from_request(request.header()); let mut res = Header::response_from_request(request.header());
header.set_recursion_available(true); res.set_response_code(ResponseCode::ServFail);
header.set_response_code(ResponseCode::ServFail);
response_handle response_handle
.send_response( .send_response(
MessageResponseBuilder::from_message_request(&*request).build( MessageResponseBuilder::from_message_request(&*request).build(
header, res,
[], [],
[], [],
[], [],
@@ -481,7 +466,7 @@ impl RequestHandler for Resolver {
), ),
) )
.await .await
.unwrap_or(header.into()) .unwrap_or(res.into())
} }
} }
} }

View File

@@ -16,33 +16,33 @@ use itertools::Itertools;
use models::GatewayId; use models::GatewayId;
use nix::net::if_::if_nametoindex; use nix::net::if_::if_nametoindex;
use patch_db::json_ptr::JsonPointer; use patch_db::json_ptr::JsonPointer;
use rpc_toolkit::{from_fn_async, Context, HandlerArgs, HandlerExt, ParentHandler}; use rpc_toolkit::{Context, HandlerArgs, HandlerExt, ParentHandler, from_fn_async};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::io::{AsyncBufReadExt, BufReader}; use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::net::{TcpListener, TcpStream}; use tokio::net::{TcpListener, TcpStream};
use tokio::process::Command; use tokio::process::Command;
use tokio::sync::oneshot;
use ts_rs::TS; use ts_rs::TS;
use zbus::proxy::{PropertyChanged, PropertyStream, SignalStream}; use zbus::proxy::{PropertyChanged, PropertyStream, SignalStream};
use zbus::zvariant::{ use zbus::zvariant::{
DeserializeDict, Dict, OwnedObjectPath, OwnedValue, Type as ZType, Value as ZValue, DICT_ENTRY_SIG_END_STR, DeserializeDict, Dict, OwnedObjectPath, OwnedValue, Type as ZType,
Value as ZValue,
}; };
use zbus::{proxy, Connection}; use zbus::{Connection, proxy};
use crate::context::{CliContext, RpcContext}; use crate::context::{CliContext, RpcContext};
use crate::db::model::public::{IpInfo, NetworkInterfaceInfo, NetworkInterfaceType};
use crate::db::model::Database; use crate::db::model::Database;
use crate::db::model::public::{IpInfo, NetworkInterfaceInfo, NetworkInterfaceType};
use crate::net::forward::START9_BRIDGE_IFACE; use crate::net::forward::START9_BRIDGE_IFACE;
use crate::net::gateway::device::DeviceProxy; use crate::net::gateway::device::DeviceProxy;
use crate::net::utils::ipv6_is_link_local; use crate::net::utils::ipv6_is_link_local;
use crate::net::web_server::Accept; use crate::net::web_server::Accept;
use crate::prelude::*; use crate::prelude::*;
use crate::util::Invoke;
use crate::util::collections::OrdMapIterMut; use crate::util::collections::OrdMapIterMut;
use crate::util::future::Until; use crate::util::future::Until;
use crate::util::io::open_file; use crate::util::io::open_file;
use crate::util::serde::{display_serializable, HandlerExtSerde}; use crate::util::serde::{HandlerExtSerde, display_serializable};
use crate::util::sync::{SyncMutex, Watch}; use crate::util::sync::{SyncMutex, Watch};
use crate::util::Invoke;
pub fn gateway_api<C: Context>() -> ParentHandler<C> { pub fn gateway_api<C: Context>() -> ParentHandler<C> {
ParentHandler::new() ParentHandler::new()
@@ -130,61 +130,64 @@ async fn list_interfaces(
#[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)] #[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)]
#[ts(export)] #[ts(export)]
struct NetworkInterfaceSetPublicParams { struct NetworkInterfaceSetPublicParams {
gateway: GatewayId, interface: GatewayId,
public: Option<bool>, public: Option<bool>,
} }
async fn set_public( async fn set_public(
ctx: RpcContext, ctx: RpcContext,
NetworkInterfaceSetPublicParams { gateway, public }: NetworkInterfaceSetPublicParams, NetworkInterfaceSetPublicParams { interface, public }: NetworkInterfaceSetPublicParams,
) -> Result<(), Error> { ) -> Result<(), Error> {
ctx.net_controller ctx.net_controller
.net_iface .net_iface
.set_public(&gateway, Some(public.unwrap_or(true))) .set_public(&interface, Some(public.unwrap_or(true)))
.await .await
} }
#[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)] #[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)]
#[ts(export)] #[ts(export)]
struct UnsetPublicParams { struct UnsetInboundParams {
gateway: GatewayId, interface: GatewayId,
} }
async fn unset_public( async fn unset_public(
ctx: RpcContext, ctx: RpcContext,
UnsetPublicParams { gateway }: UnsetPublicParams, UnsetInboundParams { interface }: UnsetInboundParams,
) -> Result<(), Error> { ) -> Result<(), Error> {
ctx.net_controller ctx.net_controller
.net_iface .net_iface
.set_public(&gateway, None) .set_public(&interface, None)
.await .await
} }
#[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)] #[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)]
#[ts(export)] #[ts(export)]
struct ForgetGatewayParams { struct ForgetInterfaceParams {
gateway: GatewayId, interface: GatewayId,
} }
async fn forget_iface( async fn forget_iface(
ctx: RpcContext, ctx: RpcContext,
ForgetGatewayParams { gateway }: ForgetGatewayParams, ForgetInterfaceParams { interface }: ForgetInterfaceParams,
) -> Result<(), Error> { ) -> Result<(), Error> {
ctx.net_controller.net_iface.forget(&gateway).await ctx.net_controller.net_iface.forget(&interface).await
} }
#[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)] #[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)]
#[ts(export)] #[ts(export)]
struct RenameGatewayParams { struct RenameInterfaceParams {
id: GatewayId, interface: GatewayId,
name: InternedString, name: String,
} }
async fn set_name( async fn set_name(
ctx: RpcContext, ctx: RpcContext,
RenameGatewayParams { id, name }: RenameGatewayParams, RenameInterfaceParams { interface, name }: RenameInterfaceParams,
) -> Result<(), Error> { ) -> Result<(), Error> {
ctx.net_controller.net_iface.set_name(&id, name).await ctx.net_controller
.net_iface
.set_name(&interface, &name)
.await
} }
#[proxy( #[proxy(
@@ -244,17 +247,12 @@ mod active_connection {
default_service = "org.freedesktop.NetworkManager" default_service = "org.freedesktop.NetworkManager"
)] )]
trait ConnectionSettings { trait ConnectionSettings {
fn get_settings(&self) -> Result<HashMap<String, HashMap<String, OwnedValue>>, Error>;
fn update2( fn update2(
&self, &self,
settings: HashMap<String, HashMap<String, OwnedValue>>, settings: HashMap<String, HashMap<String, ZValue<'_>>>,
flags: u32, flags: u32,
args: HashMap<String, ZValue<'_>>, args: HashMap<String, ZValue<'_>>,
) -> Result<HashMap<String, OwnedValue>, Error>; ) -> Result<(), Error>;
#[zbus(signal)]
fn updated(&self) -> Result<(), Error>;
} }
#[proxy( #[proxy(
@@ -558,12 +556,6 @@ async fn watch_ip(
let active_connection_proxy = let active_connection_proxy =
active_connection::ActiveConnectionProxy::new(&connection, dac).await?; active_connection::ActiveConnectionProxy::new(&connection, dac).await?;
let settings_proxy = ConnectionSettingsProxy::new(
&connection,
active_connection_proxy.connection().await?,
)
.await?;
let mut until = Until::new() let mut until = Until::new()
.with_stream( .with_stream(
active_connection_proxy active_connection_proxy
@@ -577,8 +569,7 @@ async fn watch_ip(
.receive_dhcp4_config_changed() .receive_dhcp4_config_changed()
.await .await
.stub(), .stub(),
) );
.with_stream(settings_proxy.receive_updated().await?.into_inner().stub());
loop { loop {
until until
@@ -637,10 +628,12 @@ async fn watch_ip(
let lan_ip = [ let lan_ip = [
Some(ip4_proxy.gateway().await?) Some(ip4_proxy.gateway().await?)
.filter(|g| !g.is_empty()) .filter(|g| !g.is_empty())
.and_then(|g| g.parse::<IpAddr>().log_err()), .map(|g| g.parse::<IpAddr>())
.transpose()?,
Some(ip6_proxy.gateway().await?) Some(ip6_proxy.gateway().await?)
.filter(|g| !g.is_empty()) .filter(|g| !g.is_empty())
.and_then(|g| g.parse::<IpAddr>().log_err()), .map(|g| g.parse::<IpAddr>())
.transpose()?,
] ]
.into_iter() .into_iter()
.filter_map(|a| a) .filter_map(|a| a)
@@ -657,11 +650,9 @@ async fn watch_ip(
} }
if let Some(dns) = dhcp.domain_name_servers { if let Some(dns) = dhcp.domain_name_servers {
dns_servers.extend( dns_servers.extend(
dns.split_ascii_whitespace() dns.split(",")
.filter_map(|s| { .map(|s| s.trim().parse::<IpAddr>())
s.parse::<IpAddr>().log_err() .collect::<Result<Vec<_>, _>>()?,
})
.collect::<Vec<_>>(),
); );
} }
} }
@@ -685,7 +676,7 @@ async fn watch_ip(
} else { } else {
None None
}; };
let mut ip_info = IpInfo { let ip_info = Some(IpInfo {
name: name.clone(), name: name.clone(),
scope_id, scope_id,
device_type, device_type,
@@ -694,33 +685,22 @@ async fn watch_ip(
wan_ip, wan_ip,
ntp_servers, ntp_servers,
dns_servers, dns_servers,
}; });
write_to.send_if_modified( write_to.send_if_modified(
|m: &mut OrdMap<GatewayId, NetworkInterfaceInfo>| { |m: &mut OrdMap<GatewayId, NetworkInterfaceInfo>| {
let (name, public, secure, prev_wan_ip) = m let (public, secure) = m
.get(&iface) .get(&iface)
.map_or((None, None, None, None), |i| { .map_or((None, None), |i| (i.public, i.secure));
(
i.name.clone(),
i.public,
i.secure,
i.ip_info
.as_ref()
.and_then(|i| i.wan_ip),
)
});
ip_info.wan_ip = ip_info.wan_ip.or(prev_wan_ip);
m.insert( m.insert(
iface.clone(), iface.clone(),
NetworkInterfaceInfo { NetworkInterfaceInfo {
name,
public, public,
secure, secure,
ip_info: Some(ip_info.clone()), ip_info: ip_info.clone(),
}, },
) )
.filter(|old| &old.ip_info == &Some(ip_info)) .filter(|old| &old.ip_info == &ip_info)
.is_none() .is_none()
}, },
); );
@@ -785,13 +765,7 @@ impl NetworkInterfaceWatcher {
watch_activated: impl IntoIterator<Item = GatewayId>, watch_activated: impl IntoIterator<Item = GatewayId>,
) -> Self { ) -> Self {
let ip_info = Watch::new(OrdMap::new()); let ip_info = Watch::new(OrdMap::new());
let activated = Watch::new( let activated = Watch::new(watch_activated.into_iter().map(|k| (k, false)).collect());
watch_activated
.into_iter()
.chain([NetworkInterfaceInfo::lxc_bridge().0.clone()])
.map(|k| (k, false))
.collect(),
);
Self { Self {
activated: activated.clone(), activated: activated.clone(),
ip_info: ip_info.clone(), ip_info: ip_info.clone(),
@@ -844,11 +818,9 @@ impl NetworkInterfaceWatcher {
Ok(()) Ok(())
})?; })?;
let ip_info = self.ip_info.clone_unseen(); let ip_info = self.ip_info.clone_unseen();
let activated = self.activated.clone_unseen();
Ok(NetworkInterfaceListener { Ok(NetworkInterfaceListener {
_arc: arc, _arc: arc,
ip_info, ip_info,
activated,
listeners: ListenerMap::new(port), listeners: ListenerMap::new(port),
}) })
} }
@@ -952,12 +924,11 @@ impl NetworkInterfaceController {
Ok(()) Ok(())
} }
pub fn new(db: TypedPatchDb<Database>) -> Self { pub fn new(db: TypedPatchDb<Database>) -> Self {
let (seeded_send, seeded) = oneshot::channel();
let watcher = NetworkInterfaceWatcher::new( let watcher = NetworkInterfaceWatcher::new(
{ {
let db = db.clone(); let db = db.clone();
async move { async move {
let info = match db match db
.peek() .peek()
.await .await
.as_public() .as_public()
@@ -977,26 +948,21 @@ impl NetworkInterfaceController {
tracing::debug!("{e:?}"); tracing::debug!("{e:?}");
OrdMap::new() OrdMap::new()
} }
}; }
let _ = seeded_send.send(info.clone());
info
} }
}, },
[InternedString::from_static(START9_BRIDGE_IFACE).into()], [START9_BRIDGE_IFACE.into()],
); );
let mut ip_info_watch = watcher.subscribe(); let mut ip_info = watcher.subscribe();
ip_info_watch.mark_seen();
Self { Self {
db: db.clone(), db: db.clone(),
watcher, watcher,
_sync: tokio::spawn(async move { _sync: tokio::spawn(async move {
let res: Result<(), Error> = async { let res: Result<(), Error> = async {
let mut ip_info = seeded.await.ok();
loop { loop {
if let Err(e) = async { if let Err(e) = async {
if let Some(ip_info) = ip_info { let ip_info = ip_info.read();
Self::sync(&db, &ip_info).boxed().await?; Self::sync(&db, &ip_info).boxed().await?;
}
Ok::<_, Error>(()) Ok::<_, Error>(())
} }
@@ -1006,8 +972,7 @@ impl NetworkInterfaceController {
tracing::debug!("{e:?}"); tracing::debug!("{e:?}");
} }
let _ = ip_info_watch.changed().await; let _ = ip_info.changed().await;
ip_info = Some(ip_info_watch.read());
} }
} }
.await; .await;
@@ -1127,33 +1092,66 @@ impl NetworkInterfaceController {
Ok(()) Ok(())
} }
pub async fn set_name(&self, interface: &GatewayId, name: InternedString) -> Result<(), Error> { pub async fn set_name(&self, interface: &GatewayId, name: &str) -> Result<(), Error> {
let mut sub = self let (dump, mut sub) = self
.db .db
.subscribe( .dump_and_sub(
"/public/serverInfo/network/gateways" "/public/serverInfo/network/gateways"
.parse::<JsonPointer<_, _>>() .parse::<JsonPointer<_, _>>()
.with_kind(ErrorKind::Database)? .with_kind(ErrorKind::Database)?
.join_end(interface.as_str()) .join_end(interface.as_str())
.join_end("ipInfo")
.join_end("name"), .join_end("name"),
) )
.await; .await;
let changed = self.watcher.ip_info.send_if_modified(|i| { let change = dump.value.as_str().or_not_found(interface)? != name;
i.get_mut(interface)
.map(|i| { if !change {
if i.name.as_ref() != Some(&name) { return Ok(());
i.name = Some(name);
true
} else {
false
}
})
.unwrap_or(false)
});
if changed {
sub.recv().await;
} }
let connection = Connection::system().await?;
let netman_proxy = NetworkManagerProxy::new(&connection).await?;
let device = Some(
netman_proxy
.get_device_by_ip_iface(interface.as_str())
.await?,
)
.filter(|o| &**o != "/")
.or_not_found(lazy_format!("{interface} in NetworkManager"))?;
let device_proxy = DeviceProxy::new(&connection, device).await?;
let dac = Some(device_proxy.active_connection().await?)
.filter(|o| &**o != "/")
.or_not_found(lazy_format!("ActiveConnection for {interface}"))?;
let dac_proxy = active_connection::ActiveConnectionProxy::new(&connection, dac).await?;
let settings = Some(dac_proxy.connection().await?)
.filter(|o| &**o != "/")
.or_not_found(lazy_format!("ConnectionSettings for {interface}"))?;
let settings_proxy = ConnectionSettingsProxy::new(&connection, settings).await?;
settings_proxy
.update2(
[(
"connection".into(),
[("id".into(), zbus::zvariant::Value::Str(name.into()))]
.into_iter()
.collect(),
)]
.into_iter()
.collect(),
0x1,
HashMap::new(),
)
.await?;
sub.recv().await;
Ok(()) Ok(())
} }
} }
@@ -1238,7 +1236,7 @@ pub struct PublicFilter {
} }
impl InterfaceFilter for PublicFilter { impl InterfaceFilter for PublicFilter {
fn filter(&self, _: &GatewayId, info: &NetworkInterfaceInfo) -> bool { fn filter(&self, _: &GatewayId, info: &NetworkInterfaceInfo) -> bool {
self.public == info.public() self.public || !info.public()
} }
} }
@@ -1365,14 +1363,15 @@ impl ListenerMap {
fn update( fn update(
&mut self, &mut self,
ip_info: &OrdMap<GatewayId, NetworkInterfaceInfo>, ip_info: &OrdMap<GatewayId, NetworkInterfaceInfo>,
lxc_bridge: bool,
filter: &impl InterfaceFilter, filter: &impl InterfaceFilter,
) -> Result<(), Error> { ) -> Result<(), Error> {
let mut keep = BTreeSet::<SocketAddr>::new(); let mut keep = BTreeSet::<SocketAddr>::new();
for (_, info) in ip_info for (_, info) in ip_info
.iter() .iter()
.chain([NetworkInterfaceInfo::loopback()]) .chain([
.chain(Some(NetworkInterfaceInfo::lxc_bridge()).filter(|_| lxc_bridge)) NetworkInterfaceInfo::loopback(),
NetworkInterfaceInfo::lxc_bridge(),
])
.filter(|(id, info)| filter.filter(*id, *info)) .filter(|(id, info)| filter.filter(*id, *info))
{ {
if let Some(ip_info) = &info.ip_info { if let Some(ip_info) = &info.ip_info {
@@ -1460,7 +1459,6 @@ pub fn lookup_info_by_addr(
pub struct NetworkInterfaceListener { pub struct NetworkInterfaceListener {
pub ip_info: Watch<OrdMap<GatewayId, NetworkInterfaceInfo>>, pub ip_info: Watch<OrdMap<GatewayId, NetworkInterfaceInfo>>,
activated: Watch<BTreeMap<GatewayId, bool>>,
listeners: ListenerMap, listeners: ListenerMap,
_arc: Arc<()>, _arc: Arc<()>,
} }
@@ -1476,29 +1474,21 @@ impl NetworkInterfaceListener {
filter: &impl InterfaceFilter, filter: &impl InterfaceFilter,
) -> Poll<Result<Accepted, Error>> { ) -> Poll<Result<Accepted, Error>> {
while self.ip_info.poll_changed(cx).is_ready() while self.ip_info.poll_changed(cx).is_ready()
|| self.activated.poll_changed(cx).is_ready()
|| !DynInterfaceFilterT::eq(&self.listeners.prev_filter, filter.as_any()) || !DynInterfaceFilterT::eq(&self.listeners.prev_filter, filter.as_any())
{ {
let lxc_bridge = self.activated.peek(|a| {
a.get(NetworkInterfaceInfo::lxc_bridge().0)
.copied()
.unwrap_or_default()
});
self.ip_info self.ip_info
.peek_and_mark_seen(|ip_info| self.listeners.update(ip_info, lxc_bridge, filter))?; .peek_and_mark_seen(|ip_info| self.listeners.update(ip_info, filter))?;
} }
self.listeners.poll_accept(cx) self.listeners.poll_accept(cx)
} }
pub(super) fn new( pub(super) fn new(
mut ip_info: Watch<OrdMap<GatewayId, NetworkInterfaceInfo>>, mut ip_info: Watch<OrdMap<GatewayId, NetworkInterfaceInfo>>,
activated: Watch<BTreeMap<GatewayId, bool>>,
port: u16, port: u16,
) -> Self { ) -> Self {
ip_info.mark_unseen(); ip_info.mark_unseen();
Self { Self {
ip_info, ip_info,
activated,
listeners: ListenerMap::new(port), listeners: ListenerMap::new(port),
_arc: Arc::new(()), _arc: Arc::new(()),
} }
@@ -1542,15 +1532,11 @@ pub struct SelfContainedNetworkInterfaceListener {
impl SelfContainedNetworkInterfaceListener { impl SelfContainedNetworkInterfaceListener {
pub fn bind(port: u16) -> Self { pub fn bind(port: u16) -> Self {
let ip_info = Watch::new(OrdMap::new()); let ip_info = Watch::new(OrdMap::new());
let activated = Watch::new( let _watch_thread =
[(NetworkInterfaceInfo::lxc_bridge().0.clone(), false)] tokio::spawn(watcher(ip_info.clone(), Watch::new(BTreeMap::new()))).into();
.into_iter()
.collect(),
);
let _watch_thread = tokio::spawn(watcher(ip_info.clone(), activated.clone())).into();
Self { Self {
_watch_thread, _watch_thread,
listener: NetworkInterfaceListener::new(ip_info, activated, port), listener: NetworkInterfaceListener::new(ip_info, port),
} }
} }
} }

View File

@@ -4,17 +4,17 @@ use std::net::Ipv4Addr;
use clap::Parser; use clap::Parser;
use imbl_value::InternedString; use imbl_value::InternedString;
use models::GatewayId; use models::GatewayId;
use rpc_toolkit::{from_fn_async, Context, Empty, HandlerArgs, HandlerExt, ParentHandler}; use rpc_toolkit::{Context, Empty, HandlerArgs, HandlerExt, ParentHandler, from_fn_async};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use ts_rs::TS; use ts_rs::TS;
use crate::context::{CliContext, RpcContext}; use crate::context::{CliContext, RpcContext};
use crate::db::model::DatabaseModel; use crate::db::model::DatabaseModel;
use crate::net::acme::AcmeProvider; use crate::net::acme::AcmeProvider;
use crate::net::host::{all_hosts, HostApiKind}; use crate::net::host::{HostApiKind, all_hosts};
use crate::net::tor::OnionAddress; use crate::net::tor::OnionAddress;
use crate::prelude::*; use crate::prelude::*;
use crate::util::serde::{display_serializable, HandlerExtSerde}; use crate::util::serde::{HandlerExtSerde, display_serializable};
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
@@ -27,7 +27,6 @@ pub enum HostAddress {
Domain { Domain {
address: InternedString, address: InternedString,
public: Option<PublicDomainConfig>, public: Option<PublicDomainConfig>,
private: bool,
}, },
} }
@@ -71,14 +70,11 @@ fn handle_duplicates(db: &mut DatabaseModel) -> Result<(), Error> {
for onion in host.as_onions().de()? { for onion in host.as_onions().de()? {
check_onion(&mut onions, onion)?; check_onion(&mut onions, onion)?;
} }
let public = host.as_public_domains().keys()?; for domain in host.as_public_domains().keys()? {
for domain in &public { check_domain(&mut domains, domain)?;
check_domain(&mut domains, domain.clone())?;
} }
for domain in host.as_private_domains().de()? { for domain in host.as_private_domains().de()? {
if !public.contains(&domain) { check_domain(&mut domains, domain)?;
check_domain(&mut domains, domain)?;
}
} }
} }
for host in not_in_use { for host in not_in_use {
@@ -92,21 +88,18 @@ fn handle_duplicates(db: &mut DatabaseModel) -> Result<(), Error> {
for onion in host.as_onions().de()? { for onion in host.as_onions().de()? {
check_onion(&mut onions, onion)?; check_onion(&mut onions, onion)?;
} }
let public = host.as_public_domains().keys()?; for domain in host.as_public_domains().keys()? {
for domain in &public { check_domain(&mut domains, domain)?;
check_domain(&mut domains, domain.clone())?;
} }
for domain in host.as_private_domains().de()? { for domain in host.as_private_domains().de()? {
if !public.contains(&domain) { check_domain(&mut domains, domain)?;
check_domain(&mut domains, domain)?;
}
} }
} }
Ok(()) Ok(())
} }
pub fn address_api<C: Context, Kind: HostApiKind>( pub fn address_api<C: Context, Kind: HostApiKind>()
) -> ParentHandler<C, Kind::Params, Kind::InheritedParams> { -> ParentHandler<C, Kind::Params, Kind::InheritedParams> {
ParentHandler::<C, Kind::Params, Kind::InheritedParams>::new() ParentHandler::<C, Kind::Params, Kind::InheritedParams>::new()
.subcommand( .subcommand(
"domain", "domain",
@@ -205,21 +198,16 @@ pub fn address_api<C: Context, Kind: HostApiKind>(
HostAddress::Domain { HostAddress::Domain {
address, address,
public: Some(PublicDomainConfig { gateway, acme }), public: Some(PublicDomainConfig { gateway, acme }),
private,
} => { } => {
table.add_row(row![ table.add_row(row![
address, address,
&format!( &format!("YES ({gateway})"),
"{} ({gateway})",
if *private { "YES" } else { "ONLY" }
),
acme.as_ref().map(|a| a.0.as_str()).unwrap_or("NONE") acme.as_ref().map(|a| a.0.as_str()).unwrap_or("NONE")
]); ]);
} }
HostAddress::Domain { HostAddress::Domain {
address, address,
public: None, public: None,
..
} => { } => {
table.add_row(row![address, &format!("NO"), "N/A"]); table.add_row(row![address, &format!("NO"), "N/A"]);
} }

View File

@@ -6,15 +6,15 @@ use clap::Parser;
use imbl_value::InternedString; use imbl_value::InternedString;
use itertools::Itertools; use itertools::Itertools;
use models::{HostId, PackageId}; use models::{HostId, PackageId};
use rpc_toolkit::{from_fn_async, Context, Empty, HandlerExt, OrEmpty, ParentHandler}; use rpc_toolkit::{Context, Empty, HandlerExt, OrEmpty, ParentHandler, from_fn_async};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use ts_rs::TS; use ts_rs::TS;
use crate::context::RpcContext; use crate::context::RpcContext;
use crate::db::model::DatabaseModel; use crate::db::model::DatabaseModel;
use crate::net::forward::AvailablePorts; use crate::net::forward::AvailablePorts;
use crate::net::host::address::{address_api, HostAddress, PublicDomainConfig}; use crate::net::host::address::{HostAddress, PublicDomainConfig, address_api};
use crate::net::host::binding::{binding, BindInfo, BindOptions}; use crate::net::host::binding::{BindInfo, BindOptions, binding};
use crate::net::service_interface::HostnameInfo; use crate::net::service_interface::HostnameInfo;
use crate::net::tor::OnionAddress; use crate::net::tor::OnionAddress;
use crate::prelude::*; use crate::prelude::*;
@@ -56,17 +56,14 @@ impl Host {
.map(|(address, config)| HostAddress::Domain { .map(|(address, config)| HostAddress::Domain {
address: address.clone(), address: address.clone(),
public: Some(config.clone()), public: Some(config.clone()),
private: self.private_domains.contains(address),
}), }),
) )
.chain( .chain(
self.private_domains self.private_domains
.iter() .iter()
.filter(|a| !self.public_domains.contains_key(*a))
.map(|address| HostAddress::Domain { .map(|address| HostAddress::Domain {
address: address.clone(), address: address.clone(),
public: None, public: None,
private: true,
}), }),
) )
} }

View File

@@ -0,0 +1,585 @@
use std::collections::{BTreeMap, BTreeSet};
use std::net::SocketAddr;
use std::str::FromStr;
use std::sync::{Arc, Weak};
use clap::Parser;
use color_eyre::eyre::eyre;
use futures::{FutureExt, StreamExt};
use helpers::NonDetachingJoinHandle;
use imbl_value::InternedString;
use iroh::{Endpoint, NodeId, SecretKey};
use itertools::Itertools;
use rpc_toolkit::{from_fn_async, Context, Empty, HandlerExt, ParentHandler};
use serde::{Deserialize, Serialize};
use tokio::net::TcpStream;
use crate::context::{CliContext, RpcContext};
use crate::prelude::*;
use crate::util::actor::background::BackgroundJobQueue;
use crate::util::io::ReadWriter;
use crate::util::serde::{
deserialize_from_str, display_serializable, serialize_display, HandlerExtSerde, Pem,
PemEncoding, WithIoFormat,
};
use crate::util::sync::{SyncMutex, SyncRwLock, Watch};
const HRP: bech32::Hrp = bech32::Hrp::parse_unchecked("iroh");
#[derive(Debug, Clone, Copy)]
pub struct IrohAddress(pub NodeId);
impl std::fmt::Display for IrohAddress {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
bech32::encode_lower_to_fmt::<bech32::Bech32m, _>(f, HRP, self.0.as_bytes())
.map_err(|_| std::fmt::Error)?;
write!(f, ".p2p.start9.to")
}
}
impl FromStr for IrohAddress {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Some(b32) = s.strip_suffix(".p2p.start9.to") {
let (hrp, data) = bech32::decode(b32).with_kind(ErrorKind::ParseNetAddress)?;
ensure_code!(
hrp == HRP,
ErrorKind::ParseNetAddress,
"not an iroh address"
);
Ok(Self(
NodeId::from_bytes(&*<Box<[u8; 32]>>::try_from(data).map_err(|_| {
Error::new(eyre!("invalid length"), ErrorKind::ParseNetAddress)
})?)
.with_kind(ErrorKind::ParseNetAddress)?,
))
} else {
Err(Error::new(
eyre!("Invalid iroh address"),
ErrorKind::ParseNetAddress,
))
}
}
}
impl Serialize for IrohAddress {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serialize_display(self, serializer)
}
}
impl<'de> Deserialize<'de> for IrohAddress {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
deserialize_from_str(deserializer)
}
}
impl PartialEq for IrohAddress {
fn eq(&self, other: &Self) -> bool {
self.0.as_ref() == other.0.as_ref()
}
}
impl Eq for IrohAddress {}
impl PartialOrd for IrohAddress {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
self.0.as_ref().partial_cmp(other.0.as_ref())
}
}
impl Ord for IrohAddress {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.0.as_ref().cmp(other.0.as_ref())
}
}
#[derive(Clone, Debug)]
pub struct IrohSecretKey(pub SecretKey);
impl IrohSecretKey {
pub fn iroh_address(&self) -> IrohAddress {
IrohAddress(self.0.public())
}
pub fn generate() -> Self {
Self(SecretKey::generate(
&mut ssh_key::rand_core::OsRng::default(),
))
}
}
impl PemEncoding for IrohSecretKey {
fn from_pem<E: serde::de::Error>(pem: &str) -> Result<Self, E> {
ed25519_dalek::SigningKey::from_pem(pem)
.map(From::from)
.map(Self)
}
fn to_pem<E: serde::ser::Error>(&self) -> Result<String, E> {
self.0.secret().to_pem()
}
}
#[derive(Default, Debug, Deserialize, Serialize)]
pub struct IrohKeyStore(BTreeMap<IrohAddress, Pem<IrohSecretKey>>);
impl Map for IrohKeyStore {
type Key = IrohAddress;
type Value = Pem<IrohSecretKey>;
fn key_str(key: &Self::Key) -> Result<impl AsRef<str>, Error> {
Self::key_string(key)
}
fn key_string(key: &Self::Key) -> Result<imbl_value::InternedString, Error> {
Ok(InternedString::from_display(key))
}
}
impl IrohKeyStore {
pub fn new() -> Self {
Self::default()
}
pub fn insert(&mut self, key: IrohSecretKey) {
self.0.insert(key.iroh_address(), Pem::new(key));
}
}
impl Model<IrohKeyStore> {
pub fn new_key(&mut self) -> Result<IrohSecretKey, Error> {
let key = IrohSecretKey::generate();
self.insert(&key.iroh_address(), &Pem::new(key))?;
Ok(key)
}
pub fn insert_key(&mut self, key: &IrohSecretKey) -> Result<(), Error> {
self.insert(&key.iroh_address(), Pem::new_ref(key))
}
pub fn get_key(&self, address: &IrohAddress) -> Result<IrohSecretKey, Error> {
self.as_idx(address)
.or_not_found(lazy_format!("private key for {address}"))?
.de()
.map(|k| k.0)
}
}
pub fn iroh_api<C: Context>() -> ParentHandler<C> {
ParentHandler::new()
.subcommand(
"list-services",
from_fn_async(list_services)
.with_display_serializable()
.with_custom_display_fn(|handle, result| display_services(handle.params, result))
.with_about("Display the status of running iroh services")
.with_call_remote::<CliContext>(),
)
.subcommand(
"key",
key::<C>().with_about("Manage the iroh service key store"),
)
}
pub fn key<C: Context>() -> ParentHandler<C> {
ParentHandler::new()
.subcommand(
"generate",
from_fn_async(generate_key)
.with_about("Generate an iroh service key and add it to the key store")
.with_call_remote::<CliContext>(),
)
.subcommand(
"add",
from_fn_async(add_key)
.with_about("Add an iroh service key to the key store")
.with_call_remote::<CliContext>(),
)
.subcommand(
"list",
from_fn_async(list_keys)
.with_custom_display_fn(|_, res| {
for addr in res {
println!("{addr}");
}
Ok(())
})
.with_about("List iroh services with keys in the key store")
.with_call_remote::<CliContext>(),
)
}
pub async fn generate_key(ctx: RpcContext) -> Result<IrohAddress, Error> {
ctx.db
.mutate(|db| {
Ok(db
.as_private_mut()
.as_key_store_mut()
.as_iroh_mut()
.new_key()?
.iroh_address())
})
.await
.result
}
#[derive(Deserialize, Serialize, Parser)]
pub struct AddKeyParams {
pub key: Pem<IrohSecretKey>,
}
pub async fn add_key(
ctx: RpcContext,
AddKeyParams { key }: AddKeyParams,
) -> Result<IrohAddress, Error> {
ctx.db
.mutate(|db| {
db.as_private_mut()
.as_key_store_mut()
.as_iroh_mut()
.insert_key(&key.0)
})
.await
.result?;
Ok(key.iroh_address())
}
pub async fn list_keys(ctx: RpcContext) -> Result<BTreeSet<IrohAddress>, Error> {
ctx.db
.peek()
.await
.into_private()
.into_key_store()
.into_iroh()
.keys()
}
pub fn display_services(
params: WithIoFormat<Empty>,
services: BTreeMap<IrohAddress, IrohServiceInfo>,
) -> Result<(), Error> {
use prettytable::*;
if let Some(format) = params.format {
return display_serializable(format, services);
}
let mut table = Table::new();
table.add_row(row![bc => "ADDRESS", "BINDINGS"]);
for (service, info) in services {
let row = row![
&service.to_string(),
&info
.bindings
.into_iter()
.map(|((subdomain, port), addr)| lazy_format!("{subdomain}:{port} -> {addr}"))
.join("; ")
];
table.add_row(row);
}
table.print_tty(false)?;
Ok(())
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct IrohServiceInfo {
pub bindings: BTreeMap<(InternedString, u16), SocketAddr>,
}
pub async fn list_services(
ctx: RpcContext,
_: Empty,
) -> Result<BTreeMap<IrohAddress, IrohServiceInfo>, Error> {
ctx.net_controller.iroh.list_services().await
}
#[derive(Clone)]
pub struct IrohController(Arc<IrohControllerInner>);
struct IrohControllerInner {
// client: Endpoint,
services: SyncMutex<BTreeMap<IrohAddress, IrohService>>,
}
impl IrohController {
pub fn new() -> Result<Self, Error> {
Ok(Self(Arc::new(IrohControllerInner {
services: SyncMutex::new(BTreeMap::new()),
})))
}
pub fn service(&self, key: IrohSecretKey) -> Result<IrohService, Error> {
self.0.services.mutate(|s| {
use std::collections::btree_map::Entry;
let addr = key.iroh_address();
match s.entry(addr) {
Entry::Occupied(e) => Ok(e.get().clone()),
Entry::Vacant(e) => Ok(e
.insert(IrohService::launch(self.0.client.clone(), key)?)
.clone()),
}
})
}
pub async fn gc(&self, addr: Option<IrohAddress>) -> Result<(), Error> {
if let Some(addr) = addr {
if let Some(s) = self.0.services.mutate(|s| {
let rm = if let Some(s) = s.get(&addr) {
!s.gc()
} else {
false
};
if rm {
s.remove(&addr)
} else {
None
}
}) {
s.shutdown().await
} else {
Ok(())
}
} else {
for s in self.0.services.mutate(|s| {
let mut rm = Vec::new();
s.retain(|_, s| {
if s.gc() {
true
} else {
rm.push(s.clone());
false
}
});
rm
}) {
s.shutdown().await?;
}
Ok(())
}
}
pub async fn list_services(&self) -> Result<BTreeMap<IrohAddress, IrohServiceInfo>, Error> {
Ok(self
.0
.services
.peek(|s| s.iter().map(|(a, s)| (a.clone(), s.info())).collect()))
}
pub async fn connect_iroh(
&self,
addr: &IrohAddress,
port: u16,
) -> Result<Box<dyn ReadWriter + Unpin + Send + Sync + 'static>, Error> {
if let Some(target) = self.0.services.peek(|s| {
s.get(addr).and_then(|s| {
s.0.bindings.peek(|b| {
b.get(&port).and_then(|b| {
b.iter()
.find(|(_, rc)| rc.strong_count() > 0)
.map(|(a, _)| *a)
})
})
})
}) {
Ok(Box::new(
TcpStream::connect(target)
.await
.with_kind(ErrorKind::Network)?,
))
} else {
todo!()
}
}
}
#[derive(Clone)]
pub struct IrohService(Arc<IrohServiceData>);
struct IrohServiceData {
service: Endpoint,
bindings: Arc<SyncRwLock<BTreeMap<(InternedString, u16), BTreeMap<SocketAddr, Weak<()>>>>>,
_thread: NonDetachingJoinHandle<()>,
}
impl IrohService {
fn launch(
mut client: Watch<(usize, IrohClient<TokioRustlsRuntime>)>,
key: IrohSecretKey,
) -> Result<Self, Error> {
let service = Arc::new(SyncMutex::new(None));
let bindings = Arc::new(SyncRwLock::new(BTreeMap::<
u16,
BTreeMap<SocketAddr, Weak<()>>,
>::new()));
Ok(Self(Arc::new(IrohServiceData {
service: service.clone(),
bindings: bindings.clone(),
_thread: tokio::spawn(async move {
let (bg, mut runner) = BackgroundJobQueue::new();
runner
.run_while(async {
loop {
if let Err(e) = async {
client.wait_for(|(_,c)| c.bootstrap_status().ready_for_traffic()).await;
let epoch = client.peek(|(e, c)| {
ensure_code!(c.bootstrap_status().ready_for_traffic(), ErrorKind::Iroh, "client recycled");
Ok::<_, Error>(*e)
})?;
let (new_service, stream) = client.peek(|(_, c)| {
c.launch_onion_service_with_hsid(
IrohServiceConfigBuilder::default()
.nickname(
key.iroh_address()
.to_string()
.trim_end_matches(".onion")
.parse::<HsNickname>()
.with_kind(ErrorKind::Iroh)?,
)
.build()
.with_kind(ErrorKind::Iroh)?,
key.clone().0,
)
.with_kind(ErrorKind::Iroh)
})?;
let mut status_stream = new_service.status_events();
bg.add_job(async move {
while let Some(status) = status_stream.next().await {
// TODO: health daemon?
}
});
service.replace(Some(new_service));
let mut stream = tor_hsservice::handle_rend_requests(stream);
while let Some(req) = tokio::select! {
req = stream.next() => req,
_ = client.wait_for(|(e, _)| *e != epoch) => None
} {
bg.add_job({
let bg = bg.clone();
let bindings = bindings.clone();
async move {
if let Err(e) = async {
let IncomingStreamRequest::Begin(begin) =
req.request()
else {
return req
.reject(tor_cell::relaycell::msg::End::new_with_reason(
tor_cell::relaycell::msg::EndReason::DONE,
))
.await
.with_kind(ErrorKind::Iroh);
};
let Some(target) = bindings.peek(|b| {
b.get(&begin.port()).and_then(|a| {
a.iter()
.find(|(_, rc)| rc.strong_count() > 0)
.map(|(addr, _)| *addr)
})
}) else {
return req
.reject(tor_cell::relaycell::msg::End::new_with_reason(
tor_cell::relaycell::msg::EndReason::DONE,
))
.await
.with_kind(ErrorKind::Iroh);
};
bg.add_job(async move {
if let Err(e) = async {
let mut outgoing =
TcpStream::connect(target)
.await
.with_kind(ErrorKind::Network)?;
let mut incoming = req
.accept(Connected::new_empty())
.await
.with_kind(ErrorKind::Iroh)?;
if let Err(e) =
tokio::io::copy_bidirectional(
&mut outgoing,
&mut incoming,
)
.await
{
tracing::error!("Iroh Stream Error: {e}");
tracing::debug!("{e:?}");
}
Ok::<_, Error>(())
}
.await
{
tracing::trace!("Iroh Stream Error: {e}");
tracing::trace!("{e:?}");
}
});
Ok::<_, Error>(())
}
.await
{
tracing::trace!("Iroh Request Error: {e}");
tracing::trace!("{e:?}");
}
}
});
}
Ok::<_, Error>(())
}
.await
{
tracing::error!("Iroh Client Error: {e}");
tracing::debug!("{e:?}");
}
}
})
.await
})
.into(),
})))
}
pub fn proxy_all<Rcs: FromIterator<Arc<()>>>(
&self,
bindings: impl IntoIterator<Item = (InternedString, u16, SocketAddr)>,
) -> Rcs {
self.0.bindings.mutate(|b| {
bindings
.into_iter()
.map(|(subdomain, port, target)| {
let entry = b
.entry((subdomain, port))
.or_default()
.entry(target)
.or_default();
if let Some(rc) = entry.upgrade() {
rc
} else {
let rc = Arc::new(());
*entry = Arc::downgrade(&rc);
rc
}
})
.collect()
})
}
pub fn gc(&self) -> bool {
self.0.bindings.mutate(|b| {
b.retain(|_, targets| {
targets.retain(|_, rc| rc.strong_count() > 0);
!targets.is_empty()
});
!b.is_empty()
})
}
pub async fn shutdown(self) -> Result<(), Error> {
self.0.service.replace(None);
self.0._thread.abort();
Ok(())
}
pub fn state(&self) -> IrohServiceState {
self.0
.service
.peek(|s| s.as_ref().map(|s| s.status().state().into()))
.unwrap_or(IrohServiceState::Bootstrapping)
}
pub fn info(&self) -> IrohServiceInfo {
IrohServiceInfo {
state: self.state(),
bindings: self.0.bindings.peek(|b| {
b.iter()
.filter_map(|(port, b)| {
b.iter()
.find(|(_, rc)| rc.strong_count() > 0)
.map(|(addr, _)| (*port, *addr))
})
.collect()
}),
}
}
}

View File

@@ -2,14 +2,17 @@ use serde::{Deserialize, Serialize};
use crate::account::AccountInfo; use crate::account::AccountInfo;
use crate::net::acme::AcmeCertStore; use crate::net::acme::AcmeCertStore;
use crate::net::iroh::IrohKeyStore;
use crate::net::ssl::CertStore; use crate::net::ssl::CertStore;
use crate::net::tor::OnionStore; use crate::net::tor::OnionKeyStore;
use crate::prelude::*; use crate::prelude::*;
#[derive(Debug, Deserialize, Serialize, HasModel)] #[derive(Debug, Deserialize, Serialize, HasModel)]
#[model = "Model<Self>"] #[model = "Model<Self>"]
pub struct KeyStore { pub struct KeyStore {
pub onion: OnionStore, pub onion: OnionKeyStore,
#[serde(default)]
pub iroh: IrohKeyStore,
pub local_certs: CertStore, pub local_certs: CertStore,
#[serde(default)] #[serde(default)]
pub acme: AcmeCertStore, pub acme: AcmeCertStore,
@@ -17,7 +20,8 @@ pub struct KeyStore {
impl KeyStore { impl KeyStore {
pub fn new(account: &AccountInfo) -> Result<Self, Error> { pub fn new(account: &AccountInfo) -> Result<Self, Error> {
let mut res = Self { let mut res = Self {
onion: OnionStore::new(), onion: OnionKeyStore::new(),
iroh: IrohKeyStore::new(),
local_certs: CertStore::new(account)?, local_certs: CertStore::new(account)?,
acme: AcmeCertStore::new(), acme: AcmeCertStore::new(),
}; };

View File

@@ -5,6 +5,7 @@ pub mod dns;
pub mod forward; pub mod forward;
pub mod gateway; pub mod gateway;
pub mod host; pub mod host;
pub mod iroh;
pub mod keys; pub mod keys;
pub mod mdns; pub mod mdns;
pub mod net_controller; pub mod net_controller;

View File

@@ -11,7 +11,7 @@ use tokio::sync::Mutex;
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
use tracing::instrument; use tracing::instrument;
use crate::db::model::public::{NetworkInterfaceInfo, NetworkInterfaceType}; use crate::db::model::public::NetworkInterfaceInfo;
use crate::db::model::Database; use crate::db::model::Database;
use crate::error::ErrorCollection; use crate::error::ErrorCollection;
use crate::hostname::Hostname; use crate::hostname::Hostname;
@@ -24,7 +24,8 @@ use crate::net::gateway::{
use crate::net::host::address::HostAddress; use crate::net::host::address::HostAddress;
use crate::net::host::binding::{AddSslOptions, BindId, BindOptions}; use crate::net::host::binding::{AddSslOptions, BindId, BindOptions};
use crate::net::host::{host_for, Host, Hosts}; use crate::net::host::{host_for, Host, Hosts};
use crate::net::service_interface::{GatewayInfo, HostnameInfo, IpHostname, OnionHostname}; use crate::net::iroh::IrohController;
use crate::net::service_interface::{HostnameInfo, IpHostname, OnionHostname};
use crate::net::socks::SocksController; use crate::net::socks::SocksController;
use crate::net::tor::{OnionAddress, TorController, TorSecretKey}; use crate::net::tor::{OnionAddress, TorController, TorSecretKey};
use crate::net::utils::ipv6_is_local; use crate::net::utils::ipv6_is_local;
@@ -37,6 +38,7 @@ use crate::HOST_IP;
pub struct NetController { pub struct NetController {
pub(crate) db: TypedPatchDb<Database>, pub(crate) db: TypedPatchDb<Database>,
pub(super) tor: TorController, pub(super) tor: TorController,
pub(super) iroh: IrohController,
pub(super) vhost: VHostController, pub(super) vhost: VHostController,
pub(crate) net_iface: Arc<NetworkInterfaceController>, pub(crate) net_iface: Arc<NetworkInterfaceController>,
pub(super) dns: DnsController, pub(super) dns: DnsController,
@@ -54,10 +56,12 @@ impl NetController {
) -> Result<Self, Error> { ) -> Result<Self, Error> {
let net_iface = Arc::new(NetworkInterfaceController::new(db.clone())); let net_iface = Arc::new(NetworkInterfaceController::new(db.clone()));
let tor = TorController::new()?; let tor = TorController::new()?;
let iroh = IrohController::new()?;
let socks = SocksController::new(socks_listen, tor.clone())?; let socks = SocksController::new(socks_listen, tor.clone())?;
Ok(Self { Ok(Self {
db: db.clone(), db: db.clone(),
tor, tor,
iroh,
vhost: VHostController::new(db.clone(), net_iface.clone()), vhost: VHostController::new(db.clone(), net_iface.clone()),
dns: DnsController::init(db, &net_iface.watcher).await?, dns: DnsController::init(db, &net_iface.watcher).await?,
forward: PortForwardController::new(net_iface.watcher.subscribe()), forward: PortForwardController::new(net_iface.watcher.subscribe()),
@@ -295,11 +299,7 @@ impl NetServiceData {
); // TODO: wrap onion ssl stream directly in tor ctrl ); // TODO: wrap onion ssl stream directly in tor ctrl
} }
} }
HostAddress::Domain { HostAddress::Domain { address, public } => {
address,
public,
private,
} => {
if hostnames.insert(address.clone()) { if hostnames.insert(address.clone()) {
let address = Some(address.clone()); let address = Some(address.clone());
if ssl.preferred_external_port == 443 { if ssl.preferred_external_port == 443 {
@@ -325,19 +325,10 @@ impl NetServiceData {
TargetInfo { TargetInfo {
filter: AndFilter( filter: AndFilter(
bind.net.clone(), bind.net.clone(),
if private { OrFilter(
OrFilter( IdFilter(public.gateway.clone()),
IdFilter(public.gateway.clone()), PublicFilter { public: false },
PublicFilter { public: false }, ),
)
.into_dyn()
} else {
AndFilter(
IdFilter(public.gateway.clone()),
PublicFilter { public: true },
)
.into_dyn()
},
) )
.into_dyn(), .into_dyn(),
acme: public.acme.clone(), acme: public.acme.clone(),
@@ -367,16 +358,7 @@ impl NetServiceData {
TargetInfo { TargetInfo {
filter: AndFilter( filter: AndFilter(
bind.net.clone(), bind.net.clone(),
if private { IdFilter(public.gateway.clone()),
OrFilter(
IdFilter(public.gateway.clone()),
PublicFilter { public: false },
)
.into_dyn()
} else {
IdFilter(public.gateway.clone())
.into_dyn()
},
) )
.into_dyn(), .into_dyn(),
acme: public.acme.clone(), acme: public.acme.clone(),
@@ -388,11 +370,7 @@ impl NetServiceData {
vhosts.insert( vhosts.insert(
(address.clone(), external), (address.clone(), external),
TargetInfo { TargetInfo {
filter: AndFilter( filter: bind.net.clone().into_dyn(),
bind.net.clone(),
PublicFilter { public: false },
)
.into_dyn(),
acme: None, acme: None,
addr, addr,
connect_ssl: connect_ssl.clone(), connect_ssl: connect_ssl.clone(),
@@ -427,91 +405,66 @@ impl NetServiceData {
} }
let mut bind_hostname_info: Vec<HostnameInfo> = let mut bind_hostname_info: Vec<HostnameInfo> =
hostname_info.remove(port).unwrap_or_default(); hostname_info.remove(port).unwrap_or_default();
for (gateway_id, info) in net_ifaces for (interface, info) in net_ifaces
.iter() .iter()
.filter(|(id, info)| bind.net.filter(id, info)) .filter(|(id, info)| bind.net.filter(id, info))
{ {
let gateway = GatewayInfo { if !info.public() {
id: gateway_id.clone(),
name: info
.name
.clone()
.or_else(|| info.ip_info.as_ref().map(|i| i.name.clone()))
.unwrap_or_else(|| gateway_id.clone().into()),
public: info.public(),
};
let port = bind.net.assigned_port.filter(|_| {
bind.options.secure.map_or(false, |s| {
!(s.ssl && bind.options.add_ssl.is_some()) || info.secure()
})
});
if !info.public()
&& info.ip_info.as_ref().map_or(false, |i| {
i.device_type != Some(NetworkInterfaceType::Wireguard)
})
{
bind_hostname_info.push(HostnameInfo::Ip { bind_hostname_info.push(HostnameInfo::Ip {
gateway: gateway.clone(), gateway_id: interface.clone(),
public: false, public: false,
hostname: IpHostname::Local { hostname: IpHostname::Local {
value: InternedString::from_display(&{ value: InternedString::from_display(&{
let hostname = &hostname; let hostname = &hostname;
lazy_format!("{hostname}.local") lazy_format!("{hostname}.local")
}), }),
port, port: bind.net.assigned_port,
ssl_port: bind.net.assigned_ssl_port, ssl_port: bind.net.assigned_ssl_port,
}, },
}); });
} }
for address in host.addresses() { for address in host.addresses() {
if let HostAddress::Domain { if let HostAddress::Domain {
address, address, public, ..
public,
private,
} = address } = address
{ {
let private = private && !info.public(); if bind
let public = .options
public.as_ref().map_or(false, |p| &p.gateway == gateway_id); .add_ssl
if public || private { .as_ref()
if bind .map_or(false, |ssl| ssl.preferred_external_port == 443)
.options {
.add_ssl bind_hostname_info.push(HostnameInfo::Ip {
.as_ref() gateway_id: interface.clone(),
.map_or(false, |ssl| ssl.preferred_external_port == 443) public: public.is_some(),
{ hostname: IpHostname::Domain {
bind_hostname_info.push(HostnameInfo::Ip { value: address.clone(),
gateway: gateway.clone(), port: None,
public, ssl_port: Some(443),
hostname: IpHostname::Domain { },
value: address.clone(), });
port: None, } else {
ssl_port: Some(443), bind_hostname_info.push(HostnameInfo::Ip {
}, gateway_id: interface.clone(),
}); public: public.is_some(),
} else { hostname: IpHostname::Domain {
bind_hostname_info.push(HostnameInfo::Ip { value: address.clone(),
gateway: gateway.clone(), port: bind.net.assigned_port,
public, ssl_port: bind.net.assigned_ssl_port,
hostname: IpHostname::Domain { },
value: address.clone(), });
port,
ssl_port: bind.net.assigned_ssl_port,
},
});
}
} }
} }
} }
if let Some(ip_info) = &info.ip_info { if let Some(ip_info) = &info.ip_info {
let public = info.public(); let public = info.public();
if let Some(wan_ip) = ip_info.wan_ip { if let Some(wan_ip) = ip_info.wan_ip.filter(|_| public) {
bind_hostname_info.push(HostnameInfo::Ip { bind_hostname_info.push(HostnameInfo::Ip {
gateway: gateway.clone(), gateway_id: interface.clone(),
public: true, public,
hostname: IpHostname::Ipv4 { hostname: IpHostname::Ipv4 {
value: wan_ip, value: wan_ip,
port, port: bind.net.assigned_port,
ssl_port: bind.net.assigned_ssl_port, ssl_port: bind.net.assigned_ssl_port,
}, },
}); });
@@ -521,11 +474,11 @@ impl NetServiceData {
IpNet::V4(net) => { IpNet::V4(net) => {
if !public { if !public {
bind_hostname_info.push(HostnameInfo::Ip { bind_hostname_info.push(HostnameInfo::Ip {
gateway: gateway.clone(), gateway_id: interface.clone(),
public, public,
hostname: IpHostname::Ipv4 { hostname: IpHostname::Ipv4 {
value: net.addr(), value: net.addr(),
port, port: bind.net.assigned_port,
ssl_port: bind.net.assigned_ssl_port, ssl_port: bind.net.assigned_ssl_port,
}, },
}); });
@@ -533,12 +486,12 @@ impl NetServiceData {
} }
IpNet::V6(net) => { IpNet::V6(net) => {
bind_hostname_info.push(HostnameInfo::Ip { bind_hostname_info.push(HostnameInfo::Ip {
gateway: gateway.clone(), gateway_id: interface.clone(),
public: public && !ipv6_is_local(net.addr()), public: public && !ipv6_is_local(net.addr()),
hostname: IpHostname::Ipv6 { hostname: IpHostname::Ipv6 {
value: net.addr(), value: net.addr(),
scope_id: ip_info.scope_id, scope_id: ip_info.scope_id,
port, port: bind.net.assigned_port,
ssl_port: bind.net.assigned_ssl_port, ssl_port: bind.net.assigned_ssl_port,
}, },
}); });

View File

@@ -12,7 +12,8 @@ use ts_rs::TS;
#[serde(tag = "kind")] #[serde(tag = "kind")]
pub enum HostnameInfo { pub enum HostnameInfo {
Ip { Ip {
gateway: GatewayInfo, #[ts(type = "string")]
gateway_id: GatewayId,
public: bool, public: bool,
hostname: IpHostname, hostname: IpHostname,
}, },
@@ -29,15 +30,6 @@ impl HostnameInfo {
} }
} }
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct GatewayInfo {
pub id: GatewayId,
pub name: InternedString,
pub public: bool,
}
#[derive(Clone, Debug, Deserialize, Serialize, TS)] #[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)] #[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]

View File

@@ -1,6 +1,5 @@
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration;
use helpers::NonDetachingJoinHandle; use helpers::NonDetachingJoinHandle;
use socks5_impl::protocol::{Address, Reply}; use socks5_impl::protocol::{Address, Reply};
@@ -23,21 +22,15 @@ pub struct SocksController {
} }
impl SocksController { impl SocksController {
pub fn new(listen: SocketAddr, tor: TorController) -> Result<Self, Error> { pub fn new(listen: SocketAddr, tor: TorController) -> Result<Self, Error> {
let auth: AuthAdaptor<()> = Arc::new(NoAuth);
let listener = TcpListener::from_std(
mio::net::TcpListener::bind(listen)
.with_kind(ErrorKind::Network)?
.into(),
)
.with_kind(ErrorKind::Network)?;
Ok(Self { Ok(Self {
_thread: tokio::spawn(async move { _thread: tokio::spawn(async move {
let auth: AuthAdaptor<()> = Arc::new(NoAuth);
let listener;
loop {
if let Some(l) = TcpListener::bind(listen)
.await
.with_kind(ErrorKind::Network)
.log_err()
{
listener = l;
break;
}
tokio::time::sleep(Duration::from_secs(1)).await;
}
let (bg, mut runner) = BackgroundJobQueue::new(); let (bg, mut runner) = BackgroundJobQueue::new();
runner runner
.run_while(async { .run_while(async {
@@ -156,8 +149,8 @@ impl SocksController {
} }
.await .await
{ {
tracing::trace!("SOCKS5 Stream Error: {e}"); tracing::error!("SOCKS5 Stream Error: {e}");
tracing::trace!("{e:?}"); tracing::debug!("{e:?}");
} }
}); });
} }

View File

@@ -6,7 +6,7 @@ use std::sync::{Arc, Weak};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use arti_client::config::onion_service::OnionServiceConfigBuilder; use arti_client::config::onion_service::OnionServiceConfigBuilder;
use arti_client::{DataStream, TorClient, TorClientConfig}; use arti_client::{TorClient, TorClientConfig};
use base64::Engine; use base64::Engine;
use clap::Parser; use clap::Parser;
use color_eyre::eyre::eyre; use color_eyre::eyre::eyre;
@@ -62,7 +62,7 @@ impl FromStr for OnionAddress {
Cow::Owned(format!("{s}.onion")) Cow::Owned(format!("{s}.onion"))
} }
.parse::<HsId>() .parse::<HsId>()
.with_kind(ErrorKind::Tor)?, .with_kind(ErrorKind::ParseNetAddress)?,
)) ))
} }
} }
@@ -165,8 +165,8 @@ impl<'de> Deserialize<'de> for TorSecretKey {
} }
#[derive(Default, Deserialize, Serialize)] #[derive(Default, Deserialize, Serialize)]
pub struct OnionStore(BTreeMap<OnionAddress, TorSecretKey>); pub struct OnionKeyStore(BTreeMap<OnionAddress, TorSecretKey>);
impl Map for OnionStore { impl Map for OnionKeyStore {
type Key = OnionAddress; type Key = OnionAddress;
type Value = TorSecretKey; type Value = TorSecretKey;
fn key_str(key: &Self::Key) -> Result<impl AsRef<str>, Error> { fn key_str(key: &Self::Key) -> Result<impl AsRef<str>, Error> {
@@ -176,7 +176,7 @@ impl Map for OnionStore {
Ok(InternedString::from_display(key)) Ok(InternedString::from_display(key))
} }
} }
impl OnionStore { impl OnionKeyStore {
pub fn new() -> Self { pub fn new() -> Self {
Self::default() Self::default()
} }
@@ -184,7 +184,7 @@ impl OnionStore {
self.0.insert(key.onion_address(), key); self.0.insert(key.onion_address(), key);
} }
} }
impl Model<OnionStore> { impl Model<OnionKeyStore> {
pub fn new_key(&mut self) -> Result<TorSecretKey, Error> { pub fn new_key(&mut self) -> Result<TorSecretKey, Error> {
let key = TorSecretKey::generate(); let key = TorSecretKey::generate();
self.insert(&key.onion_address(), &key)?; self.insert(&key.onion_address(), &key)?;
@@ -199,7 +199,7 @@ impl Model<OnionStore> {
.de() .de()
} }
} }
impl std::fmt::Debug for OnionStore { impl std::fmt::Debug for OnionKeyStore {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
struct OnionStoreMap<'a>(&'a BTreeMap<OnionAddress, TorSecretKey>); struct OnionStoreMap<'a>(&'a BTreeMap<OnionAddress, TorSecretKey>);
impl<'a> std::fmt::Debug for OnionStoreMap<'a> { impl<'a> std::fmt::Debug for OnionStoreMap<'a> {
@@ -227,7 +227,7 @@ pub fn tor_api<C: Context>() -> ParentHandler<C> {
from_fn_async(list_services) from_fn_async(list_services)
.with_display_serializable() .with_display_serializable()
.with_custom_display_fn(|handle, result| display_services(handle.params, result)) .with_custom_display_fn(|handle, result| display_services(handle.params, result))
.with_about("Display Tor V3 Onion Addresses") .with_about("Show the status of running onion services")
.with_call_remote::<CliContext>(), .with_call_remote::<CliContext>(),
) )
.subcommand( .subcommand(
@@ -417,7 +417,6 @@ impl TorController {
0, 0,
TorClient::with_runtime(TokioRustlsRuntime::current()?) TorClient::with_runtime(TokioRustlsRuntime::current()?)
.config(config.build().with_kind(ErrorKind::Tor)?) .config(config.build().with_kind(ErrorKind::Tor)?)
.local_resource_timeout(Duration::from_secs(0))
.create_unbootstrapped()?, .create_unbootstrapped()?,
)); ));
let reset = Arc::new(Notify::new()); let reset = Arc::new(Notify::new());
@@ -425,10 +424,10 @@ impl TorController {
let bootstrapper_client = client.clone(); let bootstrapper_client = client.clone();
let bootstrapper = tokio::spawn(async move { let bootstrapper = tokio::spawn(async move {
loop { loop {
let (epoch, client): (usize, _) = bootstrapper_client.read();
if let Err(e) = Until::new() if let Err(e) = Until::new()
.with_async_fn(|| bootstrapper_reset.notified().map(Ok)) .with_async_fn(|| bootstrapper_reset.notified().map(Ok))
.run(async { .run(async {
let (epoch, client): (usize, _) = bootstrapper_client.read();
let mut events = client.bootstrap_events(); let mut events = client.bootstrap_events();
let bootstrap_fut = let bootstrap_fut =
client.bootstrap().map(|res| res.with_kind(ErrorKind::Tor)); client.bootstrap().map(|res| res.with_kind(ErrorKind::Tor));
@@ -561,7 +560,7 @@ impl TorController {
} }
.await .await
{ {
tracing::error!("Tor Client Health Error: {e}"); tracing::error!("Tor Client Creation Error: {e}");
tracing::debug!("{e:?}"); tracing::debug!("{e:?}");
} }
} }
@@ -570,7 +569,21 @@ impl TorController {
HEALTH_CHECK_FAILURE_ALLOWANCE HEALTH_CHECK_FAILURE_ALLOWANCE
); );
} }
if let Err::<(), Error>(e) = async {
tokio::time::sleep(RETRY_COOLDOWN).await;
bootstrapper_client.send((
epoch.wrapping_add(1),
TorClient::with_runtime(TokioRustlsRuntime::current()?)
.config(config.build().with_kind(ErrorKind::Tor)?)
.create_unbootstrapped()?,
));
Ok(())
}
.await
{
tracing::error!("Tor Client Creation Error: {e}");
tracing::debug!("{e:?}");
}
Ok(()) Ok(())
}) })
.await .await
@@ -578,24 +591,6 @@ impl TorController {
tracing::error!("Tor Bootstrapper Error: {e}"); tracing::error!("Tor Bootstrapper Error: {e}");
tracing::debug!("{e:?}"); tracing::debug!("{e:?}");
} }
if let Err::<(), Error>(e) = async {
tokio::time::sleep(RETRY_COOLDOWN).await;
bootstrapper_client.send((
epoch.wrapping_add(1),
TorClient::with_runtime(TokioRustlsRuntime::current()?)
.config(config.build().with_kind(ErrorKind::Tor)?)
.local_resource_timeout(Duration::from_secs(0))
.create_unbootstrapped_async()
.await?,
));
tracing::debug!("TorClient recycled");
Ok(())
}
.await
{
tracing::error!("Tor Client Creation Error: {e}");
tracing::debug!("{e:?}");
}
} }
}) })
.into(); .into();
@@ -734,15 +729,14 @@ impl OnionService {
if let Err(e) = async { if let Err(e) = async {
client.wait_for(|(_,c)| c.bootstrap_status().ready_for_traffic()).await; client.wait_for(|(_,c)| c.bootstrap_status().ready_for_traffic()).await;
let epoch = client.peek(|(e, c)| { let epoch = client.peek(|(e, c)| {
ensure_code!(c.bootstrap_status().ready_for_traffic(), ErrorKind::Tor, "TorClient recycled"); ensure_code!(c.bootstrap_status().ready_for_traffic(), ErrorKind::Tor, "client recycled");
Ok::<_, Error>(*e) Ok::<_, Error>(*e)
})?; })?;
let addr = key.onion_address();
let (new_service, stream) = client.peek(|(_, c)| { let (new_service, stream) = client.peek(|(_, c)| {
c.launch_onion_service_with_hsid( c.launch_onion_service_with_hsid(
OnionServiceConfigBuilder::default() OnionServiceConfigBuilder::default()
.nickname( .nickname(
addr key.onion_address()
.to_string() .to_string()
.trim_end_matches(".onion") .trim_end_matches(".onion")
.parse::<HsNickname>() .parse::<HsNickname>()
@@ -755,20 +749,8 @@ impl OnionService {
.with_kind(ErrorKind::Tor) .with_kind(ErrorKind::Tor)
})?; })?;
let mut status_stream = new_service.status_events(); let mut status_stream = new_service.status_events();
let mut status = new_service.status();
if status.state().is_fully_reachable() {
tracing::debug!("{addr} is fully reachable");
} else {
tracing::debug!("{addr} is not fully reachable");
}
bg.add_job(async move { bg.add_job(async move {
while let Some(new_status) = status_stream.next().await { while let Some(status) = status_stream.next().await {
if status.state().is_fully_reachable() && !new_status.state().is_fully_reachable() {
tracing::debug!("{addr} is no longer fully reachable");
} else if !status.state().is_fully_reachable() && new_status.state().is_fully_reachable() {
tracing::debug!("{addr} is now fully reachable");
}
status = new_status;
// TODO: health daemon? // TODO: health daemon?
} }
}); });
@@ -824,8 +806,8 @@ impl OnionService {
) )
.await .await
{ {
tracing::trace!("Tor Stream Error: {e}"); tracing::error!("Tor Stream Error: {e}");
tracing::trace!("{e:?}"); tracing::debug!("{e:?}");
} }
Ok::<_, Error>(()) Ok::<_, Error>(())

View File

@@ -1,17 +1,16 @@
use clap::Parser; use clap::Parser;
use imbl_value::InternedString; use imbl_value::InternedString;
use models::GatewayId; use models::GatewayId;
use patch_db::json_ptr::JsonPointer; use rpc_toolkit::{Context, HandlerExt, ParentHandler, from_fn_async};
use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::process::Command; use tokio::process::Command;
use ts_rs::TS; use ts_rs::TS;
use crate::context::{CliContext, RpcContext}; use crate::context::{CliContext, RpcContext};
use crate::db::model::public::{NetworkInterfaceInfo, NetworkInterfaceType}; use crate::db::model::public::NetworkInterfaceType;
use crate::prelude::*; use crate::prelude::*;
use crate::util::io::{write_file_atomic, TmpDir};
use crate::util::Invoke; use crate::util::Invoke;
use crate::util::io::{TmpDir, write_file_atomic};
pub fn tunnel_api<C: Context>() -> ParentHandler<C> { pub fn tunnel_api<C: Context>() -> ParentHandler<C> {
ParentHandler::new() ParentHandler::new()
@@ -33,6 +32,7 @@ pub fn tunnel_api<C: Context>() -> ParentHandler<C> {
#[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)] #[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)]
#[ts(export)] #[ts(export)]
pub struct AddTunnelParams { pub struct AddTunnelParams {
#[ts(type = "string")]
name: InternedString, name: InternedString,
config: String, config: String,
public: bool, public: bool,
@@ -46,46 +46,26 @@ pub async fn add_tunnel(
public, public,
}: AddTunnelParams, }: AddTunnelParams,
) -> Result<GatewayId, Error> { ) -> Result<GatewayId, Error> {
let ifaces = ctx.net_controller.net_iface.watcher.subscribe(); let existing = ctx
let mut iface = GatewayId::from(InternedString::intern("wg0"));
if !ifaces.send_if_modified(|i| {
for id in 1..256 {
if !i.contains_key(&iface) {
i.insert(
iface.clone(),
NetworkInterfaceInfo {
name: Some(name),
public: Some(public),
secure: None,
ip_info: None,
},
);
return true;
}
iface = InternedString::from_display(&lazy_format!("wg{id}")).into();
}
false
}) {
return Err(Error::new(
eyre!("too many wireguard interfaces"),
ErrorKind::InvalidRequest,
));
}
let mut sub = ctx
.db .db
.subscribe( .peek()
"/public/serverInfo/network/gateways" .await
.parse::<JsonPointer>() .into_public()
.with_kind(ErrorKind::Database)? .into_server_info()
.join_end(iface.as_str()) .into_network()
.join_end("ipInfo"), .into_gateways()
) .keys()?;
.await; let mut iface = GatewayId::from("wg0");
for id in 1.. {
if !existing.contains(&iface) {
break;
}
iface = InternedString::from_display(&lazy_format!("wg{id}")).into();
}
let tmpdir = TmpDir::new().await?; let tmpdir = TmpDir::new().await?;
let conf = tmpdir.join(&iface).with_extension("conf"); let conf = tmpdir.join(&iface).with_extension("conf");
write_file_atomic(&conf, &config).await?; write_file_atomic(&conf, &config).await?;
let mut ifaces = ctx.net_controller.net_iface.watcher.subscribe();
Command::new("nmcli") Command::new("nmcli")
.arg("connection") .arg("connection")
.arg("import") .arg("import")
@@ -97,7 +77,14 @@ pub async fn add_tunnel(
.await?; .await?;
tmpdir.delete().await?; tmpdir.delete().await?;
sub.recv().await; ifaces.wait_for(|ifaces| ifaces.contains_key(&iface)).await;
ctx.net_controller
.net_iface
.set_public(&iface, Some(public))
.await?;
ctx.net_controller.net_iface.set_name(&iface, &name).await?;
Ok(iface) Ok(iface)
} }

View File

@@ -258,7 +258,6 @@ impl<A: Accept + Send + Sync + 'static> WebServer<A> {
.await .await
{ {
err = Some(e); err = Some(e);
tokio::time::sleep(Duration::from_millis(100)).await;
} else { } else {
break; break;
} }

View File

@@ -8,7 +8,7 @@ use models::{ImageId, VolumeId};
use tokio::io::{AsyncRead, AsyncSeek, AsyncWriteExt}; use tokio::io::{AsyncRead, AsyncSeek, AsyncWriteExt};
use tokio::process::Command; use tokio::process::Command;
use crate::dependencies::{DepInfo, Dependencies, MetadataSrc}; use crate::dependencies::{DepInfo, Dependencies};
use crate::prelude::*; use crate::prelude::*;
use crate::s9pk::manifest::{DeviceFilter, Manifest}; use crate::s9pk::manifest::{DeviceFilter, Manifest};
use crate::s9pk::merkle_archive::directory_contents::DirectoryContents; use crate::s9pk::merkle_archive::directory_contents::DirectoryContents;
@@ -16,10 +16,10 @@ use crate::s9pk::merkle_archive::source::TmpSource;
use crate::s9pk::merkle_archive::{Entry, MerkleArchive}; use crate::s9pk::merkle_archive::{Entry, MerkleArchive};
use crate::s9pk::v1::manifest::{Manifest as ManifestV1, PackageProcedure}; use crate::s9pk::v1::manifest::{Manifest as ManifestV1, PackageProcedure};
use crate::s9pk::v1::reader::S9pkReader; use crate::s9pk::v1::reader::S9pkReader;
use crate::s9pk::v2::pack::{ImageSource, PackSource, CONTAINER_TOOL}; use crate::s9pk::v2::pack::{CONTAINER_TOOL, ImageSource, PackSource};
use crate::s9pk::v2::{S9pk, SIG_CONTEXT}; use crate::s9pk::v2::{S9pk, SIG_CONTEXT};
use crate::util::io::{create_file, TmpDir};
use crate::util::Invoke; use crate::util::Invoke;
use crate::util::io::{TmpDir, create_file};
pub const MAGIC_AND_VERSION: &[u8] = &[0x3b, 0x3b, 0x01]; pub const MAGIC_AND_VERSION: &[u8] = &[0x3b, 0x3b, 0x01];
@@ -225,7 +225,7 @@ impl TryFrom<ManifestV1> for Manifest {
DepInfo { DepInfo {
description: value.description, description: value.description,
optional: !value.requirement.required(), optional: !value.requirement.required(),
metadata: None, s9pk: None,
}, },
) )
}) })

View File

@@ -3,10 +3,10 @@ use std::path::{Path, PathBuf};
use std::sync::Arc; use std::sync::Arc;
use clap::Parser; use clap::Parser;
use futures::future::{ready, BoxFuture}; use futures::future::{BoxFuture, ready};
use futures::{FutureExt, TryStreamExt}; use futures::{FutureExt, TryStreamExt};
use imbl_value::InternedString; use imbl_value::InternedString;
use models::{DataUrl, ImageId, PackageId, VersionString}; use models::{ImageId, PackageId, VersionString};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::process::Command; use tokio::process::Command;
use tokio::sync::OnceCell; use tokio::sync::OnceCell;
@@ -15,23 +15,23 @@ use tracing::{debug, warn};
use ts_rs::TS; use ts_rs::TS;
use crate::context::CliContext; use crate::context::CliContext;
use crate::dependencies::{DependencyMetadata, MetadataSrc}; use crate::dependencies::DependencyMetadata;
use crate::prelude::*; use crate::prelude::*;
use crate::rpc_continuations::Guid; use crate::rpc_continuations::Guid;
use crate::s9pk::S9pk;
use crate::s9pk::git_hash::GitHash; use crate::s9pk::git_hash::GitHash;
use crate::s9pk::manifest::Manifest; use crate::s9pk::manifest::Manifest;
use crate::s9pk::merkle_archive::directory_contents::DirectoryContents; use crate::s9pk::merkle_archive::directory_contents::DirectoryContents;
use crate::s9pk::merkle_archive::source::http::HttpSource; use crate::s9pk::merkle_archive::source::http::HttpSource;
use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile;
use crate::s9pk::merkle_archive::source::{ use crate::s9pk::merkle_archive::source::{
into_dyn_read, ArchiveSource, DynFileSource, DynRead, FileSource, TmpSource, ArchiveSource, DynFileSource, DynRead, FileSource, TmpSource, into_dyn_read,
}; };
use crate::s9pk::merkle_archive::{Entry, MerkleArchive}; use crate::s9pk::merkle_archive::{Entry, MerkleArchive};
use crate::s9pk::v2::SIG_CONTEXT; use crate::s9pk::v2::SIG_CONTEXT;
use crate::s9pk::S9pk; use crate::util::io::{TmpDir, create_file, open_file};
use crate::util::io::{create_file, open_file, TmpDir};
use crate::util::serde::IoFormat; use crate::util::serde::IoFormat;
use crate::util::{new_guid, Invoke, PathOrUrl}; use crate::util::{Invoke, PathOrUrl, new_guid};
#[cfg(not(feature = "docker"))] #[cfg(not(feature = "docker"))]
pub const CONTAINER_TOOL: &str = "podman"; pub const CONTAINER_TOOL: &str = "podman";
@@ -369,10 +369,12 @@ impl ImageSource {
workdir, workdir,
.. ..
} => { } => {
vec![workdir vec![
.as_deref() workdir
.unwrap_or(Path::new(".")) .as_deref()
.join(dockerfile.as_deref().unwrap_or(Path::new("Dockerfile")))] .unwrap_or(Path::new("."))
.join(dockerfile.as_deref().unwrap_or(Path::new("Dockerfile"))),
]
} }
Self::DockerTag(_) => Vec::new(), Self::DockerTag(_) => Vec::new(),
} }
@@ -697,77 +699,53 @@ pub async fn pack(ctx: CliContext, params: PackParams) -> Result<(), Error> {
let mut to_insert = Vec::new(); let mut to_insert = Vec::new();
for (id, dependency) in &mut s9pk.as_manifest_mut().dependencies.0 { for (id, dependency) in &mut s9pk.as_manifest_mut().dependencies.0 {
if let Some((title, icon)) = match dependency.metadata.take() { if let Some(s9pk) = dependency.s9pk.take() {
Some(MetadataSrc::Metadata(metadata)) => { let s9pk = match s9pk {
let icon = match metadata.icon { PathOrUrl::Path(path) => {
PathOrUrl::Path(path) => DataUrl::from_path(path).await?, S9pk::deserialize(&MultiCursorFile::from(open_file(path).await?), None)
PathOrUrl::Url(url) => { .await?
if url.scheme() == "http" || url.scheme() == "https" { .into_dyn()
DataUrl::from_response(ctx.client.get(url).send().await?).await? }
} else if url.scheme() == "data" { PathOrUrl::Url(url) => {
url.as_str().parse()? if url.scheme() == "http" || url.scheme() == "https" {
} else { S9pk::deserialize(
return Err(Error::new( &Arc::new(HttpSource::new(ctx.client.clone(), url).await?),
eyre!("unknown scheme: {}", url.scheme()), None,
ErrorKind::InvalidRequest, )
)); .await?
} .into_dyn()
} else {
return Err(Error::new(
eyre!("unknown scheme: {}", url.scheme()),
ErrorKind::InvalidRequest,
));
} }
}; }
Some((metadata.title, icon)) };
}
Some(MetadataSrc::S9pk(Some(s9pk))) => {
let s9pk = match s9pk {
PathOrUrl::Path(path) => {
S9pk::deserialize(&MultiCursorFile::from(open_file(path).await?), None)
.await?
.into_dyn()
}
PathOrUrl::Url(url) => {
if url.scheme() == "http" || url.scheme() == "https" {
S9pk::deserialize(
&Arc::new(HttpSource::new(ctx.client.clone(), url).await?),
None,
)
.await?
.into_dyn()
} else {
return Err(Error::new(
eyre!("unknown scheme: {}", url.scheme()),
ErrorKind::InvalidRequest,
));
}
}
};
Some((
s9pk.as_manifest().title.clone(),
s9pk.icon_data_url().await?,
))
}
Some(MetadataSrc::S9pk(None)) | None => {
warn!("no metadata specified for {id}, leaving metadata empty");
None
}
} {
let dep_path = Path::new("dependencies").join(id); let dep_path = Path::new("dependencies").join(id);
to_insert.push(( to_insert.push((
dep_path.join("metadata.json"), dep_path.join("metadata.json"),
Entry::file(TmpSource::new( Entry::file(TmpSource::new(
tmp_dir.clone(), tmp_dir.clone(),
PackSource::Buffered( PackSource::Buffered(
IoFormat::Json.to_vec(&DependencyMetadata { title })?.into(), IoFormat::Json
.to_vec(&DependencyMetadata {
title: s9pk.as_manifest().title.clone(),
})?
.into(),
), ),
)), )),
)); ));
let icon = s9pk.icon().await?;
to_insert.push(( to_insert.push((
dep_path dep_path.join(&*icon.0),
.join("icon")
.with_extension(icon.canonical_ext().unwrap_or("ico")),
Entry::file(TmpSource::new( Entry::file(TmpSource::new(
tmp_dir.clone(), tmp_dir.clone(),
PackSource::Buffered(icon.data.into_owned().into()), PackSource::Buffered(icon.1.expect_file()?.to_vec(icon.1.hash()).await?.into()),
)), )),
)); ));
} else {
warn!("no s9pk specified for {id}, leaving metadata empty");
} }
} }
for (path, source) in to_insert { for (path, source) in to_insert {
@@ -819,17 +797,8 @@ pub async fn list_ingredients(_: CliContext, params: PackParams) -> Result<Vec<P
let mut ingredients = vec![js_path, params.icon().await?, params.license().await?]; let mut ingredients = vec![js_path, params.icon().await?, params.license().await?];
for (_, dependency) in manifest.dependencies.0 { for (_, dependency) in manifest.dependencies.0 {
match dependency.metadata { if let Some(PathOrUrl::Path(p)) = dependency.s9pk {
Some(MetadataSrc::Metadata(crate::dependencies::Metadata { ingredients.push(p);
icon: PathOrUrl::Path(icon),
..
})) => {
ingredients.push(icon);
}
Some(MetadataSrc::S9pk(Some(PathOrUrl::Path(s9pk)))) => {
ingredients.push(s9pk);
}
_ => (),
} }
} }

View File

@@ -6,7 +6,7 @@ use std::time::{Duration, SystemTime};
use clap::Parser; use clap::Parser;
use futures::future::join_all; use futures::future::join_all;
use helpers::NonDetachingJoinHandle; use helpers::NonDetachingJoinHandle;
use imbl::{vector, Vector}; use imbl::{Vector, vector};
use imbl_value::InternedString; use imbl_value::InternedString;
use models::{HostId, PackageId, ServiceInterfaceId}; use models::{HostId, PackageId, ServiceInterfaceId};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -264,6 +264,7 @@ impl CallbackHandler {
} }
} }
pub async fn call(mut self, args: Vector<Value>) -> Result<(), Error> { pub async fn call(mut self, args: Vector<Value>) -> Result<(), Error> {
crate::dbg!(eyre!("callback fired: {}", self.handle.is_active()));
if let Some(seed) = self.seed.upgrade() { if let Some(seed) = self.seed.upgrade() {
seed.persistent_container seed.persistent_container
.callback(self.handle.take(), args) .callback(self.handle.take(), args)

View File

@@ -4,7 +4,7 @@ use models::PackageId;
use crate::context::RpcContext; use crate::context::RpcContext;
use crate::prelude::*; use crate::prelude::*;
use crate::volume::PKG_VOLUME_DIR; use crate::volume::data_dir;
use crate::{DATA_DIR, PACKAGE_DATA}; use crate::{DATA_DIR, PACKAGE_DATA};
pub async fn cleanup(ctx: &RpcContext, id: &PackageId, soft: bool) -> Result<(), Error> { pub async fn cleanup(ctx: &RpcContext, id: &PackageId, soft: bool) -> Result<(), Error> {
@@ -45,11 +45,11 @@ pub async fn cleanup(ctx: &RpcContext, id: &PackageId, soft: bool) -> Result<(),
{ {
let state = pde.state_info.expect_removing()?; let state = pde.state_info.expect_removing()?;
if !soft { if !soft {
let path = Path::new(DATA_DIR) for volume_id in &state.manifest.volumes {
.join(PKG_VOLUME_DIR) let path = data_dir(DATA_DIR, &state.manifest.id, volume_id);
.join(&state.manifest.id); if tokio::fs::metadata(&path).await.is_ok() {
if tokio::fs::metadata(&path).await.is_ok() { tokio::fs::remove_dir_all(&path).await?;
tokio::fs::remove_dir_all(&path).await?; }
} }
let logs_dir = Path::new(PACKAGE_DATA) let logs_dir = Path::new(PACKAGE_DATA)
.join("logs") .join("logs")

View File

@@ -9,7 +9,7 @@ use color_eyre::eyre::eyre;
use futures::{FutureExt, TryStreamExt}; use futures::{FutureExt, TryStreamExt};
use imbl::vector; use imbl::vector;
use imbl_value::InternedString; use imbl_value::InternedString;
use rpc_toolkit::{Context, Empty, HandlerExt, ParentHandler, from_fn_async}; use rpc_toolkit::{from_fn_async, Context, Empty, HandlerExt, ParentHandler};
use rustls::RootCertStore; use rustls::RootCertStore;
use rustls_pki_types::CertificateDer; use rustls_pki_types::CertificateDer;
use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::{Deserialize, Deserializer, Serialize, Serializer};
@@ -24,12 +24,12 @@ use crate::logs::{LogSource, LogsParams, SYSTEM_UNIT};
use crate::prelude::*; use crate::prelude::*;
use crate::rpc_continuations::{Guid, RpcContinuation, RpcContinuations}; use crate::rpc_continuations::{Guid, RpcContinuation, RpcContinuations};
use crate::shutdown::Shutdown; use crate::shutdown::Shutdown;
use crate::util::Invoke; use crate::util::cpupower::{get_available_governors, set_governor, Governor};
use crate::util::cpupower::{Governor, get_available_governors, set_governor};
use crate::util::io::open_file; use crate::util::io::open_file;
use crate::util::net::WebSocketExt; use crate::util::net::WebSocketExt;
use crate::util::serde::{HandlerExtSerde, WithIoFormat, display_serializable}; use crate::util::serde::{display_serializable, HandlerExtSerde, WithIoFormat};
use crate::util::sync::Watch; use crate::util::sync::Watch;
use crate::util::Invoke;
use crate::{MAIN_DATA, PACKAGE_DATA}; use crate::{MAIN_DATA, PACKAGE_DATA};
pub fn experimental<C: Context>() -> ParentHandler<C> { pub fn experimental<C: Context>() -> ParentHandler<C> {
@@ -1024,7 +1024,7 @@ pub struct TestSmtpParams {
#[arg(long)] #[arg(long)]
pub login: String, pub login: String,
#[arg(long)] #[arg(long)]
pub password: Option<String>, pub password: String,
} }
pub async fn test_smtp( pub async fn test_smtp(
_: RpcContext, _: RpcContext,
@@ -1037,74 +1037,23 @@ pub async fn test_smtp(
password, password,
}: TestSmtpParams, }: TestSmtpParams,
) -> Result<(), Error> { ) -> Result<(), Error> {
#[cfg(feature = "mail-send")] use lettre::message::header::ContentType;
{ use lettre::transport::smtp::authentication::Credentials;
use mail_send::SmtpClientBuilder; use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor};
use mail_send::mail_builder::{self, MessageBuilder};
use rustls_pki_types::pem::PemObject;
let Some(pass_val) = password else { AsyncSmtpTransport::<Tokio1Executor>::relay(&server)?
return Err(Error::new( .credentials(Credentials::new(login, password))
eyre!("mail-send requires a password"), .build()
ErrorKind::InvalidRequest, .send(
)); Message::builder()
}; .from(from.parse()?)
.to(to.parse()?)
let mut root_cert_store = RootCertStore::empty(); .subject("StartOS Test Email")
let pem = tokio::fs::read("/etc/ssl/certs/ca-certificates.crt").await?; .header(ContentType::TEXT_PLAIN)
for cert in CertificateDer::pem_slice_iter(&pem) { .body("This is a test email sent from your StartOS Server".to_owned())?,
root_cert_store.add_parsable_certificates([cert.with_kind(ErrorKind::OpenSsl)?]); )
} .await?;
Ok(())
let cfg = Arc::new(
rustls::ClientConfig::builder_with_provider(Arc::new(
rustls::crypto::ring::default_provider(),
))
.with_safe_default_protocol_versions()?
.with_root_certificates(root_cert_store)
.with_no_client_auth(),
);
let client = SmtpClientBuilder::new_with_tls_config(server, port, cfg)
.implicit_tls(false)
.credentials((login.split("@").next().unwrap().to_owned(), pass_val));
fn parse_address<'a>(addr: &'a str) -> mail_builder::headers::address::Address<'a> {
if addr.find("<").map_or(false, |start| {
addr.find(">").map_or(false, |end| start < end)
}) {
addr.split_once("<")
.map(|(name, addr)| (name.trim(), addr.strip_suffix(">").unwrap_or(addr)))
.unwrap()
.into()
} else {
addr.into()
}
}
let message = MessageBuilder::new()
.from(parse_address(&from))
.to(parse_address(&to))
.subject("StartOS Test Email")
.text_body("This is a test email sent from your StartOS Server");
client
.connect()
.await
.map_err(|e| {
Error::new(
eyre!("mail-send connection error: {:?}", e),
ErrorKind::Unknown,
)
})?
.send(message)
.await
.map_err(|e| Error::new(eyre!("mail-send send error: {:?}", e), ErrorKind::Unknown))?;
Ok(())
}
#[cfg(not(feature = "mail-send"))]
Err(Error::new(
eyre!("test-smtp requires mail-send feature to be enabled"),
ErrorKind::InvalidRequest,
))
} }
#[tokio::test] #[tokio::test]

View File

@@ -173,18 +173,18 @@ impl<T: Eq> EqSet<T> {
/// Retains only the elements specified by the predicate. /// Retains only the elements specified by the predicate.
/// ///
/// In other words, remove all elements `x` for which `f(&x)` returns `false`. /// In other words, remove all pairs `(k, v)` for which `f(&k, &mut v)` returns `false`.
/// The elements are visited in order. /// The elements are visited in ascending value order.
/// ///
/// # Examples /// # Examples
/// ///
/// ``` /// ```
/// use startos::util::collections::EqSet; /// use startos::util::collections::EqSet;
/// ///
/// let mut set: EqSet<i32> = (0..8).collect(); /// let mut set: EqSet<i32, i32> = (0..8).set(|x| (x, x*10)).collect();
/// // Keep only the elements with even-numbered values. /// // Keep only the elements with even-numbered values.
/// set.retain(|x| *x % 2 == 0); /// set.retain(|&k, _| k % 2 == 0);
/// assert!(set.into_iter().eq(vec![0, 2, 4, 6])); /// assert!(set.into_iter().eq(vec![(0, 0), (2, 20), (4, 40), (6, 60)]));
/// ``` /// ```
#[inline] #[inline]
pub fn retain<F>(&mut self, f: F) pub fn retain<F>(&mut self, f: F)
@@ -210,9 +210,9 @@ impl<T: Eq> EqSet<T> {
/// a.insert("c"); // Note: "c" also present in b. /// a.insert("c"); // Note: "c" also present in b.
/// ///
/// let mut b = EqSet::new(); /// let mut b = EqSet::new();
/// b.insert("c"); // Note: "c" also present in a. /// b.insert(3, "c"); // Note: "c" also present in a.
/// b.insert("d"); /// b.insert(4, "d");
/// b.insert("e"); /// b.insert(5, "e");
/// ///
/// a.append(&mut b); /// a.append(&mut b);
/// ///
@@ -246,7 +246,7 @@ impl<T: Eq> EqSet<T> {
// /// ``` // /// ```
// /// use startos::util::collections::EqSet; // /// use startos::util::collections::EqSet;
// /// // ///
// /// let mut set: EqSet<(i32, i32)> = (0..8).map(|x| (x, x)).collect(); // /// let mut set: EqSet<i32, i32> = (0..8).set(|x| (x, x)).collect();
// /// let evens: EqSet<_, _> = set.extract_if(|k, _v| k % 2 == 0).collect(); // /// let evens: EqSet<_, _> = set.extract_if(|k, _v| k % 2 == 0).collect();
// /// let odds = set; // /// let odds = set;
// /// assert_eq!(evens.values().copied().collect::<Vec<_>>(), [0, 2, 4, 6]); // /// assert_eq!(evens.values().copied().collect::<Vec<_>>(), [0, 2, 4, 6]);
@@ -366,7 +366,7 @@ impl<T: Eq, const N: usize> From<[T; N]> for EqSet<T> {
/// use startos::util::collections::EqSet; /// use startos::util::collections::EqSet;
/// ///
/// let set1 = EqSet::from([(1, 2), (3, 4)]); /// let set1 = EqSet::from([(1, 2), (3, 4)]);
/// let set2: EqSet<_> = [(1, 2), (3, 4)].into(); /// let set2: EqSet<_, _> = [(1, 2), (3, 4)].into();
/// assert_eq!(set1, set2); /// assert_eq!(set1, set2);
/// ``` /// ```
fn from(arr: [T; N]) -> Self { fn from(arr: [T; N]) -> Self {

View File

@@ -16,7 +16,25 @@ v0.4.0 is a complete rewrite of StartOS, almost nothing survived. After nearly s
## Changelog ## Changelog
### Improved User interface - [Improve user interface](#user-interface)
- [Add translations](#translations)
- [Switch to lxc-based container runtime](#lxc)
- [Update s9pk archive format](#s9pk-archive-format)
- [Improve Actions](#actions)
- [Use squashfs images for OS updates](#squashfs-updates)
- [Introduce Typescript package API and SDK](#typescript-sdk)
- [Remove Postgresql](#remove-postgressql)
- [Enable sending emails via SMTP](#smtp)
- [Support SSH password auth](#ssh-password-auth)
- [Allow managing Tor addresses](#tor-addresses)
- [Implement detailed progress reporting](#progress-reporting)
- [Improve registry protocol](#registry-protocol)
- [Replace unique .local URLs with unique ports](#lan-port-forwarding)
- [Use start-fs Fuse module for improved backups](#improved-backups)
- [Switch to Exver for versioning](#exver)
- [Add clearnet hosting](#clearnet)
### User interface
We re-wrote the StartOS UI to be more performant, more intuitive, and better looking on both mobile and desktop. Enjoy. We re-wrote the StartOS UI to be more performant, more intuitive, and better looking on both mobile and desktop. Enjoy.
@@ -24,31 +42,31 @@ We re-wrote the StartOS UI to be more performant, more intuitive, and better loo
StartOS v0.4.0 supports multiple languages and also makes it easy to add more later on. StartOS v0.4.0 supports multiple languages and also makes it easy to add more later on.
### LXC Container Runtime ### LXC
Neither Docker nor Podman offer the reliability and flexibility needed for StartOS. Instead, v0.4.0 uses a nested container paradigm based on LXC for the outer container and Linux namespaces for sub containers. This architecture naturally supports multi container setups. Replacing both Docker and Podman, StartOS v0.4.0 uses a nested container paradigm based on LXC for the outer container and linux namespaces for sub containers. This architecture naturally support multi container setups.
### New S9PK archive format ### S9PK archive format
The S9PK archive format has been overhauled to allow for signature verification of partial downloads, and allow direct mounting of container images without unpacking the s9pk. The S9PK archive format has been overhauled to allow for signature verification of partial downloads, and allow direct mounting of container images without unpacking the s9pk.
### Improved Actions ### Actions
Actions take arbitrary form input and return arbitrary responses, thus satisfying the needs of both "Config" and "Properties", which have now been removed. The new actions API gives package developers the ability to break up Config and Properties into smaller, more specific formats, or to exclude them entirely without polluting the UI. Improved form design and new input types round out the new actions experience. Actions take arbitrary form input and return arbitrary responses, thus satisfying the needs of both Config and Properties, which have been removed. The new actions API gives packages developers the ability to break up Config and Properties into smaller, more specific formats, or to exclude them entirely without polluting the UI. Improved form design and new input types round out the actions experience.
### Squashfs Images for OS Updates ### Squashfs updates
StartOS now uses squashfs images instead of rsync for OS updates. This allows for better update verification and improved reliability. StartOS now uses squashfs images to represent OS updates. This allows for better update verification, and improved reliability over rsync updates.
### Typescript Package API and SDK ### Typescript SDK
Package developers can now take advantage of StartOS APIs using the new start-sdk, available in Typescript. A barebones StartOS package (s9pk) can be produced in minutes with minimal knowledge or skill. More advanced developers can use the SDK to create highly customized user experiences for their service. Package developers can now take advantage of StartOS APIs using the new start-sdk, available in Typescript. A barebones StartOS package (s9pk) can be produced in minutes with minimal knowledge or skill. More advanced developers can use the SDK to create highly customized user experiences with their service.
### Removed PostgresSQL ### Remove PostgresSQL
StartOS itself has miniscule data persistence needs. PostgresSQL was overkill and has been removed in favor of lightweight PatchDB. StartOS itself has miniscule data persistence needs. PostgresSQL was overkill and has been removed in favor of lightweight PatchDB.
### Sending Emails via SMTP ### SMTP
You can now add your Gmail, SES, or other SMTP credentials to StartOS in order to send deliver email notifications from StartOS and from installed services that support SMTP. You can now add your Gmail, SES, or other SMTP credentials to StartOS in order to send deliver email notifications from StartOS and from installed services that support SMTP.
@@ -56,17 +74,17 @@ You can now add your Gmail, SES, or other SMTP credentials to StartOS in order t
You can now SSH into your server using your master password. SSH public key authentication is still supported as well. You can now SSH into your server using your master password. SSH public key authentication is still supported as well.
### Tor Address Management ### Tor addresses
StartOS v0.4.0 supports adding and removing Tor addresses for StartOS and all service interfaces. You can even provide your own private key instead of using one auto-generated by StartOS. This has the added benefit of permitting vanity addresses. StartOS v0.4.0 supports adding and removing Tor addresses for StartOS and all service interfaces. You can even provide your own private key instead of using one auto-generated by StartOS. This has the added benefit of permitting vanity addresses.
### Progress Reporting ### Progress reporting
A new progress reporting API enabled package developers to create unique phases and provide real-time progress reporting for actions such as installing, updating, or backing up a service. A new progress reporting API enabled package developers to create unique phases and provide real-time progress reporting for actions such as installing, updating, or backing up a service.
### Registry Protocol ### Registry protocol
The new registry protocol bifurcates package indexing (listing/validating) and package hosting (downloading). Registries are now simple indexes of packages that reference binaries hosted in arbitrary locations, locally or externally. For example, when someone visits the Start9 Registry, the curated list of packages comes from Start9. But when someone installs a listed service, the package binary is being downloaded from Github. The registry also validates the binary. This makes it much easier to host a custom registry, since it is just a curated list of services tat reference package binaries hosted on Github or elsewhere. The new registry protocol bifurcates package indexing (listing/validating) and package hosting (downloading). Registries are now simple indexes of packages that reference binaries hosted in arbitrary locations, locally or externally. For example, when someone visits the Start9 Registry, the currated list of packages comes from Start9. But when someone installs a listed service, the package binary is being downloaded from Github. The registry also valides the binary. This makes it much easier to host a custom registry, since it is just a currated list of services tat reference package binaries hosted on Github or elsewhere.
### LAN port forwarding ### LAN port forwarding
@@ -78,34 +96,12 @@ The new start-fs fuse module unifies file system expectations for various platfo
### Exver ### Exver
StartOS now uses Extended Versioning (Exver), which consists of three parts: (1) a Semver-compliant upstream version, (2) a Semver-compliant wrapper version, and (3) an optional "flavor" prefix. Flavors can be thought of as alternative implementations of services, where a user would only want one or the other installed, and data can feasibly be migrating between the two. Another common characteristic of flavors is that they satisfy the same API requirement of dependents, though this is not strictly necessary. A valid Exver looks something like this: `#knots:29.0:1.0-beta.1`. This would translate to "the first beta release of StartOS wrapper version 1.0 of Bitcoin Knots version 29.0". StartOS now uses Extended Versioning (Exver), which consists of three parts: (1) a Semver-compliant upstream version, (2) a Semver-compliant wrapper version, and (3) an optional "flavor" prefix. Flavors can be thought of as alternative implementations of services, where a user would only want one or the other installed, and data can feasibly be migrating between the two. Another common characteristic of flavors is that they satisfy the same API requirement of dependents, though this is not strictly necessary. A valid Exver looks something like this: `#knots:28.0.:1.0-beta.1`. This would translate to "the first beta release of StartOS wrapper version 1.0 of Bitcoin Knots version 27.0".
### ACME ### Clearnet
StartOS now supports using ACME protocol to automatically obtain SSL/TLS certificates from widely trusted certificate authorities, such as Let's Encrypt, for your public domains. This means people visiting your public websites and APIs will not need to download and trust your server's Root CA. It is now possible (and easy) to expose service interfaces to the public Internet on standard domains. There are two options, both of which are easy to accomplish:
### Gateways 1. Open ports on your router. This option is free and supported by all routers. The drawback is that your home IP address is revealed to anyone accessing an exposed interface. For example, hosting a blog in this way would reveal your home IP address, and therefore your approximate location, to readers.
Gateways connect your server to the Internet. They process outbound traffic, and under certain conditions, they also permit inbound traffic. For example, your router is a gateway. It is now possible add gateways to StartOS, such as StartTunnel, in order to more granularly control how your installed services are exposed to the Internet. 2. Use a Wireguard VPN to proxy web traffic. This option requires provisioning a $5-$10/month VPS and running a one-line script. The result is the successful obfuscation of the users home IP address.
### Static DNS Servers
By default, StartOS uses the DNS servers it receives via DHCP from its gateway(s). It is now possible to override these DNS servers with custom, static ones.
### Internal DNS Server
StartOS runs its own DNS server and automatically adds records for your private domains. You can update your router or other gateway to use StartOS DNS server in order to resolve these domains locally.
### Private Domains
A private domain is like to your server's .local, except it also works for VPN connectivity, and it can be _anything_. It can be a real domain you control, a made up domain, or even a domain controlled by someone else.
Similar to your local domain, private domains can only be accessed when connected to the same LAN as your server, either physically or via VPN, and they require trusting your server's Root CA.
### Public Domains (Clearnet)
It is now easy to expose service interfaces to the public Internet on a public domain you control. There are two options, both of which are easy to accomplish:
1. Open ports on your router. This option is free and supported by all routers. The drawback is that your home IP address is revealed to anyone accessing an exposed interface.
2. Use a Wireguard reverse tunnel, such as [StartTunnel](#start-tunnel) to proxy web traffic. This option requires renting a $5-$10/month VPS and installing StartTunnel (or similar). The result is a new gateway, a virtual router in the cloud, that you can use to expose service interfaces instead of your real router, thereby hiding your IP address from visitors.

View File

@@ -6,7 +6,7 @@ use models::GatewayId;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::v0_3_5::V0_3_0_COMPAT; use super::v0_3_5::V0_3_0_COMPAT;
use super::{v0_3_6_alpha_9, VersionT}; use super::{VersionT, v0_3_6_alpha_9};
use crate::net::host::address::PublicDomainConfig; use crate::net::host::address::PublicDomainConfig;
use crate::net::tor::OnionAddress; use crate::net::tor::OnionAddress;
use crate::prelude::*; use crate::prelude::*;
@@ -75,7 +75,7 @@ impl VersionT for Version {
domains.insert( domains.insert(
address.clone(), address.clone(),
PublicDomainConfig { PublicDomainConfig {
gateway: GatewayId::from(InternedString::intern("lo")), gateway: GatewayId::from("lo"),
acme: None, acme: None,
}, },
); );

View File

@@ -67,7 +67,6 @@ impl VersionT for Version {
private.insert(domain.clone()); private.insert(domain.clone());
} }
} }
host["hostnameInfo"] = json!({});
host["publicDomains"] = to_value(&public)?; host["publicDomains"] = to_value(&public)?;
host["privateDomains"] = to_value(&private)?; host["privateDomains"] = to_value(&private)?;
Ok::<_, Error>(()) Ok::<_, Error>(())
@@ -97,7 +96,7 @@ impl VersionT for Version {
} }
fix_host(&mut db["public"]["serverInfo"]["network"]["host"])?; fix_host(&mut db["public"]["serverInfo"]["network"]["host"])?;
let network = &mut db["public"]["serverInfo"]["network"]; let network = &mut db["public"]["serverInfo"]["network"];
network["gateways"] = json!({}); network["gateways"] = network["networkInterfaces"].clone();
network["dns"] = json!({ network["dns"] = json!({
"dhcpServers": [], "dhcpServers": [],
}); });

View File

@@ -1,7 +1,8 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { MetadataSrc } from "./MetadataSrc" import type { PathOrUrl } from "./PathOrUrl"
export type DepInfo = { export type DepInfo = {
description: string | null description: string | null
optional: boolean optional: boolean
} & MetadataSrc s9pk: PathOrUrl | null
}

View File

@@ -1,4 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { GatewayId } from "./GatewayId" import type { GatewayId } from "./GatewayId"
export type UnsetPublicParams = { gateway: GatewayId } export type ForgetInterfaceParams = { interface: GatewayId }

View File

@@ -1,8 +1,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { GatewayInfo } from "./GatewayInfo"
import type { IpHostname } from "./IpHostname" import type { IpHostname } from "./IpHostname"
import type { OnionHostname } from "./OnionHostname" import type { OnionHostname } from "./OnionHostname"
export type HostnameInfo = export type HostnameInfo =
| { kind: "ip"; gateway: GatewayInfo; public: boolean; hostname: IpHostname } | { kind: "ip"; gatewayId: string; public: boolean; hostname: IpHostname }
| { kind: "onion"; hostname: OnionHostname } | { kind: "onion"; hostname: OnionHostname }

View File

@@ -1,4 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { PathOrUrl } from "./PathOrUrl"
export type Metadata = { title: string; icon: PathOrUrl }

View File

@@ -1,5 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Metadata } from "./Metadata"
import type { PathOrUrl } from "./PathOrUrl"
export type MetadataSrc = { metadata: Metadata } | { s9pk: PathOrUrl | null }

View File

@@ -2,7 +2,6 @@
import type { IpInfo } from "./IpInfo" import type { IpInfo } from "./IpInfo"
export type NetworkInterfaceInfo = { export type NetworkInterfaceInfo = {
name: string | null
public: boolean | null public: boolean | null
secure: boolean | null secure: boolean | null
ipInfo: IpInfo | null ipInfo: IpInfo | null

View File

@@ -2,6 +2,6 @@
import type { GatewayId } from "./GatewayId" import type { GatewayId } from "./GatewayId"
export type NetworkInterfaceSetPublicParams = { export type NetworkInterfaceSetPublicParams = {
gateway: GatewayId interface: GatewayId
public: boolean | null public: boolean | null
} }

View File

@@ -1,4 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { GatewayId } from "./GatewayId"
export type RenameGatewayParams = { id: GatewayId; name: string }

View File

@@ -1,4 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { GatewayId } from "./GatewayId" import type { GatewayId } from "./GatewayId"
export type GatewayInfo = { id: GatewayId; name: string; public: boolean } export type RenameInterfaceParams = { interface: GatewayId; name: string }

View File

@@ -1,4 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { GatewayId } from "./GatewayId" import type { GatewayId } from "./GatewayId"
export type ForgetGatewayParams = { gateway: GatewayId } export type UnsetInboundParams = { interface: GatewayId }

View File

@@ -76,11 +76,10 @@ export { EventId } from "./EventId"
export { ExportActionParams } from "./ExportActionParams" export { ExportActionParams } from "./ExportActionParams"
export { ExportServiceInterfaceParams } from "./ExportServiceInterfaceParams" export { ExportServiceInterfaceParams } from "./ExportServiceInterfaceParams"
export { FileType } from "./FileType" export { FileType } from "./FileType"
export { ForgetGatewayParams } from "./ForgetGatewayParams" export { ForgetInterfaceParams } from "./ForgetInterfaceParams"
export { FullIndex } from "./FullIndex" export { FullIndex } from "./FullIndex"
export { FullProgress } from "./FullProgress" export { FullProgress } from "./FullProgress"
export { GatewayId } from "./GatewayId" export { GatewayId } from "./GatewayId"
export { GatewayInfo } from "./GatewayInfo"
export { GetActionInputParams } from "./GetActionInputParams" export { GetActionInputParams } from "./GetActionInputParams"
export { GetContainerIpParams } from "./GetContainerIpParams" export { GetContainerIpParams } from "./GetContainerIpParams"
export { GetHostInfoParams } from "./GetHostInfoParams" export { GetHostInfoParams } from "./GetHostInfoParams"
@@ -129,8 +128,6 @@ export { Manifest } from "./Manifest"
export { MaybeUtf8String } from "./MaybeUtf8String" export { MaybeUtf8String } from "./MaybeUtf8String"
export { MebiBytes } from "./MebiBytes" export { MebiBytes } from "./MebiBytes"
export { MerkleArchiveCommitment } from "./MerkleArchiveCommitment" export { MerkleArchiveCommitment } from "./MerkleArchiveCommitment"
export { MetadataSrc } from "./MetadataSrc"
export { Metadata } from "./Metadata"
export { MetricsCpu } from "./MetricsCpu" export { MetricsCpu } from "./MetricsCpu"
export { MetricsDisk } from "./MetricsDisk" export { MetricsDisk } from "./MetricsDisk"
export { MetricsGeneral } from "./MetricsGeneral" export { MetricsGeneral } from "./MetricsGeneral"
@@ -175,7 +172,7 @@ export { RemovePackageFromCategoryParams } from "./RemovePackageFromCategoryPara
export { RemovePackageParams } from "./RemovePackageParams" export { RemovePackageParams } from "./RemovePackageParams"
export { RemoveTunnelParams } from "./RemoveTunnelParams" export { RemoveTunnelParams } from "./RemoveTunnelParams"
export { RemoveVersionParams } from "./RemoveVersionParams" export { RemoveVersionParams } from "./RemoveVersionParams"
export { RenameGatewayParams } from "./RenameGatewayParams" export { RenameInterfaceParams } from "./RenameInterfaceParams"
export { ReplayId } from "./ReplayId" export { ReplayId } from "./ReplayId"
export { RequestCommitment } from "./RequestCommitment" export { RequestCommitment } from "./RequestCommitment"
export { RunActionParams } from "./RunActionParams" export { RunActionParams } from "./RunActionParams"
@@ -211,7 +208,7 @@ export { TaskSeverity } from "./TaskSeverity"
export { TaskTrigger } from "./TaskTrigger" export { TaskTrigger } from "./TaskTrigger"
export { Task } from "./Task" export { Task } from "./Task"
export { TestSmtpParams } from "./TestSmtpParams" export { TestSmtpParams } from "./TestSmtpParams"
export { UnsetPublicParams } from "./UnsetPublicParams" export { UnsetInboundParams } from "./UnsetInboundParams"
export { UpdatingState } from "./UpdatingState" export { UpdatingState } from "./UpdatingState"
export { VerifyCifsParams } from "./VerifyCifsParams" export { VerifyCifsParams } from "./VerifyCifsParams"
export { VersionSignerParams } from "./VersionSignerParams" export { VersionSignerParams } from "./VersionSignerParams"

View File

@@ -269,7 +269,7 @@ export class FileHelper<A> {
eq: (left: B | null | undefined, right: B | null) => boolean, eq: (left: B | null | undefined, right: B | null) => boolean,
abort?: AbortSignal, abort?: AbortSignal,
) { ) {
let prev: { value: B | null } | null = null let res
while (effects.isInContext && !abort?.aborted) { while (effects.isInContext && !abort?.aborted) {
if (await exists(this.path)) { if (await exists(this.path)) {
const ctrl = new AbortController() const ctrl = new AbortController()
@@ -287,10 +287,8 @@ export class FileHelper<A> {
} }
}) })
.catch((e) => console.error(asError(e))) .catch((e) => console.error(asError(e)))
if (!prev || !eq(prev.value, newRes)) { if (!eq(res, newRes)) yield newRes
yield newRes res = newRes
}
prev = { value: newRes }
await listen await listen
} else { } else {
yield null yield null

View File

@@ -1,12 +1,12 @@
{ {
"name": "@start9labs/start-sdk", "name": "@start9labs/start-sdk",
"version": "0.4.0-beta.38", "version": "0.4.0-beta.36",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@start9labs/start-sdk", "name": "@start9labs/start-sdk",
"version": "0.4.0-beta.38", "version": "0.4.0-beta.36",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@iarna/toml": "^3.0.0", "@iarna/toml": "^3.0.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@start9labs/start-sdk", "name": "@start9labs/start-sdk",
"version": "0.4.0-beta.38", "version": "0.4.0-beta.36",
"description": "Software development kit to facilitate packaging services for StartOS", "description": "Software development kit to facilitate packaging services for StartOS",
"main": "./package/lib/index.js", "main": "./package/lib/index.js",
"types": "./package/lib/index.d.ts", "types": "./package/lib/index.d.ts",

5842
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -46,18 +46,18 @@
"@noble/hashes": "^1.4.0", "@noble/hashes": "^1.4.0",
"@start9labs/argon2": "^0.3.0", "@start9labs/argon2": "^0.3.0",
"@start9labs/start-sdk": "file:../sdk/baseDist", "@start9labs/start-sdk": "file:../sdk/baseDist",
"@taiga-ui/addon-charts": "4.52.0", "@taiga-ui/addon-charts": "4.51.0",
"@taiga-ui/addon-commerce": "4.52.0", "@taiga-ui/addon-commerce": "4.51.0",
"@taiga-ui/addon-mobile": "4.52.0", "@taiga-ui/addon-mobile": "4.51.0",
"@taiga-ui/addon-table": "4.52.0", "@taiga-ui/addon-table": "4.51.0",
"@taiga-ui/cdk": "4.52.0", "@taiga-ui/cdk": "4.51.0",
"@taiga-ui/core": "4.52.0", "@taiga-ui/core": "4.51.0",
"@taiga-ui/dompurify": "4.1.11", "@taiga-ui/dompurify": "4.1.11",
"@taiga-ui/event-plugins": "4.7.0", "@taiga-ui/event-plugins": "4.6.0",
"@taiga-ui/experimental": "4.52.0", "@taiga-ui/experimental": "4.51.0",
"@taiga-ui/icons": "4.52.0", "@taiga-ui/icons": "4.51.0",
"@taiga-ui/kit": "4.52.0", "@taiga-ui/kit": "4.51.0",
"@taiga-ui/layout": "4.52.0", "@taiga-ui/layout": "4.51.0",
"@taiga-ui/polymorpheus": "4.9.0", "@taiga-ui/polymorpheus": "4.9.0",
"ansi-to-html": "^0.7.2", "ansi-to-html": "^0.7.2",
"base64-js": "^1.5.1", "base64-js": "^1.5.1",

View File

@@ -16,10 +16,7 @@
Back Back
</button> </button>
} }
<h1>{{ selected ? 'Install Type' : 'StartOS Install' }}</h1> <h1>{{ selected ? 'Install Type' : 'Select Disk' }}</h1>
@if (!selected) {
<h2>Select Disk</h2>
}
<div [style.color]="'var(--tui-text-negative)'">{{ error }}</div> <div [style.color]="'var(--tui-text-negative)'">{{ error }}</div>
</header> </header>
<div class="pages"> <div class="pages">

View File

@@ -34,9 +34,6 @@ main {
text-align: center; text-align: center;
padding-top: 0.25rem; padding-top: 0.25rem;
margin-bottom: -2rem; margin-bottom: -2rem;
h2 {
margin-top: 0;
}
} }
.back { .back {

View File

@@ -50,7 +50,10 @@ export class AppComponent {
private async reboot() { private async reboot() {
this.dialogs this.dialogs
.open('1. Remove the USB stick<br />2. Click "Reboot" below', SUCCESS) .open(
'Remove the USB stick and reboot your device to begin using your new Start9 server',
SUCCESS,
)
.subscribe({ .subscribe({
complete: async () => { complete: async () => {
const loader = this.loader.open().subscribe() const loader = this.loader.open().subscribe()
@@ -59,7 +62,7 @@ export class AppComponent {
await this.api.reboot() await this.api.reboot()
this.dialogs this.dialogs
.open( .open(
'Please wait 1-2 minutes, then refresh this page to access the StartOS setup wizard.', 'Please wait for StartOS to restart, then refresh this page',
{ {
label: 'Rebooting', label: 'Rebooting',
size: 's', size: 's',

View File

@@ -1,8 +1,4 @@
import { import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
provideHttpClient,
withFetch,
withInterceptorsFromDi,
} from '@angular/common/http'
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { BrowserAnimationsModule } from '@angular/platform-browser/animations' import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
import { import {
@@ -54,7 +50,7 @@ const {
provide: RELATIVE_URL, provide: RELATIVE_URL,
useValue: `/${api.url}/${api.version}`, useValue: `/${api.url}/${api.version}`,
}, },
provideHttpClient(withInterceptorsFromDi(), withFetch()), provideHttpClient(withInterceptorsFromDi()),
], ],
bootstrap: [AppComponent], bootstrap: [AppComponent],
}) })

View File

@@ -3,7 +3,7 @@ import { TuiDialogOptions } from '@taiga-ui/core'
import { TuiConfirmData } from '@taiga-ui/kit' import { TuiConfirmData } from '@taiga-ui/kit'
export const SUCCESS: Partial<TuiDialogOptions<any>> = { export const SUCCESS: Partial<TuiDialogOptions<any>> = {
label: 'Install Success!', label: 'Install Success',
closeable: false, closeable: false,
size: 's', size: 's',
data: { button: 'Reboot' }, data: { button: 'Reboot' },

View File

@@ -7,7 +7,7 @@
[url]="registry?.url || ''" [url]="registry?.url || ''"
/> />
<h1 [tuiSkeleton]="!registry"> <h1 [tuiSkeleton]="!registry">
{{ registry?.info?.name || 'Unnamed registry' }} {{ registry?.info?.name || 'Unnamed Registry' }}
</h1> </h1>
<!-- change registry modal --> <!-- change registry modal -->
<ng-content select="[slot=desktop]"></ng-content> <ng-content select="[slot=desktop]"></ng-content>
@@ -62,8 +62,12 @@
<div> <div>
<!-- link to store for brochure --> <!-- link to store for brochure -->
<ng-content select="[slot=store-mobile]" /> <ng-content select="[slot=store-mobile]" />
<a docsLink path="/packaging-guide"> <a
<span>{{ 'Package a service' | i18n }}</span> target="_blank"
rel="noreferrer"
href="https://docs.start9.com/latest/packaging-guide/"
>
<span>Package a service</span>
<tui-icon tuiAppearance="icon" icon="@tui.external-link" /> <tui-icon tuiAppearance="icon" icon="@tui.external-link" />
</a> </a>
</div> </div>
@@ -86,8 +90,12 @@
<div> <div>
<!-- link to store for brochure --> <!-- link to store for brochure -->
<ng-content select="[slot=store]" /> <ng-content select="[slot=store]" />
<a docsLink path="/packaging-guide"> <a
<span>{{ 'Package a service' | i18n }}</span> target="_blank"
rel="noreferrer"
href="https://docs.start9.com/latest/packaging-guide/"
>
<span>Package a service</span>
<tui-icon tuiAppearance="icon" icon="@tui.external-link" /> <tui-icon tuiAppearance="icon" icon="@tui.external-link" />
</a> </a>
</div> </div>

View File

@@ -1,10 +1,6 @@
import { CommonModule } from '@angular/common' import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { import { SharedPipesModule } from '@start9labs/shared'
DocsLinkDirective,
i18nPipe,
SharedPipesModule,
} from '@start9labs/shared'
import { TuiLet } from '@taiga-ui/cdk' import { TuiLet } from '@taiga-ui/cdk'
import { import {
TuiAppearance, TuiAppearance,
@@ -35,8 +31,6 @@ import { MenuComponent } from './menu.component'
TuiSkeleton, TuiSkeleton,
TuiDrawer, TuiDrawer,
TuiPopup, TuiPopup,
i18nPipe,
DocsLinkDirective,
], ],
declarations: [MenuComponent], declarations: [MenuComponent],
exports: [MenuComponent], exports: [MenuComponent],

View File

@@ -6,7 +6,7 @@ import {
output, output,
} from '@angular/core' } from '@angular/core'
import { MarketplacePkgBase } from '../../types' import { MarketplacePkgBase } from '../../types'
import { CopyService, i18nPipe } from '@start9labs/shared' import { CopyService } from '@start9labs/shared'
import { DatePipe } from '@angular/common' import { DatePipe } from '@angular/common'
import { MarketplaceItemComponent } from './item.component' import { MarketplaceItemComponent } from './item.component'
@@ -44,15 +44,15 @@ import { MarketplaceItemComponent } from './item.component'
<marketplace-item <marketplace-item
(click)="copyService.copy(gitHash)" (click)="copyService.copy(gitHash)"
[data]="gitHash" [data]="gitHash"
label="Git hash" label="Git Hash"
icon="@tui.copy" icon="@tui.copy"
class="item-copy" class="item-copy"
/> />
} @else { } @else {
<div class="item-padding"> <div class="item-padding">
<label tuiTitle> <label tuiTitle>
<span tuiSubtitle>{{ 'Git hash' | i18n }}</span> <span tuiSubtitle>Git Hash</span>
{{ 'Unknown' | i18n }} Unknown
</label> </label>
</div> </div>
} }
@@ -128,7 +128,7 @@ import { MarketplaceItemComponent } from './item.component'
} }
`, `,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [MarketplaceItemComponent, DatePipe, i18nPipe], imports: [MarketplaceItemComponent, DatePipe],
}) })
export class MarketplaceAboutComponent { export class MarketplaceAboutComponent {
readonly copyService = inject(CopyService) readonly copyService = inject(CopyService)

View File

@@ -8,14 +8,13 @@ import {
} from '@angular/core' } from '@angular/core'
import { MarketplacePkgBase } from '../../../types' import { MarketplacePkgBase } from '../../../types'
import { MarketplaceDepItemComponent } from './dependency-item.component' import { MarketplaceDepItemComponent } from './dependency-item.component'
import { i18nPipe } from '@start9labs/shared'
@Component({ @Component({
selector: 'marketplace-dependencies', selector: 'marketplace-dependencies',
template: ` template: `
<div class="background-border shadow-color-light box-shadow-lg"> <div class="background-border shadow-color-light box-shadow-lg">
<div class="dependencies-container"> <div class="dependencies-container">
<h2 class="additional-detail-title">{{ 'Dependencies' | i18n }}</h2> <h2 class="additional-detail-title">Dependencies</h2>
<div class="dependencies-list"> <div class="dependencies-list">
@for (dep of pkg.dependencyMetadata | keyvalue; track $index) { @for (dep of pkg.dependencyMetadata | keyvalue; track $index) {
<marketplace-dep-item <marketplace-dep-item
@@ -49,7 +48,7 @@ import { i18nPipe } from '@start9labs/shared'
} }
`, `,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule, MarketplaceDepItemComponent, i18nPipe], imports: [CommonModule, MarketplaceDepItemComponent],
}) })
export class MarketplaceDependenciesComponent { export class MarketplaceDependenciesComponent {
@Input({ required: true }) @Input({ required: true })

View File

@@ -1,7 +1,7 @@
import { KeyValue } from '@angular/common' import { KeyValue } from '@angular/common'
import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { RouterModule } from '@angular/router' import { RouterModule } from '@angular/router'
import { ExverPipesModule, i18nPipe } from '@start9labs/shared' import { ExverPipesModule } from '@start9labs/shared'
import { T } from '@start9labs/start-sdk' import { T } from '@start9labs/start-sdk'
import { TuiAvatar, TuiLineClamp } from '@taiga-ui/kit' import { TuiAvatar, TuiLineClamp } from '@taiga-ui/kit'
import { MarketplacePkgBase } from '../../../types' import { MarketplacePkgBase } from '../../../types'
@@ -20,9 +20,9 @@ import { MarketplacePkgBase } from '../../../types'
</span> </span>
<p> <p>
@if (dep.value.optional) { @if (dep.value.optional) {
<span>({{ 'Optional' | i18n }})</span> <span>(optional)</span>
} @else { } @else {
<span>({{ 'Required' | i18n }})</span> <span>(required)</span>
} }
</p> </p>
</div> </div>
@@ -49,11 +49,10 @@ import { MarketplacePkgBase } from '../../../types'
filter: drop-shadow(0 10px 8px rgb(0 0 0 / 0.04)) filter: drop-shadow(0 10px 8px rgb(0 0 0 / 0.04))
drop-shadow(0 4px 3px rgb(0 0 0 / 0.1)); drop-shadow(0 4px 3px rgb(0 0 0 / 0.1));
// @TODO re-engage when button can link to root with search QP &:hover {
// &:hover { background-color: rgb(63 63 70 / 0.7);
// background-color: rgb(63 63 70 / 0.7); cursor: pointer;
// cursor: pointer; }
// }
} }
.title { .title {
@@ -89,7 +88,7 @@ import { MarketplacePkgBase } from '../../../types'
} }
`, `,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [RouterModule, TuiAvatar, ExverPipesModule, TuiLineClamp, i18nPipe], imports: [RouterModule, TuiAvatar, ExverPipesModule, TuiLineClamp],
}) })
export class MarketplaceDepItemComponent { export class MarketplaceDepItemComponent {
@Input({ required: true }) @Input({ required: true })

View File

@@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { RouterLink } from '@angular/router' import { RouterLink } from '@angular/router'
import { i18nPipe, SharedPipesModule } from '@start9labs/shared' import { SharedPipesModule } from '@start9labs/shared'
import { TuiTitle } from '@taiga-ui/core' import { TuiTitle } from '@taiga-ui/core'
import { TuiAvatar } from '@taiga-ui/kit' import { TuiAvatar } from '@taiga-ui/kit'
import { TuiCell } from '@taiga-ui/layout' import { TuiCell } from '@taiga-ui/layout'
@@ -11,9 +11,7 @@ import { MarketplacePkg } from '../../types'
template: ` template: `
<div class="background-border box-shadow-lg shadow-color-light"> <div class="background-border box-shadow-lg shadow-color-light">
<div class="box-container"> <div class="box-container">
<h2 class="additional-detail-title"> <h2 class="additional-detail-title">Alternative Implementations</h2>
{{ 'Alternative Implementations' | i18n }}
</h2>
@for (pkg of pkgs; track $index) { @for (pkg of pkgs; track $index) {
<a <a
tuiCell tuiCell
@@ -44,14 +42,7 @@ import { MarketplacePkg } from '../../types'
} }
`, `,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ imports: [RouterLink, TuiCell, TuiTitle, SharedPipesModule, TuiAvatar],
RouterLink,
TuiCell,
TuiTitle,
SharedPipesModule,
TuiAvatar,
i18nPipe,
],
}) })
export class MarketplaceFlavorsComponent { export class MarketplaceFlavorsComponent {
@Input() @Input()

View File

@@ -1,5 +1,4 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { i18nKey } from '@start9labs/shared'
import { TuiIcon, TuiTitle } from '@taiga-ui/core' import { TuiIcon, TuiTitle } from '@taiga-ui/core'
import { TuiFade } from '@taiga-ui/kit' import { TuiFade } from '@taiga-ui/kit'
@@ -7,7 +6,7 @@ import { TuiFade } from '@taiga-ui/kit'
selector: 'marketplace-item', selector: 'marketplace-item',
template: ` template: `
<label tuiTitle> <label tuiTitle>
<span tuiSubtitle>{{ label || '' }}</span> <span tuiSubtitle>{{ label }}</span>
<span tuiFade>{{ data }}</span> <span tuiFade>{{ data }}</span>
</label> </label>
<tui-icon [icon]="icon" /> <tui-icon [icon]="icon" />
@@ -39,7 +38,7 @@ import { TuiFade } from '@taiga-ui/kit'
}) })
export class MarketplaceItemComponent { export class MarketplaceItemComponent {
@Input({ required: true }) @Input({ required: true })
label!: i18nKey | null label!: string
@Input({ required: true }) @Input({ required: true })
icon!: string icon!: string

View File

@@ -1,6 +1,5 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { MarketplaceItemComponent } from './item.component' import { MarketplaceItemComponent } from './item.component'
import { i18nKey } from '@start9labs/shared'
@Component({ @Component({
selector: 'marketplace-link', selector: 'marketplace-link',
@@ -14,7 +13,7 @@ import { i18nKey } from '@start9labs/shared'
}) })
export class MarketplaceLinkComponent { export class MarketplaceLinkComponent {
@Input({ required: true }) @Input({ required: true })
label!: i18nKey label!: string
@Input({ required: true }) @Input({ required: true })
icon!: string icon!: string

View File

@@ -5,7 +5,7 @@ import {
input, input,
} from '@angular/core' } from '@angular/core'
import { ActivatedRoute } from '@angular/router' import { ActivatedRoute } from '@angular/router'
import { CopyService, i18nPipe } from '@start9labs/shared' import { CopyService } from '@start9labs/shared'
import { MarketplacePkgBase } from '../../types' import { MarketplacePkgBase } from '../../types'
import { MarketplaceLinkComponent } from './link.component' import { MarketplaceLinkComponent } from './link.component'
@@ -18,13 +18,13 @@ import { MarketplaceLinkComponent } from './link.component'
<div class="detail-container"> <div class="detail-container">
<marketplace-link <marketplace-link
[url]="pkg().upstreamRepo" [url]="pkg().upstreamRepo"
label="Upstream service" label="Upstream Service"
icon="@tui.external-link" icon="@tui.external-link"
class="item-pointer" class="item-pointer"
/> />
<marketplace-link <marketplace-link
[url]="pkg().wrapperRepo" [url]="pkg().wrapperRepo"
label="StartOS package" label="StartOS Package"
icon="@tui.external-link" icon="@tui.external-link"
class="item-pointer" class="item-pointer"
/> />
@@ -34,7 +34,7 @@ import { MarketplaceLinkComponent } from './link.component'
<div class="background-border shadow-color-light box-shadow-lg"> <div class="background-border shadow-color-light box-shadow-lg">
<div class="box-container"> <div class="box-container">
<h2 class="additional-detail-title">{{ 'Links' | i18n }}</h2> <h2 class="additional-detail-title">Links</h2>
<div class="detail-container"> <div class="detail-container">
<marketplace-link <marketplace-link
[url]="pkg().marketingSite" [url]="pkg().marketingSite"
@@ -117,7 +117,7 @@ import { MarketplaceLinkComponent } from './link.component'
} }
`, `,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [MarketplaceLinkComponent, i18nPipe], imports: [MarketplaceLinkComponent],
}) })
export class MarketplaceLinksComponent { export class MarketplaceLinksComponent {
readonly copyService = inject(CopyService) readonly copyService = inject(CopyService)

View File

@@ -18,12 +18,12 @@ import { MarketplaceItemComponent } from './item.component'
template: ` template: `
<div class="background-border shadow-color-light box-shadow-lg"> <div class="background-border shadow-color-light box-shadow-lg">
<div class="box-container"> <div class="box-container">
<h2 class="additional-detail-title">{{ 'Versions' | i18n }}</h2> <h2 class="additional-detail-title">Versions</h2>
<marketplace-item <marketplace-item
(click)="promptSelectVersion(versionSelect)" (click)="promptSelectVersion(versionSelect)"
[data]="'Select another version' | i18n" data="Select another version"
icon="@tui.chevron-right" icon="@tui.chevron-right"
[label]="null" label=""
class="select" class="select"
/> />
<ng-template <ng-template

View File

@@ -1,8 +1,4 @@
import { import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
provideHttpClient,
withFetch,
withInterceptorsFromDi,
} from '@angular/common/http'
import { inject, NgModule, provideAppInitializer } from '@angular/core' import { inject, NgModule, provideAppInitializer } from '@angular/core'
import { BrowserAnimationsModule } from '@angular/platform-browser/animations' import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
import { PreloadAllModules, RouterModule } from '@angular/router' import { PreloadAllModules, RouterModule } from '@angular/router'
@@ -55,7 +51,7 @@ const version = require('../../../../package.json').version
provide: VERSION, provide: VERSION,
useValue: version, useValue: version,
}, },
provideHttpClient(withInterceptorsFromDi(), withFetch()), provideHttpClient(withInterceptorsFromDi()),
provideAppInitializer(() => { provideAppInitializer(() => {
const origin = inject(WA_LOCATION).origin const origin = inject(WA_LOCATION).origin
const module_or_path = new URL('/assets/argon2_bg.wasm', origin) const module_or_path = new URL('/assets/argon2_bg.wasm', origin)

View File

@@ -7,15 +7,11 @@ import {
import { toSignal } from '@angular/core/rxjs-interop' import { toSignal } from '@angular/core/rxjs-interop'
import { Router } from '@angular/router' import { Router } from '@angular/router'
import { import {
DialogService,
formatProgress, formatProgress,
getErrorMessage, getErrorMessage,
i18nKey,
InitializingComponent, InitializingComponent,
LoadingService,
} from '@start9labs/shared' } from '@start9labs/shared'
import { T } from '@start9labs/start-sdk' import { T } from '@start9labs/start-sdk'
import { TuiButton } from '@taiga-ui/core'
import { import {
catchError, catchError,
filter, filter,
@@ -30,43 +26,19 @@ import { ApiService } from 'src/app/services/api.service'
import { StateService } from 'src/app/services/state.service' import { StateService } from 'src/app/services/state.service'
@Component({ @Component({
template: ` template:
@if (error(); as err) { '<app-initializing [setupType]="type" [progress]="progress()" [error]="error()" />',
<section>
<h1>{{ 'Error initializing server' }}</h1>
<p>{{ err }}</p>
<button tuiButton (click)="restart()">
{{ 'Restart server' }}
</button>
</section>
} @else {
<app-initializing [initialSetup]="true" [progress]="progress()" />
}
`,
styles: ` styles: `
:host { :host {
max-width: unset; max-width: unset;
align-items: stretch; align-items: stretch;
} }
section {
border-radius: 0.25rem;
padding: 1rem;
margin: 1.5rem;
text-align: center;
// @TODO Theme
background: #e0e0e0;
color: #333;
--tui-background-neutral-1: rgba(0, 0, 0, 0.1);
}
`, `,
imports: [InitializingComponent, TuiButton], imports: [InitializingComponent],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export default class LoadingPage { export default class LoadingPage {
private readonly api = inject(ApiService) private readonly api = inject(ApiService)
private readonly loader = inject(LoadingService)
private readonly dialog = inject(DialogService)
readonly type = inject(StateService).setupType readonly type = inject(StateService).setupType
readonly router = inject(Router) readonly router = inject(Router)
@@ -112,21 +84,4 @@ export default class LoadingPage {
return null return null
} }
async restart(): Promise<void> {
const loader = this.loader.open(undefined).subscribe()
try {
await this.api.restart()
this.dialog
.openAlert('Wait 1-2 minutes and refresh the page' as i18nKey, {
label: 'Server is restarting',
})
.subscribe()
} catch (e) {
console.error(e)
} finally {
loader.unsubscribe()
}
}
} }

View File

@@ -127,11 +127,12 @@ export default class SuccessPage implements AfterViewInit {
} }
download() { download() {
const torElem = this.document.getElementById('tor-addr') const torAddress = this.document.getElementById('tor-addr')
const lanElem = this.document.getElementById('lan-addr') const lanAddress = this.document.getElementById('lan-addr')
const html = this.documentation?.nativeElement.innerHTML || ''
if (torElem) torElem.innerHTML = this.torAddresses?.join('\n') || '' if (torAddress) torAddress.innerHTML = this.torAddresses?.join('\n') || ''
if (lanElem) lanElem.innerHTML = this.lanAddress || '' if (lanAddress) lanAddress.innerHTML = this.lanAddress || ''
this.document this.document
.getElementById('cert') .getElementById('cert')
@@ -139,9 +140,6 @@ export default class SuccessPage implements AfterViewInit {
'href', 'href',
`data:application/x-x509-ca-cert;base64,${encodeURIComponent(this.cert!)}`, `data:application/x-x509-ca-cert;base64,${encodeURIComponent(this.cert!)}`,
) )
const html = this.documentation?.nativeElement.innerHTML || ''
this.downloadHtml.download('StartOS-info.html', html).then(_ => { this.downloadHtml.download('StartOS-info.html', html).then(_ => {
this.disableLogin = false this.disableLogin = false
}) })

View File

@@ -24,7 +24,6 @@ export abstract class ApiService {
abstract complete(): Promise<T.SetupResult> // setup.complete abstract complete(): Promise<T.SetupResult> // setup.complete
abstract exit(): Promise<void> // setup.exit abstract exit(): Promise<void> // setup.exit
abstract initFollowLogs(): Promise<FollowLogsRes> // setup.logs.follow abstract initFollowLogs(): Promise<FollowLogsRes> // setup.logs.follow
abstract restart(): Promise<void> // setup.restart
abstract openWebsocket$<T>(guid: string): Observable<T> abstract openWebsocket$<T>(guid: string): Observable<T>
async encrypt(toEncrypt: string): Promise<T.EncryptedWire> { async encrypt(toEncrypt: string): Promise<T.EncryptedWire> {

View File

@@ -120,13 +120,6 @@ export class LiveApiService extends ApiService {
}) })
} }
async restart(): Promise<void> {
await this.rpcRequest<void>({
method: 'setup.restart',
params: {},
})
}
private async rpcRequest<T>(opts: RPCOptions): Promise<T> { private async rpcRequest<T>(opts: RPCOptions): Promise<T> {
const res = await this.http.rpcRequest<T>(opts) const res = await this.http.rpcRequest<T>(opts)

View File

@@ -8,7 +8,7 @@ import {
} from '@start9labs/shared' } from '@start9labs/shared'
import { T } from '@start9labs/start-sdk' import { T } from '@start9labs/start-sdk'
import * as jose from 'node-jose' import * as jose from 'node-jose'
import { first, interval, map, Observable } from 'rxjs' import { interval, map, Observable } from 'rxjs'
import { ApiService } from './api.service' import { ApiService } from './api.service'
@Injectable({ @Injectable({
@@ -119,7 +119,6 @@ export class MockApiService extends ApiService {
} else if (guid === 'progress-guid') { } else if (guid === 'progress-guid') {
// @TODO Matt mock progress // @TODO Matt mock progress
return interval(1000).pipe( return interval(1000).pipe(
first(),
map(() => ({ map(() => ({
overall: true, overall: true,
phases: [ phases: [
@@ -324,10 +323,6 @@ export class MockApiService extends ApiService {
async exit(): Promise<void> { async exit(): Promise<void> {
await pauseFor(1000) await pauseFor(1000)
} }
async restart(): Promise<void> {
await pauseFor(1000)
}
} }
const rootCA = `-----BEGIN CERTIFICATE----- const rootCA = `-----BEGIN CERTIFICATE-----

View File

@@ -1,2 +0,0 @@
<?xml version="1.0" standalone="no"?>
<svg xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000" width="128" height="128" viewBox="0 0 24 24" role="img" xmlns="http://www.w3.org/2000/svg"><title style="" fill="#fff">Bitcoin icon</title><path d="M23.638 14.904c-1.602 6.43-8.113 10.34-14.542 8.736C2.67 22.05-1.244 15.525.362 9.105 1.962 2.67 8.475-1.243 14.9.358c6.43 1.605 10.342 8.115 8.738 14.548v-.002zm-6.35-4.613c.24-1.59-.974-2.45-2.64-3.03l.54-2.153-1.315-.33-.525 2.107c-.345-.087-.705-.167-1.064-.25l.526-2.127-1.32-.33-.54 2.165c-.285-.067-.565-.132-.84-.2l-1.815-.45-.35 1.407s.975.225.955.236c.535.136.63.486.615.766l-1.477 5.92c-.075.166-.24.406-.614.314.015.02-.96-.24-.96-.24l-.66 1.51 1.71.426.93.242-.54 2.19 1.32.327.54-2.17c.36.1.705.19 1.05.273l-.51 2.154 1.32.33.545-2.19c2.24.427 3.93.257 4.64-1.774.57-1.637-.03-2.58-1.217-3.196.854-.193 1.5-.76 1.68-1.93h.01zm-3.01 4.22c-.404 1.64-3.157.75-4.05.53l.72-2.9c.896.23 3.757.67 3.33 2.37zm.41-4.24c-.37 1.49-2.662.735-3.405.55l.654-2.64c.744.18 3.137.524 2.75 2.084v.006z" style="" fill="#fff"/></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -4,6 +4,7 @@ import {
computed, computed,
inject, inject,
input, input,
Input,
} from '@angular/core' } from '@angular/core'
import { TuiProgress } from '@taiga-ui/kit' import { TuiProgress } from '@taiga-ui/kit'
import { LogsWindowComponent } from './logs-window.component' import { LogsWindowComponent } from './logs-window.component'
@@ -12,25 +13,32 @@ import { i18nPipe } from '../../i18n/i18n.pipe'
@Component({ @Component({
selector: 'app-initializing', selector: 'app-initializing',
template: ` template: `
<section> @if (error(); as err) {
<h1 [style.font-size.rem]="2" [style.margin-bottom.rem]="2"> <section>
{{ <h1>{{ 'Error initializing server' | i18n }}</h1>
initialSetup() <p>{{ err }}</p>
? ('Setting up your server' | i18n) </section>
: ('Booting StartOS' | i18n) } @else {
}} <section>
</h1> <h1 [style.font-size.rem]="2" [style.margin-bottom.rem]="2">
<div> {{
{{ 'Progress' | i18n }}: {{ (progress().total * 100).toFixed(0) }}% setupType()
</div> ? ('Setting up your server' | i18n)
<progress : ('Booting StartOS' | i18n)
tuiProgressBar }}
[style.max-width.rem]="40" </h1>
[style.margin]="'1rem auto'" <div>
[attr.value]="progress().total" {{ 'Progress' | i18n }}: {{ (progress().total * 100).toFixed(0) }}%
></progress> </div>
<p [innerHTML]="message()"></p> <progress
</section> tuiProgressBar
[style.max-width.rem]="40"
[style.margin]="'1rem auto'"
[attr.value]="progress().total"
></progress>
<p [innerHTML]="message()"></p>
</section>
}
<logs-window /> <logs-window />
`, `,
styles: ` styles: `
@@ -68,7 +76,10 @@ export class InitializingComponent {
total: 0, total: 0,
message: '', message: '',
}) })
readonly initialSetup = input(false) readonly setupType = input<
'fresh' | 'restore' | 'attach' | 'transfer' | undefined
>()
readonly error = input<string>()
readonly message = computed(() => { readonly message = computed(() => {
return ( return (

View File

@@ -125,6 +125,7 @@ export default {
137: 'Tor-Logs', 137: 'Tor-Logs',
138: 'Rohdatenprotokolle des Betriebssystems ohne Filter', 138: 'Rohdatenprotokolle des Betriebssystems ohne Filter',
139: 'Diagnose für Treiber und andere Kernel-Prozesse', 139: 'Diagnose für Treiber und andere Kernel-Prozesse',
140: 'Diagnose-Logs des Tor-Daemons unter StartOS',
141: 'Downgrade', 141: 'Downgrade',
142: 'Neu installieren', 142: 'Neu installieren',
143: 'Installierte', 143: 'Installierte',
@@ -274,7 +275,7 @@ export default {
293: 'Erneut versuchen', 293: 'Erneut versuchen',
294: '.s9pk-Paketdatei hochladen', 294: '.s9pk-Paketdatei hochladen',
295: 'Warnung: Der Upload über Tor ist langsam.', 295: 'Warnung: Der Upload über Tor ist langsam.',
296: 'Auswählen', 296: 'Hochladen',
297: 'Version 1 s9pk erkannt. Dieses Format ist veraltet. Falls nötig, kann ein V1 s9pk über start-cli installiert werden.', 297: 'Version 1 s9pk erkannt. Dieses Format ist veraltet. Falls nötig, kann ein V1 s9pk über start-cli installiert werden.',
298: 'Ungültige Paketdatei', 298: 'Ungültige Paketdatei',
300: 'Anleitung anzeigen', 300: 'Anleitung anzeigen',
@@ -563,7 +564,7 @@ export default {
597: 'der sich auflöst zu', 597: 'der sich auflöst zu',
598: 'Nicht empfohlen für VPN-Zugriff. VPNs unterstützen keine „.local“-Domains ohne erweiterte Konfiguration', 598: 'Nicht empfohlen für VPN-Zugriff. VPNs unterstützen keine „.local“-Domains ohne erweiterte Konfiguration',
599: 'Kann für Clearnet-Zugriff verwendet werden', 599: 'Kann für Clearnet-Zugriff verwendet werden',
600: 'In den meisten Fällen nicht empfohlen. Öffentliche Domains werden bevorzugt', 600: 'In den meisten Fällen nicht empfohlen. Clearnet-Domains werden bevorzugt',
601: 'Lokal', 601: 'Lokal',
602: 'Kann für lokalen Zugriff verwendet werden', 602: 'Kann für lokalen Zugriff verwendet werden',
603: 'Ideal für öffentlichen Zugriff über das Internet', 603: 'Ideal für öffentlichen Zugriff über das Internet',
@@ -572,7 +573,7 @@ export default {
606: 'Host', 606: 'Host',
607: 'Wert', 607: 'Wert',
608: 'Zweck', 608: 'Zweck',
609: 'alle Subdomains von', 609: 'Subdomains von',
610: 'Dynamisches DNS', 610: 'Dynamisches DNS',
611: 'Keine Service-Schnittstellen', 611: 'Keine Service-Schnittstellen',
612: 'Grund', 612: 'Grund',
@@ -584,9 +585,4 @@ export default {
618: 'Statische Server', 618: 'Statische Server',
619: 'Warnung. StartOS verwendet derzeit das folgende Gateway für DNS', 619: 'Warnung. StartOS verwendet derzeit das folgende Gateway für DNS',
620: 'Wenn Sie dieses Gateway für die Auflösung privater Domains verwenden möchten, legen Sie alternative statische DNS-Server mit dem obigen Formular fest.', 620: 'Wenn Sie dieses Gateway für die Auflösung privater Domains verwenden möchten, legen Sie alternative statische DNS-Server mit dem obigen Formular fest.',
621: 'Einen Dienst paketieren',
622: 'Veröffentlicht',
623: 'Alternative Implementierungen',
624: 'Versionen',
625: 'Eine andere Version auswählen',
} satisfies i18n } satisfies i18n

View File

@@ -124,6 +124,7 @@ export const ENGLISH = {
'Tor Logs': 137, 'Tor Logs': 137,
'Raw, unfiltered operating system logs': 138, 'Raw, unfiltered operating system logs': 138,
'Diagnostics for drivers and other kernel processes': 139, 'Diagnostics for drivers and other kernel processes': 139,
'Diagnostic logs for the Tor daemon on StartOS': 140,
'Downgrade': 141, 'Downgrade': 141,
'Reinstall': 142, 'Reinstall': 142,
'Installed': 143, 'Installed': 143,
@@ -273,7 +274,7 @@ export const ENGLISH = {
'Try again': 293, 'Try again': 293,
'Upload .s9pk package file': 294, 'Upload .s9pk package file': 294,
'Warning: package upload will be slow over Tor.': 295, 'Warning: package upload will be slow over Tor.': 295,
'Select': 296, 'Upload': 296,
'Version 1 s9pk detected. This package format is deprecated. You can sideload a V1 s9pk via start-cli if necessary.': 297, 'Version 1 s9pk detected. This package format is deprecated. You can sideload a V1 s9pk via start-cli if necessary.': 297,
'Invalid package file': 298, 'Invalid package file': 298,
'View instructions': 300, 'View instructions': 300,
@@ -562,7 +563,7 @@ export const ENGLISH = {
'that resolves to': 597, // this is a partial sentence. It is preceded by "requires a DNS record for [domain] " 'that resolves to': 597, // this is a partial sentence. It is preceded by "requires a DNS record for [domain] "
'Not recommended for VPN access. VPNs do not support ".local" domains without advanced configuration': 598, 'Not recommended for VPN access. VPNs do not support ".local" domains without advanced configuration': 598,
'Can be used for clearnet access': 599, 'Can be used for clearnet access': 599,
'Not recommended in most cases. Public domains are preferred': 600, 'Not recommended in most cases. Clearnet domains are preferred': 600,
'Local': 601, // as in, not remote 'Local': 601, // as in, not remote
'Can be used for local access': 602, 'Can be used for local access': 602,
'Ideal for public access via the Internet': 603, 'Ideal for public access via the Internet': 603,
@@ -571,7 +572,7 @@ export const ENGLISH = {
'Host': 606, // as in, a network host 'Host': 606, // as in, a network host
'Value': 607, // as in, the value in a column of a table 'Value': 607, // as in, the value in a column of a table
'Purpose': 608, // as in, the reason for a thing to exist 'Purpose': 608, // as in, the reason for a thing to exist
'all subdomains of': 609, // this is a partial sentence. A domain name will be added after "of" to complete the sentence. 'subdomains of': 609, // this is a partial sentence. A domain name will be added after "of" to complete the sentence.
'Dynamic DNS': 610, 'Dynamic DNS': 610,
'No service interfaces': 611, // as in, there are no available interfaces (API, UI, etc) for this software application 'No service interfaces': 611, // as in, there are no available interfaces (API, UI, etc) for this software application
'Reason': 612, // as in, an explanation for something 'Reason': 612, // as in, an explanation for something
@@ -583,9 +584,4 @@ export const ENGLISH = {
'Static Servers': 618, // as in, servers that do not change 'Static Servers': 618, // as in, servers that do not change
'Warning. StartOS is currently using the following gateway for DNS': 619, 'Warning. StartOS is currently using the following gateway for DNS': 619,
'If you intend to use this gateway for private domain resolution, set alternative static DNS servers using the form above.': 620, 'If you intend to use this gateway for private domain resolution, set alternative static DNS servers using the form above.': 620,
'Package a service': 621, // as in, package a software application for an operating system
'Released': 622, // as in, the date something became available
'Alternative Implementations': 623,
'Versions': 624,
'Select another version': 625,
} as const } as const

View File

@@ -125,6 +125,7 @@ export default {
137: 'Registros de Tor', 137: 'Registros de Tor',
138: 'Registros sin filtrar del sistema operativo', 138: 'Registros sin filtrar del sistema operativo',
139: 'Diagnóstico de controladores y otros procesos del kernel', 139: 'Diagnóstico de controladores y otros procesos del kernel',
140: 'Registros de diagnóstico del servicio Tor en StartOS',
141: 'Retroceder versión', 141: 'Retroceder versión',
142: 'Reinstalar', 142: 'Reinstalar',
143: 'Instalados', 143: 'Instalados',
@@ -274,7 +275,7 @@ export default {
293: 'Intentar de nuevo', 293: 'Intentar de nuevo',
294: 'Subir archivo de paquete .s9pk', 294: 'Subir archivo de paquete .s9pk',
295: 'Advertencia: la carga del paquete será lenta a través de Tor.', 295: 'Advertencia: la carga del paquete será lenta a través de Tor.',
296: 'Seleccionar', 296: 'Subir',
297: 'Se detectó un paquete s9pk de versión 1. Este formato está obsoleto. Puedes instalarlo manualmente con start-cli si es necesario.', 297: 'Se detectó un paquete s9pk de versión 1. Este formato está obsoleto. Puedes instalarlo manualmente con start-cli si es necesario.',
298: 'Archivo de paquete inválido', 298: 'Archivo de paquete inválido',
300: 'Ver instrucciones', 300: 'Ver instrucciones',
@@ -563,7 +564,7 @@ export default {
597: 'que se resuelva en', 597: 'que se resuelva en',
598: 'No recomendado para acceso VPN. Las VPN no admiten dominios “.local” sin configuración avanzada', 598: 'No recomendado para acceso VPN. Las VPN no admiten dominios “.local” sin configuración avanzada',
599: 'Se puede usar para acceso a clearnet', 599: 'Se puede usar para acceso a clearnet',
600: 'No recomendado en la mayoría de los casos. Se prefieren los dominios de públicos', 600: 'No recomendado en la mayoría de los casos. Se prefieren los dominios de clearnet',
601: 'Local', 601: 'Local',
602: 'Se puede usar para acceso local', 602: 'Se puede usar para acceso local',
603: 'Ideal para acceso público a través de Internet', 603: 'Ideal para acceso público a través de Internet',
@@ -572,7 +573,7 @@ export default {
606: 'Host', 606: 'Host',
607: 'Valor', 607: 'Valor',
608: 'Propósito', 608: 'Propósito',
609: 'todos los subdominios de', 609: 'Subdominios de',
610: 'DNS dinámico', 610: 'DNS dinámico',
611: 'Sin interfaces de servicio', 611: 'Sin interfaces de servicio',
612: 'Razón', 612: 'Razón',
@@ -584,9 +585,4 @@ export default {
618: 'Servidores estáticos', 618: 'Servidores estáticos',
619: 'Advertencia. StartOS está utilizando actualmente la siguiente puerta de enlace para DNS', 619: 'Advertencia. StartOS está utilizando actualmente la siguiente puerta de enlace para DNS',
620: 'Si deseas usar esta puerta de enlace para la resolución de dominios privados, configura servidores DNS estáticos alternativos usando el formulario anterior.', 620: 'Si deseas usar esta puerta de enlace para la resolución de dominios privados, configura servidores DNS estáticos alternativos usando el formulario anterior.',
621: 'Empaquetar un servicio',
622: 'Publicado',
623: 'Implementaciones alternativas',
624: 'Versiones',
625: 'Seleccionar otra versión',
} satisfies i18n } satisfies i18n

View File

@@ -125,6 +125,7 @@ export default {
137: 'Journaux Tor', 137: 'Journaux Tor',
138: 'Journaux système bruts et non filtrés', 138: 'Journaux système bruts et non filtrés',
139: 'Diagnostics des pilotes et autres processus du noyau', 139: 'Diagnostics des pilotes et autres processus du noyau',
140: 'Journaux de diagnostic pour le service Tor sur StartOS',
141: 'Rétrograder', 141: 'Rétrograder',
142: 'Réinstaller', 142: 'Réinstaller',
143: 'Installé', 143: 'Installé',
@@ -274,7 +275,7 @@ export default {
293: 'Réessayer', 293: 'Réessayer',
294: 'Téléverser un fichier .s9pk', 294: 'Téléverser un fichier .s9pk',
295: 'Attention : le téléversement du paquet sera lent via Tor.', 295: 'Attention : le téléversement du paquet sera lent via Tor.',
296: 'Sélectionner', 296: 'Téléverser',
297: 'Version 1 de s9pk détectée. Ce format de paquet est obsolète. Vous pouvez installer manuellement un s9pk V1 via start-cli si nécessaire.', 297: 'Version 1 de s9pk détectée. Ce format de paquet est obsolète. Vous pouvez installer manuellement un s9pk V1 via start-cli si nécessaire.',
298: 'Fichier paquet invalide', 298: 'Fichier paquet invalide',
300: 'Voir les instructions', 300: 'Voir les instructions',
@@ -563,7 +564,7 @@ export default {
597: 'qui se résout en', 597: 'qui se résout en',
598: 'Non recommandé pour laccès VPN. Les VPN ne prennent pas en charge les domaines « .local » sans configuration avancée', 598: 'Non recommandé pour laccès VPN. Les VPN ne prennent pas en charge les domaines « .local » sans configuration avancée',
599: 'Peut être utilisé pour un accès clearnet', 599: 'Peut être utilisé pour un accès clearnet',
600: 'Non recommandé dans la plupart des cas. Les domaines publics sont préférés', 600: 'Non recommandé dans la plupart des cas. Les domaines clearnet sont préférés',
601: 'Local', 601: 'Local',
602: 'Peut être utilisé pour un accès local', 602: 'Peut être utilisé pour un accès local',
603: 'Idéal pour un accès public via Internet', 603: 'Idéal pour un accès public via Internet',
@@ -572,7 +573,7 @@ export default {
606: 'Hôte', 606: 'Hôte',
607: 'Valeur', 607: 'Valeur',
608: 'But', 608: 'But',
609: 'tous les sous-domaines de', 609: 'Sous-domaines de',
610: 'DNS dynamique', 610: 'DNS dynamique',
611: 'Aucune interface de service', 611: 'Aucune interface de service',
612: 'Raison', 612: 'Raison',
@@ -584,9 +585,4 @@ export default {
618: 'Serveurs statiques', 618: 'Serveurs statiques',
619: 'Avertissement. StartOS utilise actuellement la passerelle suivante pour le DNS', 619: 'Avertissement. StartOS utilise actuellement la passerelle suivante pour le DNS',
620: 'Si vous souhaitez utiliser cette passerelle pour la résolution de domaines privés, définissez des serveurs DNS statiques alternatifs à laide du formulaire ci-dessus.', 620: 'Si vous souhaitez utiliser cette passerelle pour la résolution de domaines privés, définissez des serveurs DNS statiques alternatifs à laide du formulaire ci-dessus.',
621: 'Emballer un service',
622: 'Publié',
623: 'Implémentations alternatives',
624: 'Versions',
625: 'Sélectionner une autre version',
} satisfies i18n } satisfies i18n

View File

@@ -125,6 +125,7 @@ export default {
137: 'Logi Tor', 137: 'Logi Tor',
138: 'Surowe, nieprzefiltrowane logi systemu operacyjnego', 138: 'Surowe, nieprzefiltrowane logi systemu operacyjnego',
139: 'Diagnostyka sterowników i innych procesów jądra', 139: 'Diagnostyka sterowników i innych procesów jądra',
140: 'Logi diagnostyczne usługi Tor w StartOS',
141: 'Przywróć starszą wersję', 141: 'Przywróć starszą wersję',
142: 'Zainstaluj ponownie', 142: 'Zainstaluj ponownie',
143: 'Zainstalowane', 143: 'Zainstalowane',
@@ -274,7 +275,7 @@ export default {
293: 'Spróbuj ponownie', 293: 'Spróbuj ponownie',
294: 'Prześlij plik pakietu .s9pk', 294: 'Prześlij plik pakietu .s9pk',
295: 'Uwaga: przesyłanie pakietu przez Tor będzie powolne.', 295: 'Uwaga: przesyłanie pakietu przez Tor będzie powolne.',
296: 'Wybierz', 296: 'Prześlij',
297: 'Wykryto pakiet s9pk w wersji 1. Ten format pakietu jest przestarzały. Możesz zainstalować pakiet s9pk V1 przez start-cli, jeśli to konieczne.', 297: 'Wykryto pakiet s9pk w wersji 1. Ten format pakietu jest przestarzały. Możesz zainstalować pakiet s9pk V1 przez start-cli, jeśli to konieczne.',
298: 'Nieprawidłowy plik pakietu', 298: 'Nieprawidłowy plik pakietu',
300: 'Zobacz instrukcje', 300: 'Zobacz instrukcje',
@@ -563,7 +564,7 @@ export default {
597: 'który rozwiązuje się na', 597: 'który rozwiązuje się na',
598: 'Niezalecane do dostępu VPN. VPN-y nie obsługują domen „.local” bez zaawansowanej konfiguracji', 598: 'Niezalecane do dostępu VPN. VPN-y nie obsługują domen „.local” bez zaawansowanej konfiguracji',
599: 'Może być używane do dostępu do clearnet', 599: 'Może być używane do dostępu do clearnet',
600: 'Niezalecane w większości przypadków. Preferowane są domeny publiczne', 600: 'Niezalecane w większości przypadków. Preferowane są domeny clearnet',
601: 'Lokalne', 601: 'Lokalne',
602: 'Może być używane do dostępu lokalnego', 602: 'Może być używane do dostępu lokalnego',
603: 'Idealne do publicznego dostępu przez Internet', 603: 'Idealne do publicznego dostępu przez Internet',
@@ -572,7 +573,7 @@ export default {
606: 'Host', 606: 'Host',
607: 'Wartość', 607: 'Wartość',
608: 'Cel', 608: 'Cel',
609: 'wszystkie subdomeny', 609: 'Subdomeny',
610: 'Dynamiczny DNS', 610: 'Dynamiczny DNS',
611: 'Brak interfejsów usług', 611: 'Brak interfejsów usług',
612: 'Powód', 612: 'Powód',
@@ -584,9 +585,4 @@ export default {
618: 'Serwery statyczne', 618: 'Serwery statyczne',
619: 'Ostrzeżenie. StartOS obecnie używa następującej bramy do DNS', 619: 'Ostrzeżenie. StartOS obecnie używa następującej bramy do DNS',
620: 'Jeśli zamierzasz używać tej bramy do rozwiązywania domen prywatnych, ustaw alternatywne statyczne serwery DNS za pomocą powyższego formularza.', 620: 'Jeśli zamierzasz używać tej bramy do rozwiązywania domen prywatnych, ustaw alternatywne statyczne serwery DNS za pomocą powyższego formularza.',
621: 'Spakietuj usługę',
622: 'Wydano',
623: 'Alternatywne implementacje',
624: 'Wersje',
625: 'Wybierz inną wersję',
} satisfies i18n } satisfies i18n

View File

@@ -39,7 +39,7 @@ export class DialogService {
}) })
} }
openConfirm( openConfirm<T = void>(
options: Partial<TuiResponsiveDialogOptions<TuiConfirmData>> & { options: Partial<TuiResponsiveDialogOptions<TuiConfirmData>> & {
label: i18nKey label: i18nKey
data?: TuiConfirmData & { data?: TuiConfirmData & {
@@ -51,7 +51,7 @@ export class DialogService {
) { ) {
const { content, yes, no } = options.data || {} const { content, yes, no } = options.data || {}
return this.dialogs.open<boolean>(TUI_CONFIRM, { return this.dialogs.open<T>(TUI_CONFIRM, {
...options, ...options,
label: this.i18n.transform(options.label), label: this.i18n.transform(options.label),
data: { data: {

View File

@@ -1,8 +1,4 @@
import { import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
provideHttpClient,
withFetch,
withInterceptorsFromDi,
} from '@angular/common/http'
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { BrowserModule } from '@angular/platform-browser' import { BrowserModule } from '@angular/platform-browser'
import { ServiceWorkerModule } from '@angular/service-worker' import { ServiceWorkerModule } from '@angular/service-worker'
@@ -27,10 +23,7 @@ import { RoutingModule } from './routing.module'
registrationStrategy: 'registerWhenStable:30000', registrationStrategy: 'registerWhenStable:30000',
}), }),
], ],
providers: [ providers: [APP_PROVIDERS, provideHttpClient(withInterceptorsFromDi())],
APP_PROVIDERS,
provideHttpClient(withInterceptorsFromDi(), withFetch()),
],
bootstrap: [AppComponent], bootstrap: [AppComponent],
}) })
export class AppModule {} export class AppModule {}

View File

@@ -39,7 +39,7 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
`, `,
styles: ` styles: `
:host { :host {
max-height: 100dvh; max-height: 100vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: flex-start; justify-content: flex-start;

View File

@@ -4,7 +4,7 @@
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
min-height: 100dvh; min-height: 100vh;
} }
[tuiButton] { [tuiButton] {

View File

@@ -194,7 +194,7 @@ export class FormArrayComponent {
add() { add() {
if (!this.warned && this.spec.warning) { if (!this.warned && this.spec.warning) {
this.dialog this.dialog
.openConfirm({ .openConfirm<boolean>({
label: 'Warning', label: 'Warning',
size: 's', size: 's',
data: { data: {

View File

@@ -20,7 +20,7 @@ import { InterfaceComponent } from './interface.component'
<header>{{ 'Gateways' | i18n }}</header> <header>{{ 'Gateways' | i18n }}</header>
@for (gateway of gateways(); track $index) { @for (gateway of gateways(); track $index) {
<label tuiCell="s" [style.background]=""> <label tuiCell="s" [style.background]="">
<span tuiTitle [style.opacity]="1">{{ gateway.name }}</span> <span tuiTitle [style.opacity]="1">{{ gateway.ipInfo.name }}</span>
@if (!interface.packageId() && !gateway.public) { @if (!interface.packageId() && !gateway.public) {
<tui-icon <tui-icon
[tuiTooltip]=" [tuiTooltip]="
@@ -48,18 +48,16 @@ import { InterfaceComponent } from './interface.component'
`, `,
styles: ` styles: `
:host { :host {
grid-column: span 3; grid-column: span 2;
&:has(+ section table) header {
background: transparent;
}
} }
[tuiCell]:has([tuiTooltip]) { [tuiCell]:has([tuiTooltip]) {
background: none !important; background: none !important;
} }
:host-context(tui-root:not(._mobile)) {
&:has(+ section table) header {
background: transparent;
}
}
`, `,
host: { class: 'g-card' }, host: { class: 'g-card' },
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,

View File

@@ -29,7 +29,7 @@ import { InterfaceAddressesComponent } from './addresses/addresses.component'
div { div {
display: grid; display: grid;
grid-template-columns: repeat(10, 1fr); grid-template-columns: repeat(6, 1fr);
gap: inherit; gap: inherit;
flex-direction: column; flex-direction: column;
} }

View File

@@ -6,9 +6,8 @@ import { PublicDomain } from './public-domains/pd.service'
import { i18nKey, i18nPipe } from '@start9labs/shared' import { i18nKey, i18nPipe } from '@start9labs/shared'
type AddressWithInfo = { type AddressWithInfo = {
url: string url: URL
info: T.HostnameInfo info: T.HostnameInfo
gateway?: GatewayPlus
} }
function cmpWithRankedPredicates<T extends AddressWithInfo>( function cmpWithRankedPredicates<T extends AddressWithInfo>(
@@ -30,7 +29,7 @@ function filterTor(a: AddressWithInfo): a is TorAddress {
} }
function cmpTor(a: TorAddress, b: TorAddress): -1 | 0 | 1 { function cmpTor(a: TorAddress, b: TorAddress): -1 | 0 | 1 {
for (let [x, y, sign] of [[a, b, 1] as const, [b, a, -1] as const]) { for (let [x, y, sign] of [[a, b, 1] as const, [b, a, -1] as const]) {
if (y.url.startsWith('http:') && x.url.startsWith('https:')) return sign if (y.url.protocol === 'http:' && x.url.protocol === 'https:') return sign
} }
return 0 return 0
} }
@@ -92,8 +91,7 @@ function cmpClearnet(
return cmpWithRankedPredicates(a, b, [ return cmpWithRankedPredicates(a, b, [
x => x =>
x.info.hostname.kind === 'domain' && x.info.hostname.kind === 'domain' &&
x.info.gateway.id === host.publicDomains[x.info.hostname.value]?.gateway, // public domain for this gateway x.info.gatewayId === host.publicDomains[x.info.hostname.value]?.gateway, // public domain for this gateway
x => x.gateway?.public ?? false, // public gateway
x => x.info.hostname.kind === 'ipv4', // ipv4 x => x.info.hostname.kind === 'ipv4', // ipv4
x => x.info.hostname.kind === 'ipv6', // ipv6 x => x.info.hostname.kind === 'ipv6', // ipv6
// remainder: private domains / domains public on other gateways // remainder: private domains / domains public on other gateways
@@ -133,11 +131,9 @@ export class InterfaceService {
if (!hostnamesInfos.length) return addresses if (!hostnamesInfos.length) return addresses
const allAddressesWithInfo: AddressWithInfo[] = hostnamesInfos.flatMap(h => const allAddressesWithInfo: AddressWithInfo[] = hostnamesInfos.flatMap(h =>
utils.addressHostToUrl(serviceInterface.addressInfo, h).map(url => ({ utils
url, .addressHostToUrl(serviceInterface.addressInfo, h)
info: h, .map(a => ({ url: new URL(a), info: h })),
gateway: gateways.find(g => h.kind === 'ip' && h.gateway.id === g.id),
})),
) )
const torAddrs = allAddressesWithInfo.filter(filterTor).sort(cmpTor) const torAddrs = allAddressesWithInfo.filter(filterTor).sort(cmpTor)
@@ -151,14 +147,7 @@ export class InterfaceService {
.filter(filterClearnet) .filter(filterClearnet)
.sort((a, b) => cmpClearnet(host, a, b)) .sort((a, b) => cmpClearnet(host, a, b))
let bestAddrs = [ let bestAddrs = [clearnetAddrs[0], lanAddrs[0], vpnAddrs[0], torAddrs[0]]
(clearnetAddrs[0]?.gateway?.public ||
clearnetAddrs[0]?.info.hostname.kind === 'domain') &&
clearnetAddrs[0],
lanAddrs[0],
vpnAddrs[0],
torAddrs[0],
]
.filter(a => !!a) .filter(a => !!a)
.reduce((acc, x) => { .reduce((acc, x) => {
if (!acc.includes(x)) acc.push(x) if (!acc.includes(x)) acc.push(x)
@@ -306,14 +295,14 @@ export class InterfaceService {
h.kind === 'ip' && h.kind === 'ip' &&
((h.hostname.kind === 'ipv6' && ((h.hostname.kind === 'ipv6' &&
utils.IPV6_LINK_LOCAL.contains(h.hostname.value)) || utils.IPV6_LINK_LOCAL.contains(h.hostname.value)) ||
h.gateway.id === 'lo') h.gatewayId === 'lo')
), ),
) || [] ) || []
) )
} }
private toDisplayAddress( private toDisplayAddress(
{ info, url, gateway }: AddressWithInfo, { info, url }: AddressWithInfo,
gateways: GatewayPlus[], gateways: GatewayPlus[],
publicDomains: Record<string, T.PublicDomainConfig>, publicDomains: Record<string, T.PublicDomainConfig>,
): DisplayAddress { ): DisplayAddress {
@@ -321,6 +310,7 @@ export class InterfaceService {
let gatewayName: DisplayAddress['gatewayName'] let gatewayName: DisplayAddress['gatewayName']
let type: DisplayAddress['type'] let type: DisplayAddress['type']
let bullets: any[] let bullets: any[]
// let bullets: DisplayAddress['bullets']
const rootCaRequired = this.i18n.transform( const rootCaRequired = this.i18n.transform(
"Requires trusting your server's Root CA", "Requires trusting your server's Root CA",
@@ -339,7 +329,7 @@ export class InterfaceService {
this.i18n.transform('Requires using a Tor-enabled device or browser'), this.i18n.transform('Requires using a Tor-enabled device or browser'),
] ]
// Tor (HTTPS) // Tor (HTTPS)
if (url.startsWith('https:')) { if (url.protocol.startsWith('https')) {
type = `${type} (HTTPS)` type = `${type} (HTTPS)`
bullets = [ bullets = [
this.i18n.transform('Only useful for clients that enforce HTTPS'), this.i18n.transform('Only useful for clients that enforce HTTPS'),
@@ -353,15 +343,13 @@ export class InterfaceService {
'Ideal for anonymous, censorship-resistant hosting and remote access', 'Ideal for anonymous, censorship-resistant hosting and remote access',
), ),
) )
if (url.startsWith('http:')) { type = `${type} (HTTP)`
type = `${type} (HTTP)`
}
} }
// ** Not Tor ** // ** Not Tor **
} else { } else {
const port = info.hostname.sslPort || info.hostname.port const port = info.hostname.sslPort || info.hostname.port
const gateway = gateways.find(g => g.id === info.gateway.id)! const gateway = gateways.find(g => g.id === info.gatewayId)!
gatewayName = gateway.name gatewayName = gateway.ipInfo.name
const gatewayLanIpv4 = gateway.lanIpv4[0] const gatewayLanIpv4 = gateway.lanIpv4[0]
const isWireguard = gateway.ipInfo.deviceType === 'wireguard' const isWireguard = gateway.ipInfo.deviceType === 'wireguard'
@@ -401,13 +389,13 @@ export class InterfaceService {
bullets = [ bullets = [
this.i18n.transform('Can be used for clearnet access'), this.i18n.transform('Can be used for clearnet access'),
this.i18n.transform( this.i18n.transform(
'Not recommended in most cases. Public domains are preferred', 'Not recommended in most cases. Clearnet domains are preferred',
), ),
rootCaRequired, rootCaRequired,
] ]
if (!gateway.public) { if (!gateway.public) {
bullets.push( bullets.push(
`${portForwarding} "${gatewayName}": ${port} -> ${gateway.subnets.find(s => s.isIpv4())?.address}:${port}`, `${portForwarding} "${gatewayName}": ${port} -> ${info.hostname.value}:${port}`,
) )
} }
} else { } else {
@@ -440,14 +428,8 @@ export class InterfaceService {
access = 'public' access = 'public'
bullets = [ bullets = [
`${dnsFor} ${info.hostname.value} ${resolvesTo} ${gateway.ipInfo.wanIp}`, `${dnsFor} ${info.hostname.value} ${resolvesTo} ${gateway.ipInfo.wanIp}`,
`${portForwarding} "${gatewayName}": ${port} -> ${info.hostname.value}:${port === 443 ? 5443 : port}`,
] ]
if (!gateway.public) {
bullets.push(
`${portForwarding} "${gatewayName}": ${port} -> ${gateway.subnets.find(s => s.isIpv4())?.address}:${port === 443 ? 5443 : port}`,
)
}
if (publicDomains[info.hostname.value]?.acme) { if (publicDomains[info.hostname.value]?.acme) {
bullets.unshift( bullets.unshift(
this.i18n.transform('Ideal for public access via the Internet'), this.i18n.transform('Ideal for public access via the Internet'),
@@ -488,7 +470,7 @@ export class InterfaceService {
} }
return { return {
url, url: url.href,
access, access,
gatewayName, gatewayName,
type, type,

View File

@@ -78,8 +78,7 @@ import { InterfaceComponent } from './interface.component'
`, `,
styles: ` styles: `
:host { :host {
grid-column: span 4; grid-column: span 3;
overflow-wrap: break-word;
} }
`, `,
host: { class: 'g-card' }, host: { class: 'g-card' },

View File

@@ -125,7 +125,7 @@ export class DnsComponent {
const segments = subdomain.split('.').slice(1) const segments = subdomain.split('.').slice(1)
const subdomains = this.i18n.transform('all subdomains of') const subdomains = this.i18n.transform('subdomains of')
return [ return [
{ {

View File

@@ -59,7 +59,7 @@ import { PublicDomain, PublicDomainService } from './pd.service'
`, `,
styles: ` styles: `
:host { :host {
grid-column: span 7; grid-column: span 4;
} }
`, `,
host: { class: 'g-card' }, host: { class: 'g-card' },

View File

@@ -19,8 +19,8 @@ import { toAuthorityName } from 'src/app/utils/acme'
selector: 'tr[publicDomain]', selector: 'tr[publicDomain]',
template: ` template: `
<td>{{ publicDomain().fqdn }}</td> <td>{{ publicDomain().fqdn }}</td>
<td>{{ publicDomain().gateway?.name }}</td> <td>{{ publicDomain().gateway?.ipInfo?.name }}</td>
<td class="authority">{{ authority() }}</td> <td>{{ authority() }}</td>
<td> <td>
<button <button
tuiIconButton tuiIconButton
@@ -89,9 +89,6 @@ import { toAuthorityName } from 'src/app/utils/acme'
} }
:host-context(tui-root._mobile) { :host-context(tui-root._mobile) {
.authority {
grid-column: span 2;
}
tui-badge { tui-badge {
vertical-align: bottom; vertical-align: bottom;
margin-inline-start: 0.25rem; margin-inline-start: 0.25rem;

View File

@@ -206,12 +206,10 @@ export class PublicDomainService {
} else { } else {
setTimeout( setTimeout(
() => () =>
this.dialog this.dialog.openAlert(
.openAlert( `${fqdn} ${this.i18n.transform('resolves to')} ${wanIp}` as i18nKey,
`${fqdn} ${this.i18n.transform('resolves to')} ${wanIp}` as i18nKey, { label: 'DNS record detected!', appearance: 'positive' },
{ label: 'DNS record detected!', appearance: 'positive' }, ),
)
.subscribe(),
250, 250,
) )
} }
@@ -237,14 +235,14 @@ export class PublicDomainService {
values: data.gateways.reduce<Record<string, string>>( values: data.gateways.reduce<Record<string, string>>(
(obj, gateway) => ({ (obj, gateway) => ({
...obj, ...obj,
[gateway.id]: gateway.name || gateway.ipInfo!.name, [gateway.id]: gateway.ipInfo!.name,
}), }),
{}, {},
), ),
default: '', default: '',
disabled: data.gateways disabled: data.gateways
.filter( .filter(
g => !g.ipInfo!.wanIp || utils.CGNAT.contains(g.ipInfo!.wanIp), g => !g.ipInfo?.wanIp || utils.CGNAT.contains(g.ipInfo?.wanIp),
) )
.map(g => g.id), .map(g => g.id),
})), })),

View File

@@ -82,8 +82,7 @@ type OnionForm = {
`, `,
styles: ` styles: `
:host { :host {
grid-column: span 6; grid-column: span 3;
overflow-wrap: break-word;
} }
`, `,
host: { class: 'g-card' }, host: { class: 'g-card' },

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