316 lines
10 KiB
Bash
Executable File
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
|