diff --git a/scripts/INSTALL.md b/scripts/INSTALL.md index 56118c7..36326bd 100644 --- a/scripts/INSTALL.md +++ b/scripts/INSTALL.md @@ -282,3 +282,20 @@ Notes: - Offline frontend libraries are the default. Use `--libs online` only when CDN loading is preferred. - Local auth is configured directly by the installer. External auth providers require a trusted reverse proxy setup; see `auth.md`. - Reverse proxy mode enables `PYTORRENT_PROXY_FIX_ENABLE`, secure cookies and CORS/API origins for the HTTPS domains plus localhost/local IP origins. + +### rTorrent SCGI over Unix socket + +Stack installers use TCP SCGI by default (`scgi://127.0.0.1:5000`). To configure rTorrent with a Unix socket and expose it to pyTorrent through `rtorrent-scgi-proxy`, run one of: + +```bash +sudo bash scripts/install_stack.sh --scgi-unix-socket +sudo bash scripts/install_stack.sh --rtorrent-socket /run/rtorrent/rtorrent.sock +``` + +Equivalent environment variables: + +```bash +RTORRENT_SCGI_BACKEND=unix RTORRENT_SCGI_SOCKET=/run/rtorrent/rtorrent.sock sudo bash scripts/install_stack.sh +``` + +When socket mode is selected, the installer adds the proxy user to the rTorrent group and grants the proxy systemd unit supplementary group access so it can open the socket. diff --git a/scripts/install_pytorrent_only.sh b/scripts/install_pytorrent_only.sh index 39f5645..235b39c 100755 --- a/scripts/install_pytorrent_only.sh +++ b/scripts/install_pytorrent_only.sh @@ -33,6 +33,7 @@ RT_PROXY_ALLOW_NET="${RTORRENT_SCGI_PROXY_ALLOW_NET:-127.0.0.1}" RT_PROXY_TARGET_NETWORK_EXPLICIT="${RTORRENT_SCGI_PROXY_TARGET_NETWORK+x}" RT_PROXY_TARGET_NETWORK="${RTORRENT_SCGI_PROXY_TARGET_NETWORK:-tcp}" RT_PROXY_TARGET_ADDRESS="${RTORRENT_SCGI_PROXY_TARGET_ADDRESS:-127.0.0.1:5000}" +RT_PROXY_EXTRA_GROUPS="${RTORRENT_SCGI_PROXY_EXTRA_GROUPS:-}" RT_PROXY_BINARY_URL="${RTORRENT_SCGI_PROXY_BINARY_URL:-https://git.linuxiarz.pl/gru/rtorrent-scgi-proxy/raw/branch/master/dist/rtorrent-scgi-proxy-linux-amd64}" RT_PROXY_TARGET_URI="${RTORRENT_SCGI_PROXY_TARGET_URI:-/RPC2}" ASSUME_YES=0 @@ -75,6 +76,7 @@ Options: --proxy-allow-net VALUE SCGI proxy ALLOW_NET. Default: 127.0.0.1. --proxy-target-network tcp|unix --proxy-target-address VALUE + --proxy-extra-groups CSV Extra system groups for rtorrent-scgi-proxy, useful for Unix socket access. --skip-profile Do not create/update pyTorrent rTorrent profile. -h, --help Show this help. @@ -160,6 +162,7 @@ parse_args() { --proxy-allow-net) RT_PROXY_ALLOW_NET="$2"; shift 2 ;; --proxy-target-network) RT_PROXY_TARGET_NETWORK="$2"; RT_PROXY_TARGET_NETWORK_EXPLICIT=1; shift 2 ;; --proxy-target-address) RT_PROXY_TARGET_ADDRESS="$2"; shift 2 ;; + --proxy-extra-groups) RT_PROXY_EXTRA_GROUPS="$2"; shift 2 ;; --skip-profile) SKIP_PROFILE=1; shift ;; -h|--help) usage; exit 0 ;; *) fail "Unknown option: $1" ;; @@ -212,7 +215,9 @@ ask_configuration() { prompt PROFILE_NAME "pyTorrent profile name" "Local rTorrent" if [[ -n "${RTORRENT_SOCKET}" ]]; then - INSTALL_SCGI_PROXY="${INSTALL_SCGI_PROXY:-ask}" + INSTALL_SCGI_PROXY="yes" + RT_PROXY_TARGET_NETWORK="unix" + RT_PROXY_TARGET_ADDRESS="${RTORRENT_SOCKET}" fi if [[ "${INSTALL_SCGI_PROXY}" == "ask" ]]; then prompt INSTALL_SCGI_PROXY "Install rtorrent-scgi-proxy for Unix socket backend? yes/no" "no" @@ -531,6 +536,46 @@ SERVICE systemctl restart "${SERVICE_NAME}" } + +grant_scgi_proxy_socket_access() { + [[ "${INSTALL_SCGI_PROXY}" == "yes" ]] || return 0 + [[ "${RT_PROXY_TARGET_NETWORK}" == "unix" ]] || return 0 + local socket_path="${RT_PROXY_TARGET_ADDRESS}" + [[ -n "${socket_path}" ]] || return 0 + + local groups="${RT_PROXY_EXTRA_GROUPS}" + if [[ -S "${socket_path}" ]]; then + local socket_group + socket_group="$(stat -c '%G' "${socket_path}" 2>/dev/null || true)" + if [[ -n "${socket_group}" && "${socket_group}" != "UNKNOWN" ]]; then + groups="${groups:+${groups},}${socket_group}" + chmod g+rw "${socket_path}" 2>/dev/null || true + fi + fi + if [[ -n "${RTORRENT_USER:-}" ]] && getent group "${RTORRENT_USER}" >/dev/null 2>&1; then + groups="${groups:+${groups},}${RTORRENT_USER}" + fi + if [[ -z "${groups}" ]] && getent group rtorrent >/dev/null 2>&1; then + groups="rtorrent" + fi + + if [[ -n "${groups}" ]]; then + local normalized="" group + IFS=',' read -r -a _groups <<< "${groups}" + for group in "${_groups[@]}"; do + group="$(printf '%s' "${group}" | xargs)" + [[ -n "${group}" ]] || continue + getent group "${group}" >/dev/null 2>&1 || continue + usermod -aG "${group}" "${RT_PROXY_USER}" || true + case ",${normalized}," in + *,${group},*) ;; + *) normalized="${normalized:+${normalized},}${group}" ;; + esac + done + RT_PROXY_EXTRA_GROUPS="${normalized}" + fi +} + install_scgi_proxy() { # Note: The proxy exposes a TCP SCGI endpoint for pyTorrent when rTorrent listens on a Unix socket. [[ "${INSTALL_SCGI_PROXY}" == "yes" ]] || return 0 @@ -540,6 +585,7 @@ install_scgi_proxy() { [[ -x "${shell_path}" ]] || shell_path="/usr/bin/nologin" useradd --system --no-create-home --shell "${shell_path}" "${RT_PROXY_USER}" fi + grant_scgi_proxy_socket_access curl -fL "${RT_PROXY_BINARY_URL}" -o /usr/local/bin/rtorrent-scgi-proxy chmod 0755 /usr/local/bin/rtorrent-scgi-proxy cat > /etc/rtorrent-scgi-proxy.env <= 3: + runtime_directory = parts[2] + write_service(DEFAULT_SERVICE_PATH, "/usr/local/bin/rtorrent", ":".join(runtime_lib_dirs), runtime_directory=runtime_directory) enable_service(args.user, debug=args.debug) print(f"\nService status hint: systemctl status rtorrent@{args.user}.service") diff --git a/scripts/stack_installers/install_rtorrent_rhel.py b/scripts/stack_installers/install_rtorrent_rhel.py index 70ef726..9b5e2db 100755 --- a/scripts/stack_installers/install_rtorrent_rhel.py +++ b/scripts/stack_installers/install_rtorrent_rhel.py @@ -536,7 +536,10 @@ def install_symlinks(rtorrent_install, libtorrent_install, xmlrpc_install=None, run(["ldconfig"], debug=debug) -def write_service(service_path, binary_path, runtime_lib_dirs): +def write_service(service_path, binary_path, runtime_lib_dirs, *, runtime_directory=None): + runtime_directory_lines = "" + if runtime_directory: + runtime_directory_lines = f"RuntimeDirectory={runtime_directory}\nRuntimeDirectoryMode=0750\n" service_content = f"""[Unit] Description=rTorrent for %I | https://git.linuxiarz.pl/gru/tools_scripts/_edit/master/install_rtorrent.py After=network.target @@ -554,7 +557,7 @@ TimeoutStopSec=300 Restart=always RestartSec=3 LimitNOFILE=1048576 -Environment=LD_LIBRARY_PATH={runtime_lib_dirs} +{runtime_directory_lines}Environment=LD_LIBRARY_PATH={runtime_lib_dirs} [Install] WantedBy=multi-user.target @@ -579,7 +582,11 @@ def rtorrent_bind_address_directive(rtorrent_ref, rtorrent_version=None): return "network.bind_address.set" return "network.bind_address.ipv4.set" -def build_rtorrent_config_content(username, scgi_port, torrent_port, bind_address_directive): +def build_rtorrent_config_content(username, scgi_port, torrent_port, bind_address_directive, scgi_backend="tcp", scgi_socket="/run/rtorrent/rtorrent.sock"): + if scgi_backend == "unix": + scgi_line = f"network.scgi.open_local = {scgi_socket}\nexecute.nothrow = chmod,660,{scgi_socket}" + else: + scgi_line = f"network.scgi.open_port = 127.0.0.1:{scgi_port}" return f""" ## https://git.linuxiarz.pl/gru/tools_scripts/_edit/master/install_rtorrent.py # Generated by install_rtorrent.py @@ -588,7 +595,7 @@ directory.default.set = /home/{username}/downloads session.path.set = /home/{username}/.session encoding.add = UTF-8 -network.scgi.open_port = 127.0.0.1:{scgi_port} +{scgi_line} network.port_range.set = {torrent_port}-{torrent_port} network.port_random.set = no {bind_address_directive} = 0.0.0.0 @@ -633,9 +640,9 @@ pieces.hash.on_completion.set = 0 """.lstrip() -def write_rtorrent_config(user_home, username, scgi_port, torrent_port, bind_address_directive, *, force_config=False): +def write_rtorrent_config(user_home, username, scgi_port, torrent_port, bind_address_directive, *, scgi_backend="tcp", scgi_socket="/run/rtorrent/rtorrent.sock", force_config=False): config_path = Path(user_home) / ".rtorrent.rc" - config_content = build_rtorrent_config_content(username, scgi_port, torrent_port, bind_address_directive) + config_content = build_rtorrent_config_content(username, scgi_port, torrent_port, bind_address_directive, scgi_backend=scgi_backend, scgi_socket=scgi_socket) if config_path.exists() and not force_config: print(f"Config already exists: {config_path}") @@ -809,7 +816,9 @@ def build_parser(): parser.add_argument("--user", default=DEFAULT_USER, help=f"System user for the service (default: {DEFAULT_USER})") parser.add_argument("--group", default=DEFAULT_GROUP, help=f"System group for the service (default: {DEFAULT_GROUP})") parser.add_argument("--home", default=DEFAULT_HOME, help=f"Home directory for the service user (default: {DEFAULT_HOME})") - parser.add_argument("--scgi-port", type=int, default=DEFAULT_SCGI_PORT, help=f"SCGI listen port for rTorrent XMLRPC/SCGI (default: {DEFAULT_SCGI_PORT})") + parser.add_argument("--scgi-port", type=int, default=DEFAULT_SCGI_PORT, help=f"SCGI TCP listen port for rTorrent XMLRPC/SCGI (default: {DEFAULT_SCGI_PORT})") + parser.add_argument("--scgi-backend", choices=["tcp", "unix"], default=os.getenv("RTORRENT_SCGI_BACKEND", "tcp"), help="rTorrent SCGI backend to write in .rtorrent.rc (default: tcp).") + parser.add_argument("--scgi-socket", default=os.getenv("RTORRENT_SCGI_SOCKET", "/run/rtorrent/rtorrent.sock"), help="Unix socket path used when --scgi-backend unix is selected.") parser.add_argument("--torrent-port", type=int, default=DEFAULT_TORRENT_PORT, help=f"Incoming BitTorrent listen port (default: {DEFAULT_TORRENT_PORT})") parser.add_argument("--force-config", action="store_true", help="Overwrite existing ~/.rtorrent.rc. By default, existing config is left unchanged and the proposed changes are printed.") parser.add_argument("--only-build", action="store_true", help="Only build and install libtorrent/rTorrent under /opt. Skip user, config and systemd.") @@ -861,7 +870,10 @@ def main(): print(" - skip service user, config and systemd setup") else: print(f" - configure systemd service for user '{args.user}'") - print(f" - use SCGI port {args.scgi_port} and torrent port {args.torrent_port}") + if args.scgi_backend == "unix": + print(f" - use SCGI Unix socket {args.scgi_socket} and torrent port {args.torrent_port}") + else: + print(f" - use SCGI port {args.scgi_port} and torrent port {args.torrent_port}") if not prompt_yes_no("Continue?", default=True, assume_yes=args.yes): print("Aborted by user.") @@ -900,7 +912,7 @@ def main(): prepare_user_dirs(args.home, args.user) bind_address_directive = rtorrent_bind_address_directive(args.rtorrent_ref, rtorrent_version) print(f"Using rTorrent bind address directive: {bind_address_directive}") - write_rtorrent_config(args.home, args.user, args.scgi_port, args.torrent_port, bind_address_directive, force_config=args.force_config) + write_rtorrent_config(args.home, args.user, args.scgi_port, args.torrent_port, bind_address_directive, scgi_backend=args.scgi_backend, scgi_socket=args.scgi_socket, force_config=args.force_config) runtime_lib_dirs = [f"{libtorrent_install}/lib"] if args.rpc_backend == "xmlrpc-c" and xmlrpc_install: runtime_lib_dirs.append(f"{xmlrpc_install}/lib") @@ -908,7 +920,12 @@ def main(): runtime_lib_dirs.append(f"{curl_install}/lib") if cares_install: runtime_lib_dirs.append(f"{cares_install}/lib") - write_service(DEFAULT_SERVICE_PATH, "/usr/local/bin/rtorrent", ":".join(runtime_lib_dirs)) + runtime_directory = None + if args.scgi_backend == "unix" and args.scgi_socket.startswith("/run/"): + parts = Path(args.scgi_socket).parts + if len(parts) >= 3: + runtime_directory = parts[2] + write_service(DEFAULT_SERVICE_PATH, "/usr/local/bin/rtorrent", ":".join(runtime_lib_dirs), runtime_directory=runtime_directory) enable_service(args.user, debug=args.debug) print(f"\nService status hint: systemctl status rtorrent@{args.user}.service") diff --git a/scripts/stack_installers/install_stack_arch.sh b/scripts/stack_installers/install_stack_arch.sh index 8e5dd9e..286e603 100755 --- a/scripts/stack_installers/install_stack_arch.sh +++ b/scripts/stack_installers/install_stack_arch.sh @@ -16,6 +16,10 @@ RTORRENT_USER="${RTORRENT_USER:-rtorrent}" RTORRENT_HOME="${RTORRENT_HOME:-/home/${RTORRENT_USER}}" RTORRENT_BASE_DIR="${RTORRENT_BASE_DIR:-/opt/rtorrent_build}" RTORRENT_SCGI_PORT="${RTORRENT_SCGI_PORT:-5000}" +RTORRENT_SCGI_BACKEND="${RTORRENT_SCGI_BACKEND:-tcp}" +RTORRENT_SCGI_SOCKET="${RTORRENT_SCGI_SOCKET:-/run/rtorrent/rtorrent.sock}" +RTORRENT_SCGI_PROXY_LISTEN="${RTORRENT_SCGI_PROXY_LISTEN:-127.0.0.1:5050}" +RTORRENT_SCGI_PROXY_TOKEN="${RTORRENT_SCGI_PROXY_TOKEN:-}" RTORRENT_TORRENT_PORT="${RTORRENT_TORRENT_PORT:-51300}" RTORRENT_REF="${RTORRENT_REF:-v0.16.11}" LIBTORRENT_REF="${LIBTORRENT_REF:-v0.16.11}" @@ -25,10 +29,28 @@ PYTORRENT_BASE_URL="${PYTORRENT_BASE_URL:-http://127.0.0.1:${PYTORRENT_PORT}}" PYTORRENT_PROFILE_NAME="${PYTORRENT_PROFILE_NAME:-Local rTorrent}" PYTORRENT_API_TOKEN="${PYTORRENT_API_TOKEN:-}" PYTORRENT_SERVICE_NAME="${PYTORRENT_SERVICE_NAME:-pytorrent}" -PYTORRENT_RTORRENT_SCGI_URL="${PYTORRENT_RTORRENT_SCGI_URL:-scgi://127.0.0.1:${RTORRENT_SCGI_PORT}}" +PYTORRENT_RTORRENT_SCGI_URL="${PYTORRENT_RTORRENT_SCGI_URL:-}" RTORRENT_BUILD_FROM_SOURCE="${RTORRENT_BUILD_FROM_SOURCE:-0}" RTORRENT_FORCE_CONFIG="${RTORRENT_FORCE_CONFIG:-1}" +normalize_scgi_settings() { + case "${RTORRENT_SCGI_BACKEND}" in + tcp|unix) ;; + *) echo "Invalid RTORRENT_SCGI_BACKEND: ${RTORRENT_SCGI_BACKEND}" >&2; exit 1 ;; + esac + if [[ "${RTORRENT_SCGI_BACKEND}" == "unix" ]]; then + if [[ -z "${RTORRENT_SCGI_PROXY_TOKEN}" ]]; then + RTORRENT_SCGI_PROXY_TOKEN="$(LC_ALL=C tr -dc 'A-Za-z0-9_-' &2 - echo "Supported options: --build-rtorrent, --with-xmlrpc-c" >&2 + echo "Supported options: --build-rtorrent, --with-xmlrpc-c, --scgi-backend tcp|unix, --scgi-unix-socket, --rtorrent-socket PATH" >&2 exit 1 ;; esac done +normalize_scgi_settings + if [[ "${RTORRENT_WITH_XMLRPC_C:-0}" == "1" ]]; then RTORRENT_BUILD_FROM_SOURCE=1 RTORRENT_EXTRA_ARGS+=(--with-xmlrpc-c) @@ -75,12 +112,19 @@ write_rtorrent_config() { echo "Keeping existing config: ${config}" return fi + local scgi_line + if [[ "${RTORRENT_SCGI_BACKEND}" == "unix" ]]; then + scgi_line="network.scgi.open_local = ${RTORRENT_SCGI_SOCKET} +execute.nothrow = chmod,660,${RTORRENT_SCGI_SOCKET}" + else + scgi_line="network.scgi.open_port = 127.0.0.1:${RTORRENT_SCGI_PORT}" + fi cat > "${config}" < /etc/systemd/system/rtorrent@.service <&2; exit 1 ;; + esac + if [[ "${RTORRENT_SCGI_BACKEND}" == "unix" ]]; then + if [[ -z "${RTORRENT_SCGI_PROXY_TOKEN}" ]]; then + RTORRENT_SCGI_PROXY_TOKEN="$(LC_ALL=C tr -dc 'A-Za-z0-9_-' &2; exit 1 ;; + esac + if [[ "${RTORRENT_SCGI_BACKEND}" == "unix" ]]; then + if [[ -z "${RTORRENT_SCGI_PROXY_TOKEN}" ]]; then + RTORRENT_SCGI_PROXY_TOKEN="$(LC_ALL=C tr -dc 'A-Za-z0-9_-'