Files
movim-rs/install.sh
T
2026-10-24 15:22:36 +07:00

316 lines
10 KiB
Bash
Executable File

#!/bin/sh
# Install Movim as a systemd service (not enabled or started).
#
# From a release tarball (see build-release.sh), installs the prebuilt binary:
# tar xzf movim-*.tar.gz && cd movim-* && ./install.sh
# From the repository, compiles first (as a normal user, not root):
# ./install.sh
# Privileged steps go through sudo when not run as root.
#
# Overridable: MOVIM_USER (default movim, created if missing), PREFIX
# (default /opt/movim), DATA_DIR (default /var/lib/movim), CONF_DIR
# (default /etc/movim), XMPP_DOMAIN, WEB_HOST and MOVIM_PORT (asked when run
# in a terminal and the configuration does not exist yet).
set -eu
MOVIM_USER=${MOVIM_USER:-movim}
PREFIX=${PREFIX:-/opt/movim}
DATA_DIR=${DATA_DIR:-/var/lib/movim}
CONF_DIR=${CONF_DIR:-/etc/movim}
XMPP_DOMAIN=${XMPP_DOMAIN:-}
WEB_HOST=${WEB_HOST:-}
MOVIM_PORT=${MOVIM_PORT:-}
UNIT=/etc/systemd/system/movim.service
MIN_RUSTC=1.94
die() { echo "error: $*" >&2; exit 1; }
# Validators print why a value is refused.
valid_host() {
printf '%s' "$1" | grep -Eq '^([A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?\.)+[A-Za-z]{2,}$' && return 0
echo " not a valid domain name: $1"
return 1
}
port_in_use() {
command -v ss >/dev/null && [ -n "$(ss -Htln "sport = :$1")" ]
}
# The service has no capabilities, so no privileged ports.
valid_port() {
case $1 in ''|*[!0-9]*) echo " not a port number: $1"; return 1 ;; esac
if [ "$1" -lt 1024 ] || [ "$1" -gt 65535 ]; then
echo " port must be between 1024 and 65535"
return 1
fi
if port_in_use "$1"; then
echo " port $1 is already used by another service (ss -tlnp)"
return 1
fi
}
free_port() {
p=$1
while port_in_use "$p"; do p=$((p + 1)); done
echo "$p"
}
# ask VAR "question" default [validator]: prompt on the terminal until valid.
ask() {
check=${4:-valid_host}
eval "current=\${$1}"
[ -z "$current" ] || { "$check" "$current" >&2 || die "invalid $1"; return 0; }
if [ ! -t 0 ]; then
echo " no terminal, $1 defaults to $3 (set $1=... to choose)"
eval "$1=\$3"
return 0
fi
while :; do
printf '%s [%s]: ' "$2" "$3"
read -r answer || die "aborted"
answer=${answer:-$3}
if "$check" "$answer"; then
eval "$1=\$answer"
return 0
fi
done
}
cd "$(dirname "$0")"
[ -d frontend ] && [ -d templates ] || die "run this from the movim-rs repository or an extracted release"
command -v systemctl >/dev/null || die "systemd is required"
if [ "$(id -u)" -eq 0 ]; then
sudo() { "$@"; }
else
command -v sudo >/dev/null || die "sudo is required (or run as root)"
fi
if [ -f Cargo.toml ]; then
[ "$(id -u)" -ne 0 ] || die "do not compile as root; run as a normal user, privileged steps use sudo"
BIN=target/release/movim
elif [ -x movim ]; then
BIN=./movim
echo "==> Prebuilt binary: $(file -b movim 2>/dev/null || echo movim)"
else
die "no Cargo.toml and no prebuilt ./movim"
fi
# --- Questions (before the build, so nobody has to wait for it) -----------
if [ ! -e "$CONF_DIR/movim.env" ]; then
ask XMPP_DOMAIN "XMPP domain users log in with (MOVIM_DOMAIN)" example.org
ask MOVIM_PORT "Local port for Movim (behind nginx)" "$(free_port 8080)" valid_port
fi
if [ -d /etc/nginx/sites-available ] && [ ! -e /etc/nginx/sites-available/movim ]; then
default_web=movim.${XMPP_DOMAIN:-example.org}
ask WEB_HOST "Web hostname serving Movim (nginx server_name)" "$default_web"
fi
# --- Build -----------------------------------------------------------------
build() {
rustup_help="Distribution packages (e.g. Debian's rustc) are too old. Install a current
toolchain for your user with rustup, it does not conflict with apt's rustc:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
then run this script again."
# Prefer a rustup toolchain over the distribution's cargo, even if
# ~/.cargo/bin is not in PATH (non-login shell, Debian's cargo first).
[ ! -x "$HOME/.cargo/bin/cargo" ] || PATH="$HOME/.cargo/bin:$PATH"
command -v cargo >/dev/null || die "cargo not found, need Rust >= $MIN_RUSTC.
$rustup_help"
rustc_version=$(rustc --version | cut -d' ' -f2)
lowest=$(printf '%s\n%s\n' "$MIN_RUSTC" "$rustc_version" | sort -V | head -n1)
if [ "$lowest" != "$MIN_RUSTC" ]; then
if command -v rustup >/dev/null; then
die "rustc $rustc_version is too old, need >= $MIN_RUSTC: rustup update stable && rustup default stable"
fi
die "rustc $rustc_version ($(command -v rustc)) is too old, need >= $MIN_RUSTC.
$rustup_help"
fi
# Linker and C compiler (bundled SQLite, crypto libraries).
command -v cc >/dev/null || die "no C compiler; on Debian: sudo apt install build-essential cmake"
echo "==> Building (rustc $rustc_version)"
cargo build --release --locked
}
[ ! -f Cargo.toml ] || build
# --- Install ---------------------------------------------------------------
echo "==> Installing to $PREFIX (sudo)"
if ! id "$MOVIM_USER" >/dev/null 2>&1; then
nologin=$(command -v nologin || echo /usr/sbin/nologin)
echo " creating system user $MOVIM_USER"
sudo useradd --system --user-group --home-dir "$DATA_DIR" --no-create-home \
--shell "$nologin" "$MOVIM_USER"
fi
group=$(id -gn "$MOVIM_USER")
# Code and assets are owned by root and read-only for the service.
sudo install -d -m 0755 -o root -g root "$PREFIX"
sudo install -m 0755 -o root -g root "$BIN" "$PREFIX/movim.new"
for d in frontend templates; do
sudo rm -rf "$PREFIX/$d.new"
sudo cp -R "$d" "$PREFIX/$d.new"
sudo chown -R root:root "$PREFIX/$d.new"
sudo chmod -R u=rwX,go=rX "$PREFIX/$d.new"
done
# Swap in the new files only once everything is copied.
sudo mv -f "$PREFIX/movim.new" "$PREFIX/movim"
for d in frontend templates; do
sudo rm -rf "$PREFIX/$d.old"
[ ! -e "$PREFIX/$d" ] || sudo mv "$PREFIX/$d" "$PREFIX/$d.old"
sudo mv "$PREFIX/$d.new" "$PREFIX/$d"
sudo rm -rf "$PREFIX/$d.old"
done
# Database, VAPID key: only the service user can read them.
sudo install -d -m 0750 -o "$MOVIM_USER" -g "$group" "$DATA_DIR"
# Configuration: never overwrite an existing one.
sudo install -d -m 0755 -o root -g root "$CONF_DIR"
if [ ! -e "$CONF_DIR/movim.env" ]; then
echo " writing $CONF_DIR/movim.env"
sudo tee "$CONF_DIR/movim.env" >/dev/null <<EOF
# Movim configuration, read by $UNIT
MOVIM_ROOT=$PREFIX
MOVIM_DATA_DIR=$DATA_DIR
# Listen on localhost only, behind a TLS reverse proxy (WebSocket on /ws/).
MOVIM_BIND=127.0.0.1:$MOVIM_PORT
# XMPP domain proposed on the login page.
MOVIM_DOMAIN=$XMPP_DOMAIN
# Comma separated XMPP domains allowed to log in (empty: all).
#MOVIM_XMPP_WHITELIST=example.org
#MOVIM_DISABLE_REGISTRATION=1
#MOVIM_MAX_SESSIONS=0
#MOVIM_LOCALE=en
#MOVIM_SESSION_TIMEOUT_HOURS=168
#MOVIM_BANNER=
#MOVIM_INFO=
EOF
sudo chown root:"$group" "$CONF_DIR/movim.env"
sudo chmod 0640 "$CONF_DIR/movim.env"
else
echo " keeping existing $CONF_DIR/movim.env"
fi
bind=$(sudo sed -n 's/^MOVIM_BIND=//p' "$CONF_DIR/movim.env")
bind=${bind:-127.0.0.1:8080}
# nginx site (Debian layout): written to sites-available, never overwritten,
# never linked into sites-enabled.
nginx_site=
if [ -d /etc/nginx/sites-available ]; then
nginx_site=/etc/nginx/sites-available/movim
if [ ! -e "$nginx_site" ]; then
echo " writing $nginx_site"
sudo tee "$nginx_site" >/dev/null <<EOF
# Movim behind nginx. Add your HTTPS settings (certificates, headers) below.
server {
listen 80;
listen [::]:80;
server_name $WEB_HOST;
return 301 https://\$host\$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name $WEB_HOST;
# TODO: your HTTPS settings, e.g. include snippets/ssl.conf;
# Movim already sends X-Content-Type-Options, X-Frame-Options and
# Referrer-Policy on pages; if your file adds them too, hide Movim's to
# avoid duplicates (proxy_hide_header X-Frame-Options; ... in both
# locations below).
# HTTP file uploads go through Movim, match your XMPP upload limit.
client_max_body_size 100m;
location / {
proxy_pass http://$bind;
proxy_set_header Host \$host;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_set_header X-Forwarded-Host \$host;
}
location /ws {
proxy_pass http://$bind;
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host \$host;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_set_header X-Forwarded-Host \$host;
proxy_read_timeout 1h;
proxy_send_timeout 1h;
}
}
EOF
else
echo " keeping existing $nginx_site"
fi
fi
echo " writing $UNIT"
sudo tee "$UNIT" >/dev/null <<EOF
[Unit]
Description=Movim XMPP web client
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=$MOVIM_USER
Group=$group
EnvironmentFile=$CONF_DIR/movim.env
ExecStart=$PREFIX/movim
Restart=on-failure
RestartSec=5
# Sandboxing: the service only writes to its data directory.
ReadWritePaths=$DATA_DIR
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
NoNewPrivileges=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectClock=yes
ProtectHostname=yes
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
RestrictNamespaces=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
LockPersonality=yes
MemoryDenyWriteExecute=yes
SystemCallArchitectures=native
SystemCallFilter=@system-service
SystemCallFilter=~@privileged @resources
CapabilityBoundingSet=
UMask=0027
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
echo
echo "Installed. Not enabled or started; as admin:"
echo " 1. check $CONF_DIR/movim.env"
echo " 2. sudo systemctl enable --now movim"
if [ -n "$nginx_site" ]; then
echo " 3. add your HTTPS settings to $nginx_site (see the TODO), then:"
echo " sudo ln -s $nginx_site /etc/nginx/sites-enabled/movim"
echo " sudo nginx -t && sudo systemctl reload nginx"
else
echo " 3. put a TLS reverse proxy in front of $bind (WebSocket on /ws/)"
fi
if systemctl is-active --quiet movim; then
echo
echo "movim is running the previous version: sudo systemctl restart movim"
fi