fix: 保留 Compose 环境变量中的特殊字符

- 引用并转义生成的 .env 值,防止 Compose 意外执行变量插值
- 为兼容旧版 Compose,拒绝管理员凭证中不受支持的单引号
- 从运行中的容器显示实际生效的管理员凭证
This commit is contained in:
eddy
2026-07-18 04:10:48 +08:00
parent fe2baeb8f3
commit d380f35db9
2 changed files with 96 additions and 20 deletions
+48 -10
View File
@@ -170,9 +170,27 @@ gen_secret() {
openssl rand -hex 32 2>/dev/null || head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n' openssl rand -hex 32 2>/dev/null || head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n'
} }
# Compose 对 .env 中未加引号和双引号的值执行变量插值。
# 不含单引号的值用单引号包裹,按字面处理($、#、空格、\ 均不被改写);
# 含单引号的值改用双引号包裹,并转义 \、" 和 $。
compose_env_literal() {
local value=$1
if [[ $value != *"'"* ]]; then
printf "'%s'" "$value"
return
fi
local escaped=${value//\\/\\\\}
escaped=${escaped//\"/\\\"}
escaped=${escaped//\$/\\\$}
printf '"%s"' "$escaped"
}
if [ ! -f "$env_file" ]; then if [ ! -f "$env_file" ]; then
# 兼容旧版安装:尝试从旧 docker-compose.yml 中提取已在用的数据库密码, # 兼容旧版安装:尝试从旧 docker-compose.yml 中提取已在用的数据库密码,
# 避免生成新密码后与已有 postgres 数据目录不一致导致连不上库 # 避免生成新密码后与已有 postgres 数据目录不一致导致连不上库
# 注意:这里回读的是旧版明文 docker-compose.yml(历史格式),与本脚本新生成的
# 引号化 .env 表示法不同。切勿改成从 .env 回读——.env 里的值可能带引号/转义,
# 下面的 [^ #]* 会在引号或 # 处截断,取到错误密码。
old_pg_password="" old_pg_password=""
old_pg_user="" old_pg_user=""
if [ -f docker-compose.yml ]; then if [ -f docker-compose.yml ]; then
@@ -192,23 +210,40 @@ if [ ! -f "$env_file" ]; then
admin_email="${admin_email:-$default_admin_email}" admin_email="${admin_email:-$default_admin_email}"
read -r -p "管理员密码(回车使用随机密码 $default_admin_password):" admin_password read -r -p "管理员密码(回车使用随机密码 $default_admin_password):" admin_password
admin_password="${admin_password:-$default_admin_password}" admin_password="${admin_password:-$default_admin_password}"
# 单引号值无法在旧版 docker-compose (v1) 的 .env 解析中可靠转义,直接拒绝
while [[ $admin_email == *"'"* ]] || [[ $admin_password == *"'"* ]]; do
echo "邮箱或密码不能包含单引号('),请重新输入。"
read -r -p "管理员邮箱(默认 $default_admin_email):" admin_email
admin_email="${admin_email:-$default_admin_email}"
read -r -p "管理员密码(回车使用随机密码 $default_admin_password):" admin_password
admin_password="${admin_password:-$default_admin_password}"
done
pg_user_env=$(compose_env_literal "$pg_user")
pg_password_env=$(compose_env_literal "$pg_password")
pg_db_env=$(compose_env_literal "sub2api")
jwt_secret_env=$(compose_env_literal "$(gen_secret)")
totp_key_env=$(compose_env_literal "$(gen_secret)")
admin_email_env=$(compose_env_literal "$admin_email")
admin_password_env=$(compose_env_literal "$admin_password")
tz_env=$(compose_env_literal "Asia/Shanghai")
cat <<EOF > "$env_file" cat <<EOF > "$env_file"
# Sub2API 环境配置(由安装脚本生成,请勿删除,否则需重新配置) # Sub2API 环境配置(由安装脚本生成,请勿删除,否则需重新配置)
# 数据库 # 数据库
POSTGRES_USER=$pg_user POSTGRES_USER=$pg_user_env
POSTGRES_PASSWORD=$pg_password POSTGRES_PASSWORD=$pg_password_env
POSTGRES_DB=sub2api POSTGRES_DB=$pg_db_env
# 固定密钥:保证容器重建/更新后登录态和 2FA 不失效 # 固定密钥:保证容器重建/更新后登录态和 2FA 不失效
JWT_SECRET=$(gen_secret) JWT_SECRET=$jwt_secret_env
TOTP_ENCRYPTION_KEY=$(gen_secret) TOTP_ENCRYPTION_KEY=$totp_key_env
# 管理员账户(仅首次启动时创建,之后修改无效) # 管理员账户(仅首次启动时创建,之后修改无效)
ADMIN_EMAIL=$admin_email ADMIN_EMAIL=$admin_email_env
ADMIN_PASSWORD=$admin_password ADMIN_PASSWORD=$admin_password_env
TZ=Asia/Shanghai TZ=$tz_env
EOF EOF
chmod 600 "$env_file" chmod 600 "$env_file"
echo "已生成 $env_file(包含数据库密码、密钥和管理员初始密码,请妥善保管)。" echo "已生成 $env_file(包含数据库密码、密钥和管理员初始密码,请妥善保管)。"
@@ -346,8 +381,11 @@ fi
echo "------------------------" echo "------------------------"
echo "访问链接:" echo "访问链接:"
echo "http://服务器IP:$port80" echo "http://服务器IP:$port80"
echo "管理员账户(首次安装时自动创建,凭证保存在 $env_file):" echo "管理员账户(首次安装时自动创建,以下为容器实际收到的凭证):"
grep -E '^(ADMIN_EMAIL|ADMIN_PASSWORD)=' "$env_file" || true if ! docker inspect sub2api-2-app --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null \
| grep -E '^(ADMIN_EMAIL|ADMIN_PASSWORD)='; then
echo "(容器尚未就绪,凭证可稍后执行 docker inspect sub2api-2-app 查看;如查看 $env_file,注意值外层的引号不属于密码本身)"
fi
echo "------------------------" echo "------------------------"
echo "已启用 AUTO_SETUP 自动初始化:更新/重建容器后无需重新走网页安装向导。" echo "已启用 AUTO_SETUP 自动初始化:更新/重建容器后无需重新走网页安装向导。"
echo "已开启定时自动更新:每天 04:10 仅拉取最新 app 镜像并重启(db / redis 不动)。" echo "已开启定时自动更新:每天 04:10 仅拉取最新 app 镜像并重启(db / redis 不动)。"
+48 -10
View File
@@ -107,9 +107,27 @@ gen_secret() {
openssl rand -hex 32 2>/dev/null || head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n' openssl rand -hex 32 2>/dev/null || head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n'
} }
# Compose 对 .env 中未加引号和双引号的值执行变量插值。
# 不含单引号的值用单引号包裹,按字面处理($、#、空格、\ 均不被改写);
# 含单引号的值改用双引号包裹,并转义 \、" 和 $。
compose_env_literal() {
local value=$1
if [[ $value != *"'"* ]]; then
printf "'%s'" "$value"
return
fi
local escaped=${value//\\/\\\\}
escaped=${escaped//\"/\\\"}
escaped=${escaped//\$/\\\$}
printf '"%s"' "$escaped"
}
if [ ! -f "$env_file" ]; then if [ ! -f "$env_file" ]; then
# 兼容旧版安装:尝试从旧 docker-compose.yml 中提取已在用的数据库密码, # 兼容旧版安装:尝试从旧 docker-compose.yml 中提取已在用的数据库密码,
# 避免生成新密码后与已有 postgres 数据目录不一致导致连不上库 # 避免生成新密码后与已有 postgres 数据目录不一致导致连不上库
# 注意:这里回读的是旧版明文 docker-compose.yml(历史格式),与本脚本新生成的
# 引号化 .env 表示法不同。切勿改成从 .env 回读——.env 里的值可能带引号/转义,
# 下面的 [^ #]* 会在引号或 # 处截断,取到错误密码。
old_pg_password="" old_pg_password=""
old_pg_user="" old_pg_user=""
if [ -f docker-compose.yml ]; then if [ -f docker-compose.yml ]; then
@@ -129,23 +147,40 @@ if [ ! -f "$env_file" ]; then
admin_email="${admin_email:-$default_admin_email}" admin_email="${admin_email:-$default_admin_email}"
read -r -p "管理员密码(回车使用随机密码 $default_admin_password):" admin_password read -r -p "管理员密码(回车使用随机密码 $default_admin_password):" admin_password
admin_password="${admin_password:-$default_admin_password}" admin_password="${admin_password:-$default_admin_password}"
# 单引号值无法在旧版 docker-compose (v1) 的 .env 解析中可靠转义,直接拒绝
while [[ $admin_email == *"'"* ]] || [[ $admin_password == *"'"* ]]; do
echo "邮箱或密码不能包含单引号('),请重新输入。"
read -r -p "管理员邮箱(默认 $default_admin_email):" admin_email
admin_email="${admin_email:-$default_admin_email}"
read -r -p "管理员密码(回车使用随机密码 $default_admin_password):" admin_password
admin_password="${admin_password:-$default_admin_password}"
done
pg_user_env=$(compose_env_literal "$pg_user")
pg_password_env=$(compose_env_literal "$pg_password")
pg_db_env=$(compose_env_literal "sub2api")
jwt_secret_env=$(compose_env_literal "$(gen_secret)")
totp_key_env=$(compose_env_literal "$(gen_secret)")
admin_email_env=$(compose_env_literal "$admin_email")
admin_password_env=$(compose_env_literal "$admin_password")
tz_env=$(compose_env_literal "Asia/Shanghai")
cat <<EOF > "$env_file" cat <<EOF > "$env_file"
# Sub2API 环境配置(由安装脚本生成,请勿删除,否则需重新配置) # Sub2API 环境配置(由安装脚本生成,请勿删除,否则需重新配置)
# 数据库 # 数据库
POSTGRES_USER=$pg_user POSTGRES_USER=$pg_user_env
POSTGRES_PASSWORD=$pg_password POSTGRES_PASSWORD=$pg_password_env
POSTGRES_DB=sub2api POSTGRES_DB=$pg_db_env
# 固定密钥:保证容器重建/更新后登录态和 2FA 不失效 # 固定密钥:保证容器重建/更新后登录态和 2FA 不失效
JWT_SECRET=$(gen_secret) JWT_SECRET=$jwt_secret_env
TOTP_ENCRYPTION_KEY=$(gen_secret) TOTP_ENCRYPTION_KEY=$totp_key_env
# 管理员账户(仅首次启动时创建,之后修改无效) # 管理员账户(仅首次启动时创建,之后修改无效)
ADMIN_EMAIL=$admin_email ADMIN_EMAIL=$admin_email_env
ADMIN_PASSWORD=$admin_password ADMIN_PASSWORD=$admin_password_env
TZ=Asia/Shanghai TZ=$tz_env
EOF EOF
chmod 600 "$env_file" chmod 600 "$env_file"
echo "已生成 $env_file(包含数据库密码、密钥和管理员初始密码,请妥善保管)。" echo "已生成 $env_file(包含数据库密码、密钥和管理员初始密码,请妥善保管)。"
@@ -283,8 +318,11 @@ fi
echo "------------------------" echo "------------------------"
echo "访问链接:" echo "访问链接:"
echo "https://sub2api.ghuang.top" echo "https://sub2api.ghuang.top"
echo "管理员账户(首次安装时自动创建,凭证保存在 $env_file):" echo "管理员账户(首次安装时自动创建,以下为容器实际收到的凭证):"
grep -E '^(ADMIN_EMAIL|ADMIN_PASSWORD)=' "$env_file" || true if ! docker inspect sub2api-app --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null \
| grep -E '^(ADMIN_EMAIL|ADMIN_PASSWORD)='; then
echo "(容器尚未就绪,凭证可稍后执行 docker inspect sub2api-app 查看;如查看 $env_file,注意值外层的引号不属于密码本身)"
fi
echo "------------------------" echo "------------------------"
echo "已启用 AUTO_SETUP 自动初始化:更新/重建容器后无需重新走网页安装向导。" echo "已启用 AUTO_SETUP 自动初始化:更新/重建容器后无需重新走网页安装向导。"
echo "已开启定时自动更新:每天 04:00 仅拉取最新 app 镜像并重启(db / redis 不动)。" echo "已开启定时自动更新:每天 04:00 仅拉取最新 app 镜像并重启(db / redis 不动)。"