#!/bin/bash set -euo pipefail API="${JELLYFIN_API}" CURL="curl --connect-timeout 5 --max-time 30" CT="${JELLYFIN_CT}" PHASE="${JELLYFIN_PHASE}" CLIENT_HDR='X-Emby-Authorization: MediaBrowser Client="infinito", Device="ansible", DeviceId="infinito-deploy", Version="1.0.0"' log() { echo "[jellyfin-auth] $*"; } wait_up() { for _ in $(seq 1 60); do ${CURL} -fsS -o /dev/null "${API}/System/Info/Public" && return 0 sleep 5 done log "Jellyfin did not become ready at ${API}"; return 1 } complete_wizard() { ${CURL} -fsS -X POST "${API}/Startup/Configuration" -H "Content-Type: application/json" -H "${CLIENT_HDR}" \ -d '{"UICulture":"en-US","MetadataCountryCode":"US","PreferredMetadataLanguage":"en"}' >/dev/null 2>&1 || true # nocheck: shell-or-true -- grandfathered: worked in practice; TODO: sharpen to catch only the exact tolerated error ${CURL} -fsS "${API}/Startup/User" -H "${CLIENT_HDR}" >/dev/null 2>&1 || true # nocheck: shell-or-true -- grandfathered: worked in practice; TODO: sharpen to catch only the exact tolerated error ${CURL} -fsS -X POST "${API}/Startup/User" -H "Content-Type: application/json" -H "${CLIENT_HDR}" \ -d "{\"Name\":\"${JELLYFIN_ADMIN_USERNAME}\",\"Password\":\"${JELLYFIN_ADMIN_PASSWORD}\"}" >/dev/null 2>&1 || true # nocheck: shell-or-true -- grandfathered: worked in practice; TODO: sharpen to catch only the exact tolerated error ${CURL} -fsS -X POST "${API}/Startup/RemoteAccess" -H "Content-Type: application/json" -H "${CLIENT_HDR}" \ -d '{"EnableRemoteAccess":true,"EnableAutomaticPortMapping":false}' >/dev/null 2>&1 || true # nocheck: shell-or-true -- grandfathered: worked in practice; TODO: sharpen to catch only the exact tolerated error ${CURL} -fsS -X POST "${API}/Startup/Complete" -H "${CLIENT_HDR}" >/dev/null 2>&1 || true # nocheck: shell-or-true -- grandfathered: worked in practice; TODO: sharpen to catch only the exact tolerated error } authenticate() { ${CURL} -fsS -X POST "${API}/Users/AuthenticateByName" -H "Content-Type: application/json" -H "${CLIENT_HDR}" \ -d "{\"Username\":\"${JELLYFIN_ADMIN_USERNAME}\",\"Pw\":\"${1-${JELLYFIN_ADMIN_PASSWORD}}\"}" } token_of() { sed -n 's/.*"AccessToken":"\([^"]*\)".*/\1/p'; } user_id_of() { sed -n 's/.*"User":{[^}]*"Id":"\([^"]*\)".*/\1/p'; } get_token() { authenticate | token_of } realign_admin_password() { local pin pin_token user_id auth ${CURL} -fsS -X POST "${API}/Users/ForgotPassword" -H "Content-Type: application/json" -H "${CLIENT_HDR}" \ -d "{\"EnteredUsername\":\"${JELLYFIN_ADMIN_USERNAME}\"}" >/dev/null || { log "ERROR: Jellyfin refused the password reset request" return 1 } pin="$(container exec "${CT}" sh -c 'cat /config/passwordreset*.json 2>/dev/null' \ | sed -n 's/.*"Pin":"\([^"]*\)".*/\1/p' | head -n1)" || pin="" if [ -z "${pin}" ]; then log "ERROR: Jellyfin accepted the reset request but wrote no pin file" return 1 fi ${CURL} -fsS -X POST "${API}/Users/ForgotPassword/Pin" -H "Content-Type: application/json" -H "${CLIENT_HDR}" \ -d "{\"Pin\":\"${pin}\"}" >/dev/null || { log "ERROR: Jellyfin rejected the reset pin" return 1 } auth="$(authenticate "${pin}")" || auth="" pin_token="$(printf '%s' "${auth}" | token_of)" user_id="$(printf '%s' "${auth}" | user_id_of)" if [ -z "${pin_token}" ] || [ -z "${user_id}" ]; then log "ERROR: redeeming the reset pin cleared the stored password and the administrator still does not authenticate; the account is now WITHOUT a password" return 1 fi ${CURL} -fsS -X POST "${API}/Users/${user_id}/Password" -H "Content-Type: application/json" \ -H "Authorization: MediaBrowser Token=\"${pin_token}\"" \ -d "{\"CurrentPw\":\"${pin}\",\"NewPw\":\"${JELLYFIN_ADMIN_PASSWORD}\"}" >/dev/null || { log "ERROR: the password change was rejected; the reset already cleared the old one, so the administrator is now WITHOUT a password" return 1 } log "realigned the administrator password with the one this deployment declares" } seed_admin_and_get_token() { for _ in $(seq 1 40); do complete_wizard TOKEN="$(get_token || true)" [ -n "${TOKEN:-}" ] && return 0 sleep 3 done realign_admin_password || return 1 TOKEN="$(get_token)" || TOKEN="" [ -n "${TOKEN:-}" ] && return 0 return 1 } install_plugin() { if ${CURL} -fsS -X POST "${API}/Packages/Installed/${1}" -H "Authorization: MediaBrowser Token=\"${TOKEN}\"" >/dev/null 2>&1; then log "requested install: ${1}" else log "install request failed (already present?): ${1}" fi } install_ldap_plugin() { : "${JELLYFIN_LDAP_PLUGIN_URL:?missing (set via meta/addons/ldap-authentication.yml)}" : "${JELLYFIN_LDAP_PLUGIN_VERSION:?missing (set via meta/addons/ldap-authentication.yml)}" local tmp tmp="$(mktemp -d)" if ! ${CURL} --max-time 300 -fsSL -o "${tmp}/ldap.zip" "${JELLYFIN_LDAP_PLUGIN_URL}"; then log "ERROR: failed to download LDAP plugin from ${JELLYFIN_LDAP_PLUGIN_URL}" rm -rf "${tmp}"; return 1 fi if command -v md5sum >/dev/null 2>&1 \ && ! echo "${JELLYFIN_LDAP_PLUGIN_MD5} ${tmp}/ldap.zip" | md5sum -c - >/dev/null 2>&1; then log "ERROR: LDAP plugin checksum mismatch"; rm -rf "${tmp}"; return 1 fi python3 -m zipfile -e "${tmp}/ldap.zip" "${tmp}/ldap" container exec "${CT}" mkdir -p "/config/plugins/LDAP Authentication_${JELLYFIN_LDAP_PLUGIN_VERSION}" # nocheck: container-cp - plugin payload is staged in a script-local tmp dir on the same node container cp "${tmp}/ldap/." "${CT}:/config/plugins/LDAP Authentication_${JELLYFIN_LDAP_PLUGIN_VERSION}" rm -rf "${tmp}" log "installed LDAP plugin ${JELLYFIN_LDAP_PLUGIN_VERSION} from GitHub release" } write_config() { container exec -i "${CT}" sh -c "mkdir -p /config/plugins/configurations && cat > /config/plugins/configurations/$1" } if [ "${PHASE}" = "install" ]; then wait_up seed_admin_and_get_token || { log "ERROR: the administrator cannot authenticate after wizard retries; the deployed password does not match the running server" exit 1 } if [ -n "${TOKEN:-}" ] && [ "${JELLYFIN_SSO_ENABLED}" = "true" ]; then ${CURL} -fsS -X POST "${API}/Repositories" -H "Content-Type: application/json" \ -H "Authorization: MediaBrowser Token=\"${TOKEN}\"" \ -d "[{\"Name\":\"jellyfin-plugin-sso\",\"Url\":\"${JELLYFIN_SSO_PLUGIN_MANIFEST}\",\"Enabled\":true}]" >/dev/null 2>&1 || true # nocheck: shell-or-true -- grandfathered: worked in practice; TODO: sharpen to catch only the exact tolerated error fi [ "${JELLYFIN_LDAP_ENABLED}" = "true" ] && install_ldap_plugin [ -n "${TOKEN:-}" ] && [ "${JELLYFIN_SSO_ENABLED}" = "true" ] && install_plugin "SSO%20Authentication" sleep 10 log "plugin install phase complete (restart is driven by the calling task)" exit 0 fi if [ "${PHASE}" != "configure" ]; then log "ERROR: unknown JELLYFIN_PHASE '${PHASE}' (expected install|configure)" exit 1 fi wait_up seed_admin_and_get_token || { log "ERROR: the administrator cannot authenticate after wizard retries; the deployed password does not match the running server" exit 1 } if [ "${JELLYFIN_LDAP_ENABLED}" = "true" ]; then write_config "LDAP-Auth.xml" < ${JELLYFIN_LDAP_SERVER} ${JELLYFIN_LDAP_PORT} false false false ${JELLYFIN_LDAP_BIND_DN} ${JELLYFIN_LDAP_BIND_PASSWORD} ${JELLYFIN_LDAP_BASE_DN} (objectClass=inetOrgPerson) uid, cn, mail, displayName true ${JELLYFIN_LDAP_UID_ATTR} ${JELLYFIN_LDAP_UID_ATTR} userPassword true XML container exec -i "${CT}" sh -c "python3 -c \"import xml.dom.minidom,sys; xml.dom.minidom.parse('/config/plugins/configurations/LDAP-Auth.xml')\"" 2>/dev/null \ || log "WARN: LDAP-Auth.xml not parseable by python (still written); confirm on live deploy" fi if [ -n "${TOKEN:-}" ] && [ "${JELLYFIN_SSO_ENABLED}" = "true" ]; then SSO_PLUGIN_GUID="505ce9d1-d916-42fa-86ca-673ef241d7df" if ${CURL} -fsS -X POST "${API}/Plugins/${SSO_PLUGIN_GUID}/Configuration" \ -H "Content-Type: application/json" \ -H "Authorization: MediaBrowser Token=\"${TOKEN}\"" --data @- >/dev/null 2>&1 <","CustomCss":"a.raised.emby-button { padding: 0.9em 1em; color: inherit !important; } .disclaimerContainer { display: block; }","SplashscreenEnabled":false}' >/dev/null 2>&1; then log "configured SSO login button via branding" else log "WARN: failed to set SSO login branding" fi fi log "auth provisioning complete (final restart is driven by the calling task)"