354 lines
13 KiB
Bash
Executable File
354 lines
13 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
TITLE="Federated Computer Core"
|
|
DESCRIPTION="Superpanel, Tailscale, Nextcloud, email, files and Element"
|
|
|
|
# What you entered last time, so running this again only does what's missing
|
|
CORE_CONF="$HOME/.config/coredesktop/core.conf"
|
|
GOA_ACCOUNT="$COREDESKTOP/configure.d/lib/goa-account"
|
|
[ -f "$CORE_CONF" ] && source "$CORE_CONF"
|
|
|
|
# CORE_DOMAIN, CORE_EMAIL and CORE_NAME come from core.conf; the rest follow
|
|
core_names() {
|
|
CORE_USER="${CORE_EMAIL%@*}"
|
|
NEXTCLOUD="https://nextcloud.$CORE_DOMAIN"
|
|
WEBDAV="$NEXTCLOUD/remote.php/dav/files/$CORE_USER"
|
|
MOUNT="/mnt/$CORE_DOMAIN"
|
|
SUPERPANEL_DESKTOP="$HOME/.local/share/applications/core-$CORE_DOMAIN-superpanel.desktop"
|
|
MATRIX="https://matrix.$CORE_DOMAIN"
|
|
ELEMENT_CONFIG="$HOME/.config/Element/config.json"
|
|
}
|
|
|
|
superpanel_done() {
|
|
[ -f "$SUPERPANEL_DESKTOP" ]
|
|
}
|
|
|
|
# Tailscale has no status field naming its login server, so core.conf
|
|
# records that it was set up for this Core
|
|
tailscale_done() {
|
|
[ "$CORE_TAILSCALE" = "$CORE_DOMAIN" ] \
|
|
&& tailscale status --json 2> /dev/null | jq -e '.BackendState == "Running"' > /dev/null
|
|
}
|
|
|
|
nextcloud_done() {
|
|
"$GOA_ACCOUNT" exists owncloud "$CORE_USER@nextcloud.$CORE_DOMAIN" 2> /dev/null
|
|
}
|
|
|
|
email_done() {
|
|
"$GOA_ACCOUNT" exists imap_smtp "$CORE_EMAIL" 2> /dev/null
|
|
}
|
|
|
|
files_done() {
|
|
grep -qs " $MOUNT davfs " /etc/fstab && grep -qs "^$MOUNT " "$HOME/.davfs2/secrets"
|
|
}
|
|
|
|
element_installed() {
|
|
command -v element-desktop > /dev/null
|
|
}
|
|
|
|
# Done when Element isn't installed, as there's nothing to set up
|
|
element_done() {
|
|
element_installed || return 0
|
|
jq -e --arg url "$MATRIX" \
|
|
'.default_server_config["m.homeserver"].base_url | rtrimstr("/") == $url' \
|
|
"$ELEMENT_CONFIG" &> /dev/null
|
|
}
|
|
|
|
is_configured() {
|
|
[ -n "$CORE_DOMAIN" ] || return 1
|
|
core_names
|
|
superpanel_done && tailscale_done && nextcloud_done && email_done && files_done && element_done
|
|
}
|
|
|
|
save_conf() {
|
|
mkdir -p "$(dirname "$CORE_CONF")"
|
|
{
|
|
printf 'CORE_DOMAIN=%q\n' "$CORE_DOMAIN"
|
|
printf 'CORE_EMAIL=%q\n' "$CORE_EMAIL"
|
|
printf 'CORE_NAME=%q\n' "$CORE_NAME"
|
|
printf 'CORE_TAILSCALE=%q\n' "$CORE_TAILSCALE"
|
|
} > "$CORE_CONF"
|
|
}
|
|
|
|
# Basic auth header for curl, read from stdin (-H @-) so the password isn't
|
|
# in the process list
|
|
auth_header() {
|
|
printf 'Authorization: Basic %s\n' "$(printf '%s:%s' "$1" "$2" | base64 -w0)"
|
|
}
|
|
|
|
# Create a Nextcloud app password for the Online Accounts account and the
|
|
# files mount, so they don't keep your Core password. It shows in Nextcloud
|
|
# under Settings > Security as "CoreDesktop on <this computer>", where it can
|
|
# be revoked. Returns 2 if Nextcloud says the password is wrong.
|
|
nextcloud_app_password() {
|
|
local response code
|
|
response="$(auth_header "$CORE_USER" "$PASSWORD" | curl -s -w '\n%{http_code}' -H @- \
|
|
-H 'OCS-APIRequest: true' -A "CoreDesktop on $HOSTNAME" \
|
|
"$NEXTCLOUD/ocs/v2.php/core/getapppassword?format=json")"
|
|
code="${response##*$'\n'}"
|
|
case "$code" in
|
|
200) APP_PASSWORD="$(jq -r '.ocs.data.apppassword // empty' <<< "${response%$'\n'*}")"
|
|
[ -n "$APP_PASSWORD" ] ;;
|
|
401) return 2 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
desktop_entry() {
|
|
local file="$1" name="$2" comment="$3" url="$4" icon="$5"
|
|
mkdir -p "$(dirname "$file")"
|
|
cat > "$file" << EOF
|
|
[Desktop Entry]
|
|
Type=Application
|
|
Name=$name
|
|
Comment=$comment
|
|
Exec=xdg-open $url
|
|
Icon=$icon
|
|
Categories=Network;
|
|
EOF
|
|
}
|
|
|
|
setup_superpanel() {
|
|
local url="https://superpanel.$CORE_DOMAIN"
|
|
local icon="$HOME/.local/share/icons/core-$CORE_DOMAIN-superpanel.png"
|
|
|
|
# The login page carries Superpanel's icon inline
|
|
mkdir -p "$(dirname "$icon")"
|
|
if ! curl -s "$url/" | grep -o 'rel="icon"[^>]*base64,[^"]*' | head -n 1 \
|
|
| sed 's/.*base64,//' | base64 -d > "$icon" 2> /dev/null || [ ! -s "$icon" ]; then
|
|
rm -f "$icon"
|
|
icon="applications-internet"
|
|
fi
|
|
desktop_entry "$SUPERPANEL_DESKTOP" "Superpanel" "Manage your Federated Computer Core ($CORE_DOMAIN)" "$url" "$icon"
|
|
echo "Added Superpanel to your applications."
|
|
}
|
|
|
|
setup_tailscale() {
|
|
# --reset and --force-reauth move the machine from any other tailnet;
|
|
# --operator lets you, and the Tailscale GNOME extension, use it without sudo
|
|
if ! systemctl is-active -q tailscaled; then
|
|
sudo systemctl enable --now tailscaled || return 1
|
|
fi
|
|
echo "Open the link below to sign in to your Core's Tailscale network."
|
|
sudo tailscale up --reset --force-reauth --login-server "https://headscale.$CORE_DOMAIN" \
|
|
--accept-routes --operator="$USER" || return 1
|
|
CORE_TAILSCALE="$CORE_DOMAIN"
|
|
save_conf
|
|
}
|
|
|
|
setup_nextcloud() {
|
|
[ -n "$APP_PASSWORD" ] || { echo "This needs your Core password, and Nextcloud didn't accept it."; return 1; }
|
|
"$GOA_ACCOUNT" add << EOF || return 1
|
|
$(jq -n --arg user "$CORE_USER" --arg host "nextcloud.$CORE_DOMAIN" --arg url "$NEXTCLOUD" \
|
|
--arg password "$APP_PASSWORD" '{
|
|
provider: "owncloud",
|
|
identity: $user,
|
|
presentation_identity: "\($user)@\($host)",
|
|
credentials: {password: $password},
|
|
details: {
|
|
Uri: "\($url)/remote.php/webdav",
|
|
FilesEnabled: "true",
|
|
CalendarEnabled: "true",
|
|
CalDavUri: "\($url)/remote.php/dav",
|
|
ContactsEnabled: "true",
|
|
CardDavUri: "\($url)/remote.php/dav",
|
|
AcceptSslErrors: "false"
|
|
}}')
|
|
EOF
|
|
echo "Added Nextcloud to Online Accounts (calendar, contacts and files)."
|
|
}
|
|
|
|
# The mail server publishes its settings (Thunderbird's autoconfig format).
|
|
# Prints: imap_host imap_port imap_socket smtp_host smtp_port smtp_socket username
|
|
mail_settings() {
|
|
curl -sf --max-time 15 \
|
|
"https://autoconfig.$CORE_DOMAIN/mail/config-v1.1.xml?emailaddress=$CORE_EMAIL" \
|
|
| python3 -I -c '
|
|
import sys, xml.etree.ElementTree as ET
|
|
provider = ET.parse(sys.stdin).getroot().find("emailProvider")
|
|
imap = provider.find("incomingServer[@type=\"imap\"]")
|
|
smtp = provider.find("outgoingServer[@type=\"smtp\"]")
|
|
print(*(s.findtext(f) for s in (imap, smtp) for f in ("hostname", "port", "socketType")),
|
|
imap.findtext("username"))'
|
|
}
|
|
|
|
# Online Accounts takes host:port, and only the host for the usual port
|
|
host_port() {
|
|
if [ "$2" = "$3" ]; then echo "$1"; else echo "$1:$2"; fi
|
|
}
|
|
|
|
setup_email() {
|
|
[ -n "$PASSWORD" ] || { echo "This needs your Core password."; return 1; }
|
|
local imap_host imap_port imap_socket smtp_host smtp_port smtp_socket username
|
|
if ! read -r imap_host imap_port imap_socket smtp_host smtp_port smtp_socket username < <(mail_settings) \
|
|
|| [ -z "$smtp_socket" ]; then
|
|
echo "The mail server didn't give its settings; using mail.$CORE_DOMAIN with the usual ports."
|
|
imap_host="mail.$CORE_DOMAIN" imap_port=993 imap_socket=SSL
|
|
smtp_host="mail.$CORE_DOMAIN" smtp_port=587 smtp_socket=STARTTLS
|
|
username="%EMAILADDRESS%"
|
|
fi
|
|
case "$username" in
|
|
%EMAILLOCALPART%) username="$CORE_USER" ;;
|
|
*) username="$CORE_EMAIL" ;;
|
|
esac
|
|
[ "$imap_socket" = SSL ] && imap_std=993 || imap_std=143
|
|
[ "$smtp_socket" = SSL ] && smtp_std=465 || smtp_std=587
|
|
|
|
"$GOA_ACCOUNT" add << EOF || return 1
|
|
$(jq -n --arg email "$CORE_EMAIL" --arg name "$CORE_NAME" --arg user "$username" \
|
|
--arg password "$PASSWORD" \
|
|
--arg imap "$(host_port "$imap_host" "$imap_port" "$imap_std")" --arg imap_socket "$imap_socket" \
|
|
--arg smtp "$(host_port "$smtp_host" "$smtp_port" "$smtp_std")" --arg smtp_socket "$smtp_socket" '{
|
|
provider: "imap_smtp",
|
|
identity: $email,
|
|
presentation_identity: $email,
|
|
credentials: {"imap-password": $password, "smtp-password": $password},
|
|
details: {
|
|
Enabled: "true",
|
|
EmailAddress: $email,
|
|
Name: $name,
|
|
ImapHost: $imap,
|
|
ImapUserName: $user,
|
|
ImapUseSsl: (if $imap_socket == "SSL" then "true" else "false" end),
|
|
ImapUseTls: (if $imap_socket == "STARTTLS" then "true" else "false" end),
|
|
ImapAcceptSslErrors: "false",
|
|
SmtpHost: $smtp,
|
|
SmtpUseAuth: "true",
|
|
SmtpUserName: $user,
|
|
SmtpAuthLogin: "false",
|
|
SmtpAuthPlain: "true",
|
|
SmtpUseSsl: (if $smtp_socket == "SSL" then "true" else "false" end),
|
|
SmtpUseTls: (if $smtp_socket == "STARTTLS" then "true" else "false" end),
|
|
SmtpAcceptSslErrors: "false"
|
|
}}')
|
|
EOF
|
|
echo "Added $CORE_EMAIL to Online Accounts."
|
|
}
|
|
|
|
# Mount your Nextcloud files at /mnt/<domain> with davfs2. You can mount it
|
|
# without sudo once it's in /etc/fstab with "user" and you're in Arch's davfs2
|
|
# group (network). It's mounted now and each time you log in.
|
|
setup_files() {
|
|
[ -n "$APP_PASSWORD" ] || { echo "This needs your Core password, and Nextcloud didn't accept it."; return 1; }
|
|
local secret
|
|
|
|
sudo install -d -o "$USER" -g "$(id -gn)" "$MOUNT" || return 1
|
|
if ! grep -qs " $MOUNT davfs " /etc/fstab; then
|
|
echo "$WEBDAV $MOUNT davfs rw,user,noauto,uid=$USER,_netdev 0 0" | sudo tee -a /etc/fstab > /dev/null || return 1
|
|
fi
|
|
if ! id -nG "$USER" | grep -qw network; then
|
|
sudo usermod -aG network "$USER" || return 1
|
|
fi
|
|
|
|
# davfs2 only reads this file if no one else can
|
|
mkdir -p -m 700 "$HOME/.davfs2"
|
|
touch "$HOME/.davfs2/secrets"
|
|
chmod 600 "$HOME/.davfs2/secrets"
|
|
secret="${APP_PASSWORD//\\/\\\\}"
|
|
secret="${secret//\"/\\\"}"
|
|
sed -i "\\#^$MOUNT #d" "$HOME/.davfs2/secrets"
|
|
printf '%s %s "%s"\n' "$MOUNT" "$CORE_USER" "$secret" >> "$HOME/.davfs2/secrets"
|
|
|
|
mkdir -p "$HOME/.config/autostart"
|
|
cat > "$HOME/.config/autostart/core-$CORE_DOMAIN-files.desktop" << EOF
|
|
[Desktop Entry]
|
|
Type=Application
|
|
Name=Nextcloud files ($CORE_DOMAIN)
|
|
Comment=Mount your Nextcloud files at $MOUNT
|
|
Exec=sh -c 'mountpoint -q $MOUNT || mount $MOUNT'
|
|
X-GNOME-Autostart-enabled=true
|
|
NoDisplay=true
|
|
EOF
|
|
|
|
# Shown in the Files sidebar as the domain
|
|
local bookmarks="$HOME/.config/gtk-3.0/bookmarks"
|
|
mkdir -p "$(dirname "$bookmarks")"
|
|
grep -qs "^file://$MOUNT " "$bookmarks" || echo "file://$MOUNT $CORE_DOMAIN" >> "$bookmarks"
|
|
|
|
# A new session through sudo picks up the network group now, without
|
|
# logging out and in again (Arch has no sg)
|
|
if ! mountpoint -q "$MOUNT"; then
|
|
sudo -u "$USER" mount "$MOUNT" || return 1
|
|
fi
|
|
echo "Your Nextcloud files are at $MOUNT (\"$CORE_DOMAIN\" in the Files sidebar)."
|
|
}
|
|
|
|
# Element Desktop reads ~/.config/Element/config.json over its built-in
|
|
# settings. Pointing it at the Core's Matrix server means you only choose
|
|
# "Sign in" (the Core offers single sign-on through Authelia). The Core's Matrix
|
|
# server is named after its address, matrix.<domain>, and says where its API is.
|
|
setup_element() {
|
|
local base_url config
|
|
base_url="$(curl -sf --max-time 15 "$MATRIX/.well-known/matrix/client" \
|
|
| jq -r '.["m.homeserver"].base_url // empty' 2> /dev/null)"
|
|
base_url="${base_url:-$MATRIX}"
|
|
|
|
mkdir -p "$(dirname "$ELEMENT_CONFIG")"
|
|
config="$(cat "$ELEMENT_CONFIG" 2> /dev/null)"
|
|
[ -n "$config" ] || config='{}'
|
|
jq --arg url "${base_url%/}" --arg name "matrix.$CORE_DOMAIN" \
|
|
'.default_server_config = {"m.homeserver": {base_url: $url, server_name: $name}}' \
|
|
<<< "$config" > "$ELEMENT_CONFIG.tmp" && mv -f "$ELEMENT_CONFIG.tmp" "$ELEMENT_CONFIG" || return 1
|
|
echo "Element will use $MATRIX the next time it starts."
|
|
}
|
|
|
|
# Run a step unless it's done; a failed step doesn't stop the ones after it
|
|
step() {
|
|
local name="$1" title="$2"
|
|
if "${name}_done"; then
|
|
echo " $title: already set up"
|
|
return 0
|
|
fi
|
|
if ! confirm_yes "Set up $title?"; then
|
|
return 0
|
|
fi
|
|
"setup_$name" || { echo -e "\e[31m$title was not set up.\e[0m"; STATUS=1; }
|
|
}
|
|
|
|
run_configure() {
|
|
local url
|
|
STATUS=0
|
|
|
|
read -erp "Your Superpanel's address (e.g. https://superpanel.example.com): " \
|
|
-i "${CORE_DOMAIN:+https://superpanel.$CORE_DOMAIN}" url
|
|
url="${url#*://}"
|
|
url="${url%%/*}"
|
|
CORE_DOMAIN="${url#superpanel.}"
|
|
if [ -z "$CORE_DOMAIN" ] || ! curl -s -o /dev/null --max-time 15 "https://superpanel.$CORE_DOMAIN/"; then
|
|
echo "Couldn't reach https://superpanel.$CORE_DOMAIN."
|
|
return 1
|
|
fi
|
|
|
|
read -erp "Your Core email: " -i "${CORE_EMAIL:-$USER@$CORE_DOMAIN}" CORE_EMAIL
|
|
read -erp "Your name, as shown on email you send: " \
|
|
-i "${CORE_NAME:-$(getent passwd "$USER" | cut -d: -f5 | cut -d, -f1)}" CORE_NAME
|
|
[[ "$CORE_EMAIL" == *@* ]] || { echo "That isn't an email address."; return 1; }
|
|
core_names
|
|
save_conf
|
|
|
|
# Nextcloud, email and files need your Core password; Nextcloud checks it
|
|
if ! nextcloud_done || ! email_done || ! files_done; then
|
|
local tries
|
|
for tries in 1 2 3; do
|
|
read -rsp "Your Core password: " PASSWORD
|
|
echo
|
|
nextcloud_app_password
|
|
case $? in
|
|
0) break ;;
|
|
2) echo "Nextcloud says that password is wrong."; PASSWORD="" ;;
|
|
*) echo "Couldn't reach Nextcloud at $NEXTCLOUD; trying the password for email anyway."; break ;;
|
|
esac
|
|
done
|
|
fi
|
|
|
|
echo
|
|
step superpanel "Superpanel in your applications"
|
|
step tailscale "Tailscale with your Core (headscale.$CORE_DOMAIN)"
|
|
step nextcloud "Nextcloud in Online Accounts"
|
|
step email "Email in Online Accounts"
|
|
step files "Nextcloud files at $MOUNT"
|
|
element_installed && step element "Element with your Core's Matrix server"
|
|
unset PASSWORD APP_PASSWORD
|
|
return $STATUS
|
|
}
|