#!/bin/sh # GoToSocial Controller for SecuBox # Manages GoToSocial in a Debian LXC container (glibc for proper bcrypt support) set -e VERSION="0.2.0" GTS_VERSION="0.17.0" # LXC container settings LXC_NAME="gotosocial" LXC_PATH="/srv/lxc" LXC_ROOTFS="$LXC_PATH/$LXC_NAME/rootfs" LXC_CONFIG="$LXC_PATH/$LXC_NAME/config" # Data paths (bind mounted into container) DATA_PATH="/srv/gotosocial" CONFIG_FILE="/etc/config/gotosocial" # Logging log_info() { logger -t gotosocial -p daemon.info "$1"; echo "[INFO] $1"; } log_error() { logger -t gotosocial -p daemon.err "$1"; echo "[ERROR] $1" >&2; } log_warn() { logger -t gotosocial -p daemon.warn "$1"; echo "[WARN] $1"; } # UCI helpers get_config() { local section="$1" local option="$2" local default="$3" uci -q get "gotosocial.${section}.${option}" || echo "$default" } set_config() { uci set "gotosocial.$1.$2=$3" uci commit gotosocial } # LXC helpers has_lxc() { command -v lxc-start >/dev/null 2>&1 && \ command -v lxc-stop >/dev/null 2>&1 } lxc_running() { lxc-info -n "$LXC_NAME" -s 2>/dev/null | grep -q "RUNNING" } lxc_exists() { [ -f "$LXC_CONFIG" ] && [ -d "$LXC_ROOTFS" ] } # Check if GoToSocial is installed (container exists with binary) gts_installed() { [ -x "$LXC_ROOTFS/opt/gotosocial/gotosocial" ] } # Check if GoToSocial is running (LXC container running) gts_running() { lxc_running } # ============================================================================= # LXC CONTAINER MANAGEMENT # ============================================================================= lxc_stop() { if lxc_running; then log_info "Stopping GoToSocial container..." lxc-stop -n "$LXC_NAME" -k 2>/dev/null || true sleep 2 fi } lxc_create_rootfs() { log_info "Creating Debian rootfs for GoToSocial..." mkdir -p "$LXC_PATH/$LXC_NAME" # Download Alpine minirootfs (simple and reliable, glibc not needed since # GoToSocial binary is statically linked) # Actually, use Debian for glibc bcrypt compatibility local arch="x86_64" case "$(uname -m)" in aarch64) arch="aarch64" ;; armv7l) arch="armv7" ;; esac # Use Alpine minirootfs as base local alpine_url="https://dl-cdn.alpinelinux.org/alpine/v3.21/releases/$arch/alpine-minirootfs-3.21.2-$arch.tar.gz" local rootfs_tar="/tmp/alpine-gts.tar.gz" log_info "Downloading Alpine rootfs..." wget -q -O "$rootfs_tar" "$alpine_url" || { log_error "Failed to download Alpine rootfs" return 1 } log_info "Extracting rootfs..." mkdir -p "$LXC_ROOTFS" tar -xzf "$rootfs_tar" -C "$LXC_ROOTFS" || { log_error "Failed to extract rootfs" return 1 } rm -f "$rootfs_tar" # Configure Alpine cat > "$LXC_ROOTFS/etc/resolv.conf" << 'EOF' nameserver 1.1.1.1 nameserver 8.8.8.8 EOF cat > "$LXC_ROOTFS/etc/apk/repositories" << 'EOF' https://dl-cdn.alpinelinux.org/alpine/v3.21/main https://dl-cdn.alpinelinux.org/alpine/v3.21/community EOF # Install gcompat for glibc compatibility (needed for bcrypt) log_info "Installing glibc compatibility layer..." chroot "$LXC_ROOTFS" /bin/sh -c " apk update && apk add --no-cache gcompat libc6-compat sqlite " || log_warn "Could not install gcompat (may not be needed)" mkdir -p "$LXC_ROOTFS/opt/gotosocial" mkdir -p "$LXC_ROOTFS/data" mkdir -p "$LXC_ROOTFS/var/log" log_info "Alpine rootfs with glibc compatibility created successfully" } # Download and install GoToSocial into the container lxc_install_gotosocial() { local version="${1:-$GTS_VERSION}" # GoToSocial uses different arch naming local gts_arch="amd64" case "$(uname -m)" in aarch64) gts_arch="arm64" ;; armv7l) gts_arch="armv7" ;; esac local url="https://codeberg.org/superseriousbusiness/gotosocial/releases/download/v${version}/gotosocial_${version}_linux_${gts_arch}.tar.gz" local tmp_dir="/tmp/gotosocial_install" log_info "Downloading GoToSocial v${version} for ${arch}..." rm -rf "$tmp_dir" mkdir -p "$tmp_dir" cd "$tmp_dir" # Download with curl (handles redirects) or wget curl -L -o gotosocial.tar.gz "$url" 2>/dev/null || \ wget -O gotosocial.tar.gz "$url" || { log_error "Failed to download GoToSocial" return 1 } # Verify download size (should be >10MB) local size=$(stat -c%s gotosocial.tar.gz 2>/dev/null || stat -f%z gotosocial.tar.gz 2>/dev/null || echo 0) if [ "$size" -lt 10000000 ]; then log_error "Downloaded file too small ($size bytes), likely failed" rm -f gotosocial.tar.gz return 1 fi tar -xzf gotosocial.tar.gz # Install into container rootfs cp gotosocial "$LXC_ROOTFS/opt/gotosocial/" chmod +x "$LXC_ROOTFS/opt/gotosocial/gotosocial" # Copy web assets [ -d "web" ] && cp -r web "$LXC_ROOTFS/opt/gotosocial/" rm -rf "$tmp_dir" log_info "GoToSocial v${version} installed in container" } # Create start script inside container lxc_create_start_script() { cat > "$LXC_ROOTFS/opt/start-gotosocial.sh" << 'SCRIPT' #!/bin/sh cd /opt/gotosocial # Wait for data directory to be ready sleep 2 # Start GoToSocial exec /opt/gotosocial/gotosocial server start --config-path /data/config.yaml SCRIPT chmod +x "$LXC_ROOTFS/opt/start-gotosocial.sh" } # Create LXC configuration lxc_create_config() { local port=$(get_config main port "8484") local memory_limit=$(get_config main memory_limit "512M") # LXC arch names local lxc_arch="x86_64" case "$(uname -m)" in aarch64) lxc_arch="aarch64" ;; armv7l) lxc_arch="armhf" ;; esac local mem_bytes=$(echo "$memory_limit" | sed 's/M/000000/;s/G/000000000/') cat > "$LXC_CONFIG" << EOF # GoToSocial LXC Configuration lxc.uts.name = $LXC_NAME lxc.rootfs.path = dir:$LXC_ROOTFS lxc.arch = $lxc_arch # Network: use host network for binding ports lxc.net.0.type = none # Mount data directory lxc.mount.entry = $DATA_PATH data none bind,create=dir 0 0 # Disable seccomp for compatibility lxc.seccomp.profile = # TTY/PTY settings lxc.tty.max = 0 lxc.pty.max = 256 # cgroup v2 memory limit lxc.cgroup2.memory.max = $mem_bytes # Init lxc.init.cmd = /opt/start-gotosocial.sh EOF log_info "LXC config created at $LXC_CONFIG" } # Create data directory structure on host (bind mounted into container) create_data_dir() { log_info "Creating data directories..." mkdir -p "$DATA_PATH"/{storage,web} log_info "Data directories created at $DATA_PATH" } # Generate GoToSocial config (written to DATA_PATH which is bind-mounted as /data in container) generate_config() { local host=$(get_config main host "social.local") local port=$(get_config main port "8484") local protocol=$(get_config main protocol "https") local bind=$(get_config main bind_address "0.0.0.0") local instance_name=$(get_config main instance_name "SecuBox Social") local instance_desc=$(get_config main instance_description "A SecuBox Fediverse instance") local reg_open_val=$(get_config main accounts_registration_open "0") local approval_val=$(get_config main accounts_approval_required "1") # Convert 0/1 to false/true for YAML local reg_open="false" local approval="true" [ "$reg_open_val" = "1" ] && reg_open="true" [ "$approval_val" = "0" ] && approval="false" mkdir -p "$DATA_PATH/storage" # Note: paths are relative to container where DATA_PATH is mounted as /data cat > "$DATA_PATH/config.yaml" < '" } # Uninstall cmd_uninstall() { local keep_data="$1" if [ "$(id -u)" -ne 0 ]; then log_error "Root required" exit 1 fi log_info "Uninstalling GoToSocial..." # Stop container if running lxc_stop # Remove container if [ -d "$LXC_PATH/$LXC_NAME" ]; then rm -rf "$LXC_PATH/$LXC_NAME" log_info "Container removed" fi # Remove data unless --keep-data if [ "$keep_data" != "--keep-data" ]; then rm -rf "$DATA_PATH" log_info "Data removed" else log_info "Data preserved at $DATA_PATH" fi log_info "GoToSocial uninstalled" } # Start GoToSocial (LXC container) cmd_start() { if ! gts_installed; then log_error "GoToSocial not installed. Run 'gotosocialctl install' first." return 1 fi if gts_running; then log_info "GoToSocial container is already running" return 0 fi # Regenerate config in case settings changed generate_config # Regenerate LXC config lxc_create_config log_info "Starting GoToSocial container..." # Start in background lxc-start -n "$LXC_NAME" -d || { log_error "Failed to start GoToSocial container" return 1 } # Wait for startup (WASM compilation takes time on first run) local port=$(get_config main port "8484") local count=0 while [ $count -lt 120 ]; do sleep 2 if curl -s --connect-timeout 1 "http://127.0.0.1:$port/api/v1/instance" >/dev/null 2>&1; then log_info "GoToSocial started" log_info "Web interface available at http://localhost:$port" return 0 fi if ! lxc_running; then log_error "GoToSocial container stopped unexpectedly" log_error "Check: lxc-attach -n gotosocial -- cat /var/log/gotosocial.log" return 1 fi count=$((count + 1)) done log_error "GoToSocial startup timeout. Container still running, may need more time." return 1 } # Stop GoToSocial (LXC container) cmd_stop() { if ! gts_running; then log_info "GoToSocial is not running" return 0 fi log_info "Stopping GoToSocial..." lxc_stop log_info "GoToSocial stopped" } # Restart cmd_restart() { cmd_stop sleep 1 cmd_start } # Reload config cmd_reload() { log_info "Reloading configuration..." generate_config cmd_restart } # Status (JSON output for RPCD) cmd_status() { local installed="false" local running="false" local service_state="false" local host=$(get_config main host "social.example.com") local port=$(get_config main port "8484") local version=$(get_config container version "$GTS_VERSION") local tor_enabled=$(get_config federation tor_enabled "0") local dns_enabled=$(get_config proxy enabled "0") local mesh_enabled=$(get_config mesh announce_to_peers "0") gts_installed && installed="true" gts_running && running="true" # Check if API responds if [ "$running" = "true" ]; then curl -s --connect-timeout 2 "http://127.0.0.1:$port/api/v1/instance" >/dev/null 2>&1 && service_state="true" fi cat </dev/null 2>&1; then echo "API: responding" else echo "API: not responding (may still be starting)" fi else echo "GoToSocial: stopped" if gts_installed; then echo "Container: installed but not running" else echo "Container: not installed" fi return 1 fi } # Shell access to container cmd_shell() { if ! gts_running; then log_error "Container not running. Start with 'gotosocialctl start' first." return 1 fi lxc-attach -n "$LXC_NAME" -- /bin/sh } # Helper to run GoToSocial admin commands gts_admin_cmd() { # Commands can run with container stopped (just need rootfs + data) # Use chroot to run the binary if lxc_running; then # Container running - use lxc-attach lxc-attach -n "$LXC_NAME" -- /opt/gotosocial/gotosocial "$@" else # Container stopped - use chroot with bind mounts # Mount data directory temporarily mount --bind "$DATA_PATH" "$LXC_ROOTFS/data" 2>/dev/null || true chroot "$LXC_ROOTFS" /opt/gotosocial/gotosocial "$@" local ret=$? umount "$LXC_ROOTFS/data" 2>/dev/null || true return $ret fi } # Create user cmd_user_create() { local username="$1" local email="$2" local password="$3" local admin="${4:-false}" [ -z "$username" ] || [ -z "$email" ] && { echo "Usage: gotosocialctl user create [password] [--admin]" return 1 } [ "$3" = "--admin" ] && { admin="true"; password=""; } [ "$4" = "--admin" ] && admin="true" if ! gts_installed; then log_error "GoToSocial is not installed" return 1 fi log_info "Creating user $username..." # Generate random password if not provided [ -z "$password" ] && password=$(openssl rand -base64 12) gts_admin_cmd admin account create \ --username "$username" \ --email "$email" \ --password "$password" \ --config-path "/data/config.yaml" if [ "$admin" = "true" ]; then gts_admin_cmd admin account promote \ --username "$username" \ --config-path "/data/config.yaml" fi # Confirm the user gts_admin_cmd admin account confirm \ --username "$username" \ --config-path "/data/config.yaml" 2>/dev/null || true echo "" echo "User created successfully!" echo "Username: $username" echo "Email: $email" echo "Password: $password" echo "" echo "Please change this password after first login." } # List users (JSON output for RPCD) cmd_users() { local db_path="$DATA_PATH/gotosocial.db" local users="[]" if [ -f "$db_path" ] && command -v sqlite3 >/dev/null; then users=$(sqlite3 -json "$db_path" "SELECT username, created_at as created, CASE WHEN suspended_at IS NULL THEN 0 ELSE 1 END as suspended, CASE WHEN confirmed_at IS NULL THEN 0 ELSE 1 END as confirmed FROM accounts WHERE domain IS NULL OR domain = '';" 2>/dev/null || echo "[]") fi echo "{\"users\":$users}" } # List users (human readable) cmd_user_list() { local db_path="$DATA_PATH/gotosocial.db" if [ -f "$db_path" ] && command -v sqlite3 >/dev/null; then sqlite3 "$db_path" "SELECT username, created_at, suspended_at FROM accounts WHERE domain IS NULL OR domain = '';" 2>/dev/null || { echo "Unable to query database directly. Use the web interface." } else echo "Use the web interface to manage users." echo "URL: https://$(get_config main host)/admin" fi } # Confirm user email cmd_user_confirm() { local username="$1" [ -z "$username" ] && { echo "Usage: gotosocialctl user confirm " return 1 } if ! gts_installed; then log_error "GoToSocial is not installed" return 1 fi gts_admin_cmd admin account confirm \ --username "$username" \ --config-path "/data/config.yaml" log_info "User $username confirmed" } # Reset user password cmd_user_password() { local username="$1" local password="$2" [ -z "$username" ] && { echo "Usage: gotosocialctl user password [new-password]" return 1 } if ! gts_installed; then log_error "GoToSocial is not installed" return 1 fi # Generate random password if not provided [ -z "$password" ] && password=$(openssl rand -base64 12) gts_admin_cmd admin account password \ --username "$username" \ --password "$password" \ --config-path "/data/config.yaml" echo "" echo "Password reset for $username" echo "New password: $password" } # Emancipate - expose via HAProxy cmd_emancipate() { local domain="$1" [ -z "$domain" ] && domain=$(get_config main host) [ -z "$domain" ] || [ "$domain" = "social.example.com" ] && { echo "Usage: gotosocialctl emancipate " echo "Example: gotosocialctl emancipate social.mysite.com" return 1 } local port=$(get_config main port "8484") local lan_ip=$(uci -q get network.lan.ipaddr || echo "192.168.255.1") log_info "Exposing GoToSocial at $domain..." # Update config set_config main host "$domain" set_config proxy enabled "1" set_config proxy vhost_domain "$domain" # Create HAProxy backend uci set haproxy.gotosocial=backend uci set haproxy.gotosocial.name='gotosocial' uci set haproxy.gotosocial.mode='http' uci set haproxy.gotosocial.balance='roundrobin' uci set haproxy.gotosocial.enabled='1' uci set haproxy.gotosocial_srv=server uci set haproxy.gotosocial_srv.backend='gotosocial' uci set haproxy.gotosocial_srv.name='gotosocial' uci set haproxy.gotosocial_srv.address="$lan_ip" uci set haproxy.gotosocial_srv.port="$port" uci set haproxy.gotosocial_srv.weight='100' uci set haproxy.gotosocial_srv.check='1' uci set haproxy.gotosocial_srv.enabled='1' # Create vhost local vhost_name=$(echo "$domain" | tr '.-' '_') uci set haproxy.${vhost_name}=vhost uci set haproxy.${vhost_name}.domain="$domain" uci set haproxy.${vhost_name}.backend='gotosocial' uci set haproxy.${vhost_name}.ssl='1' uci set haproxy.${vhost_name}.ssl_redirect='1' uci set haproxy.${vhost_name}.acme='1' uci set haproxy.${vhost_name}.enabled='1' uci commit haproxy uci commit gotosocial # Regenerate HAProxy config if command -v haproxyctl >/dev/null; then haproxyctl generate /etc/init.d/haproxy reload fi # Regenerate GoToSocial config with new domain generate_config # Restart to apply new config gts_running && cmd_restart log_info "GoToSocial exposed at https://$domain" log_info "SSL certificate will be provisioned automatically" } # Backup cmd_backup() { local backup_path="${1:-/tmp/gotosocial-backup-$(date +%Y%m%d-%H%M%S).tar.gz}" log_info "Creating backup..." # Stop for consistent backup local was_running=false if gts_running; then was_running=true cmd_stop fi tar -czf "$backup_path" -C "$DATA_PATH" . 2>/dev/null || { log_error "Backup failed" [ "$was_running" = "true" ] && cmd_start return 1 } [ "$was_running" = "true" ] && cmd_start log_info "Backup created: $backup_path" ls -lh "$backup_path" } # Restore cmd_restore() { local backup_path="$1" [ -z "$backup_path" ] || [ ! -f "$backup_path" ] && { echo "Usage: gotosocialctl restore " return 1 } log_info "Restoring from $backup_path..." # Stop if running gts_running && cmd_stop # Clear existing data rm -rf "$DATA_PATH"/* # Extract backup tar -xzf "$backup_path" -C "$DATA_PATH" || { log_error "Restore failed" return 1 } log_info "Restore complete" cmd_start } # Federation commands cmd_federation_list() { local port=$(get_config main port "8484") curl -s "http://127.0.0.1:$port/api/v1/instance/peers" 2>/dev/null | jq -r '.[]' 2>/dev/null || { echo "Unable to fetch federation list. Is GoToSocial running?" } } # Show logs (JSON output) cmd_logs() { local lines="${1:-50}" local logs logs=$(logread -e gotosocial 2>/dev/null | tail -n "$lines" | jq -R -s 'split("\n") | map(select(length > 0))' 2>/dev/null || echo "[]") echo "{\"logs\":$logs}" } # Show help cmd_help() { cat < [options] Installation: install [version] Install GoToSocial (default: v$GTS_VERSION) uninstall [--keep-data] Remove GoToSocial update [version] Update to new version Service: start Start GoToSocial container stop Stop GoToSocial container restart Restart GoToSocial reload Reload configuration status Show status (JSON) status-human Show status (human readable) User Management: user create [password] [--admin] Create user user list List users user confirm Confirm user email user password [pwd] Reset user password Exposure: emancipate Expose via HAProxy + SSL Container: shell Open shell in container Backup: backup [path] Backup data restore Restore from backup Federation: federation list List federated instances Other: help Show this help version Show version Examples: gotosocialctl install gotosocialctl start gotosocialctl user create alice alice@example.com --admin gotosocialctl user password alice newpassword123 gotosocialctl emancipate social.mysite.com EOF } # Main case "$1" in install) cmd_install "$2" ;; uninstall) cmd_uninstall "$2" ;; update) cmd_stop lxc_install_gotosocial "${2:-$GTS_VERSION}" cmd_start ;; start) cmd_start ;; stop) cmd_stop ;; restart) cmd_restart ;; reload) cmd_reload ;; status) cmd_status ;; status-human) cmd_status_human ;; users) cmd_users ;; logs) cmd_logs "$2" ;; shell) cmd_shell ;; user) case "$2" in create) cmd_user_create "$3" "$4" "$5" "$6" ;; list) cmd_user_list ;; confirm) cmd_user_confirm "$3" ;; password) cmd_user_password "$3" "$4" ;; *) echo "Usage: gotosocialctl user {create|list|confirm|password}" ;; esac ;; emancipate) cmd_emancipate "$2" ;; backup) cmd_backup "$2" ;; restore) cmd_restore "$2" ;; federation) case "$2" in list) cmd_federation_list ;; *) echo "Usage: gotosocialctl federation {list}" ;; esac ;; version) echo "gotosocialctl v$VERSION (GoToSocial v$GTS_VERSION)" ;; help|--help|-h|"") cmd_help ;; *) echo "Unknown command: $1" cmd_help exit 1 ;; esac