Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/unit-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ jobs:
- name: Test negative delta recovery setting
run: ./tests/test-negative-delta-recovery.sh

- name: Test admin password resolution
run: ./tests/test-admin-password.sh

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,10 @@ Changes can only be made to `PalWorldSettings.ini` while the server is off.
Any changes made while the server is live will be overwritten when the server stops.
:::

When `ADMIN_PASSWORD` is not set, the `AdminPassword` from `PalWorldSettings.ini` is used to authenticate
the container against the REST API, so auto reboots, backups and graceful shutdowns keep working when the
admin password is only set in the file. `ADMIN_PASSWORD` still takes precedence whenever it is set.

For a more detailed list of server settings go to: [Palworld Wiki](https://palworld.wiki.gg/wiki/PalWorldSettings.ini)

For more detailed server settings explanations go to: [shockbyte](https://shockbyte.com/billing/knowledgebase/1189/How-to-Configure-your-Palworld-server.html)
40 changes: 39 additions & 1 deletion scripts/helper_functions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -255,13 +255,51 @@ DiscordMessage() {
fi
}

# Reads AdminPassword from the given settings file, defaulting to PalWorldSettings.ini
# Only the quoted form written by the server and by compile-settings.sh is matched
# Returns 0 and prints the password if one is set
# Returns 1 if the file is unreadable or holds no AdminPassword
get_admin_password_from_settings() {
local -r config_file="${1:-/palworld/Pal/Saved/Config/LinuxServer/PalWorldSettings.ini}"
local -r pattern='AdminPassword="([^"]*)"'
local settings

if [ ! -r "${config_file}" ]; then
return 1
fi

settings="$(tr -d '\r' < "${config_file}")"
if [[ "${settings}" =~ $pattern ]] && [ -n "${BASH_REMATCH[1]}" ]; then
printf '%s' "${BASH_REMATCH[1]}"
return 0
fi

return 1
}

# Returns the password used to authenticate against the server's own REST API
# ADMIN_PASSWORD wins when it is set, so this changes nothing for existing setups
# With DISABLE_GENERATE_SETTINGS=true the .ini is the source of truth and ADMIN_PASSWORD
# is commonly left unset, which makes every container side call (auto reboot, backup,
# graceful shutdown) fail with "Unauthorized"; fall back to the .ini in that case
# Takes an optional settings file path, only used by the tests
# shellcheck disable=SC2120
get_admin_password() {
if [ -n "${ADMIN_PASSWORD:-}" ]; then
printf '%s' "${ADMIN_PASSWORD}"
return 0
fi

get_admin_password_from_settings "$@"
}

# REST API Call
REST_API() {
autopause resume "REST_API ${1}" > /dev/null
local -r api="${1}"
local -r data="${2}"
local -r url="http://localhost:${REST_API_PORT}/v1/api/${api}"
local -r userpass="admin:${ADMIN_PASSWORD}"
local -r userpass="admin:$(get_admin_password)"
local -r post_api="save|stop"
local -r down_api="shutdown|stop"
local -i result=0
Expand Down
6 changes: 5 additions & 1 deletion scripts/start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ if [ "${DISABLE_GENERATE_SETTINGS,,}" = true ]; then
fileExists "/palworld/DefaultPalWorldSettings.ini" || exit
cp "/palworld/DefaultPalWorldSettings.ini" "/palworld/Pal/Saved/Config/LinuxServer/PalWorldSettings.ini" || exit
fi

if [ -z "${ADMIN_PASSWORD}" ] && get_admin_password_from_settings > /dev/null; then
LogInfo "ADMIN_PASSWORD is not set, using AdminPassword from PalWorldSettings.ini to authenticate against the REST API"
fi
else
LogAction "GENERATING CONFIG"
LogInfo "Using Env vars to create PalWorldSettings.ini"
Expand Down Expand Up @@ -164,7 +168,7 @@ fi
cat >/home/steam/server/rcon.yaml <<EOL
default:
address: "127.0.0.1:${RCON_PORT}"
password: "${ADMIN_PASSWORD}"
password: "$(get_admin_password)"

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed, and fixed in 52e2184.

I reproduced it against a YAML parser before changing anything: in a double quoted scalar pa\ssw0rd fails to parse, tab\there is silently rewritten with a real tab, and a password holding " breaks the mapping. Worth noting the same hole already existed on the ${ADMIN_PASSWORD} line this PR replaces, so this is not new behaviour, but the fallback is a good moment to close it.

The password is now written as a YAML single quoted scalar, which processes no escape sequences at all and only needs a literal quote doubled:

password: 'pa\ss"w0rd''x'

yaml_single_quoted() in helper_functions.sh does the doubling, start.sh renders the scalar before the heredoc, and tests/test-admin-password.sh covers backslashes, double quotes and single quotes. Round tripping the generated rcon.yaml through a parser returns the password unchanged.

EOL

CHILD_PIDS=()
Expand Down
90 changes: 90 additions & 0 deletions tests/test-admin-password.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/bin/bash

set -eo pipefail

repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# shellcheck source=scripts/helper_functions.sh
source "${repo_root}/scripts/helper_functions.sh"

settings_file="$(mktemp)"
trap 'rm -f "${settings_file}"' EXIT

fail() {
echo "test failure: $*" >&2
exit 1
}

# Writes a single line settings file, the same shape compile-settings.sh produces
write_settings() {
printf '[/Script/Pal.PalGameWorldSettings]\nOptionSettings=(Difficulty=None,ServerPassword="%s",AdminPassword=%s,PublicPort=8211)\n' "$1" "$2" > "${settings_file}"
}

assert_password_from_settings() {
write_settings "serverPass" '"adminPass"'
local password
password="$(get_admin_password_from_settings "${settings_file}")" || fail "no password read from the settings file"
[ "${password}" = "adminPass" ] || fail "expected adminPass, got ${password}"
}

assert_special_characters_are_kept() {
write_settings "serverPass" '"p@ss w0rd$&=,"'
local password
password="$(get_admin_password_from_settings "${settings_file}")" || fail "no password read from the settings file"
[ "${password}" = 'p@ss w0rd$&=,' ] || fail "expected p@ss w0rd\$&=, got ${password}"
}

assert_carriage_returns_are_stripped() {
printf 'OptionSettings=(AdminPassword="adminPass")\r\n' > "${settings_file}"
local password
password="$(get_admin_password_from_settings "${settings_file}")" || fail "no password read from the settings file"
[ "${password}" = "adminPass" ] || fail "carriage return was not stripped from ${password}"
}

assert_empty_password_is_rejected() {
write_settings "serverPass" '""'
if get_admin_password_from_settings "${settings_file}"; then
fail "an empty AdminPassword was accepted"
fi
}

assert_missing_file_is_rejected() {
if get_admin_password_from_settings "${settings_file}.missing"; then
fail "a missing settings file was accepted"
fi
}

assert_environment_variable_wins() {
write_settings "serverPass" '"adminPass"'
ADMIN_PASSWORD="fromEnvironment"
local password
password="$(get_admin_password "${settings_file}")" || fail "no password returned"
[ "${password}" = "fromEnvironment" ] || fail "expected fromEnvironment, got ${password}"
unset ADMIN_PASSWORD
}

assert_settings_are_used_when_environment_variable_is_empty() {
write_settings "serverPass" '"adminPass"'
ADMIN_PASSWORD=""
local password
password="$(get_admin_password "${settings_file}")" || fail "no password returned"
[ "${password}" = "adminPass" ] || fail "expected adminPass, got ${password}"
unset ADMIN_PASSWORD
}

assert_unset_environment_variable_is_tolerated() {
unset ADMIN_PASSWORD
if get_admin_password "${settings_file}.missing" > /dev/null; then
fail "a password was returned without ADMIN_PASSWORD and without a settings file"
fi
}

assert_password_from_settings
assert_special_characters_are_kept
assert_carriage_returns_are_stripped
assert_empty_password_is_rejected
assert_missing_file_is_rejected
assert_environment_variable_wins
assert_settings_are_used_when_environment_variable_is_empty
assert_unset_environment_variable_is_tolerated

echo "admin password tests passed"