mirror of
https://github.com/okxlin/appstore.git
synced 2026-09-24 16:01:02 +00:00
Add mobile Codex sessions through the published Paseo-enabled image, default the host listener to loopback, preserve existing permissions and persistent state, and migrate missing or empty bind settings without overwriting custom ports or addresses.
179 lines
6.1 KiB
Bash
Executable File
179 lines
6.1 KiB
Bash
Executable File
#!/bin/bash
|
|
# init.sh — codex-claude-workstation 1Panel 初始化脚本
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
|
|
ROOT_DIR="$(cd -- "${SCRIPT_DIR}/.." && pwd -P)"
|
|
ENV_FILE="${ROOT_DIR}/.env"
|
|
|
|
if [[ -L "$ENV_FILE" ]]; then
|
|
echo "Refusing to read a symlinked .env file" >&2
|
|
exit 1
|
|
elif [[ -f "$ENV_FILE" ]]; then
|
|
chmod 0600 -- "$ENV_FILE"
|
|
fi
|
|
|
|
strip_matching_quotes() {
|
|
local value="$1"
|
|
if [[ ${#value} -ge 2 ]]; then
|
|
if [[ "${value:0:1}" == '"' && "${value: -1}" == '"' ]]; then
|
|
value="${value:1:${#value}-2}"
|
|
elif [[ "${value:0:1}" == "'" && "${value: -1}" == "'" ]]; then
|
|
value="${value:1:${#value}-2}"
|
|
fi
|
|
fi
|
|
printf '%s\n' "$value"
|
|
}
|
|
|
|
get_config_value() {
|
|
local key="$1"
|
|
local default="$2"
|
|
local value=""
|
|
|
|
if [[ -n "${!key+x}" ]]; then
|
|
value="${!key}"
|
|
elif [[ -f "$ENV_FILE" && ! -L "$ENV_FILE" ]]; then
|
|
value="$(sed -n -E "s/^${key}=//p" "$ENV_FILE" | tail -n 1)"
|
|
value="$(strip_matching_quotes "$value")"
|
|
fi
|
|
|
|
if [[ -n "$value" ]]; then
|
|
printf '%s\n' "$value"
|
|
else
|
|
printf '%s\n' "$default"
|
|
fi
|
|
}
|
|
|
|
paseo_password_is_websocket_token() {
|
|
local value="${1-}"
|
|
local token_pattern="^[A-Za-z0-9!#\$%&'*+.^_\`|~-]+$"
|
|
|
|
[[ ${#value} -ge 1 && ${#value} -le 128 && "$value" =~ $token_pattern ]]
|
|
}
|
|
|
|
paseo_bind_ip_is_valid() {
|
|
local value="$1"
|
|
local octet decimal
|
|
local -a octets=()
|
|
|
|
[[ "$value" =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]] || return 1
|
|
IFS='.' read -r -a octets <<< "$value"
|
|
[[ ${#octets[@]} -eq 4 ]] || return 1
|
|
for octet in "${octets[@]}"; do
|
|
decimal=$((10#${octet}))
|
|
(( decimal <= 255 )) || return 1
|
|
done
|
|
}
|
|
|
|
resolve_app_path() {
|
|
local key="$1"
|
|
local raw="$2"
|
|
local clean candidate resolved current part
|
|
local -a parts=()
|
|
case "$raw" in
|
|
""|/*|.|..|../*|*/../*|*/..) echo "unsafe ${key} path" >&2; return 1 ;;
|
|
esac
|
|
if [[ "$raw" =~ [[:cntrl:]] ]]; then
|
|
echo "unsafe ${key} path" >&2
|
|
return 1
|
|
fi
|
|
clean="${raw#./}"
|
|
[[ -n "$clean" ]] || { echo "unsafe ${key} path" >&2; return 1; }
|
|
command -v realpath >/dev/null 2>&1 || { echo "realpath is required" >&2; return 1; }
|
|
candidate="$ROOT_DIR/$clean"
|
|
resolved="$(realpath -m -- "$candidate")" || { echo "unsafe ${key} path" >&2; return 1; }
|
|
case "$resolved" in
|
|
"$ROOT_DIR"/*) ;;
|
|
*) echo "unsafe ${key} path" >&2; return 1 ;;
|
|
esac
|
|
current="$ROOT_DIR"
|
|
IFS='/' read -r -a parts <<< "$clean"
|
|
for part in "${parts[@]}"; do
|
|
[[ -z "$part" || "$part" == "." ]] && continue
|
|
current="$current/$part"
|
|
if [[ -L "$current" ]]; then
|
|
echo "unsafe ${key} path" >&2
|
|
return 1
|
|
fi
|
|
done
|
|
printf '%s\n' "$resolved"
|
|
}
|
|
|
|
APP_DATA_DIR_RAW="$(get_config_value APP_DATA_DIR ./data)"
|
|
CUSTOM_ENV_FILE_RAW="$(get_config_value CUSTOM_ENV_FILE ./data/custom.env)"
|
|
PANEL_APP_PORT_HTTP_VALUE="$(get_config_value PANEL_APP_PORT_HTTP 8080)"
|
|
PANEL_APP_PORT_PASEO_VALUE="$(get_config_value PANEL_APP_PORT_PASEO 6767)"
|
|
PASEO_BIND_IP_VALUE="$(get_config_value PASEO_BIND_IP 127.0.0.1)"
|
|
PASEO_PASSWORD_VALUE="$(get_config_value PASEO_PASSWORD "")"
|
|
CODE_SERVER_PASSWORD_VALUE="$(get_config_value CODE_SERVER_PASSWORD change-me)"
|
|
|
|
if ! paseo_bind_ip_is_valid "$PASEO_BIND_IP_VALUE"; then
|
|
echo "PASEO_BIND_IP must be a valid IPv4 address" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ -n "$PASEO_PASSWORD_VALUE" ]]; then
|
|
if ! paseo_password_is_websocket_token "$PASEO_PASSWORD_VALUE"; then
|
|
echo "PASEO_PASSWORD must be 1-128 HTTP-token-safe characters; use: openssl rand -hex 24" >&2
|
|
exit 1
|
|
elif [[ ${#PASEO_PASSWORD_VALUE} -lt 20 ]]; then
|
|
echo "PASEO_PASSWORD must contain at least 20 characters for public access" >&2
|
|
exit 1
|
|
fi
|
|
elif ! paseo_password_is_websocket_token "$CODE_SERVER_PASSWORD_VALUE"; then
|
|
echo "WARN: blank PASEO_PASSWORD falls back to a code-server password that is not browser WebSocket-token-safe." >&2
|
|
echo "WARN: code-server will remain usable, but set a separate PASEO_PASSWORD before enabling mobile access." >&2
|
|
elif [[ ${#CODE_SERVER_PASSWORD_VALUE} -lt 20 ]]; then
|
|
echo "WARN: blank PASEO_PASSWORD uses a code-server password shorter than the recommended 20 characters." >&2
|
|
fi
|
|
|
|
APP_DATA_DIR_ABS="$(resolve_app_path APP_DATA_DIR "$APP_DATA_DIR_RAW")"
|
|
WORKSPACE_DIR="$(resolve_app_path APP_DATA_DIR "${APP_DATA_DIR_RAW%/}/workspace")"
|
|
CUSTOM_ENV_FILE_ABS="$(resolve_app_path CUSTOM_ENV_FILE "$CUSTOM_ENV_FILE_RAW")"
|
|
|
|
mkdir -p -- "$WORKSPACE_DIR" "$(dirname -- "$CUSTOM_ENV_FILE_ABS")"
|
|
|
|
# Recheck after creation so a pre-existing or concurrently introduced symlink
|
|
# cannot silently redirect the file preparation outside the app directory.
|
|
[[ "$(resolve_app_path APP_DATA_DIR "$APP_DATA_DIR_RAW")" == "$APP_DATA_DIR_ABS" ]]
|
|
[[ "$(resolve_app_path APP_DATA_DIR "${APP_DATA_DIR_RAW%/}/workspace")" == "$WORKSPACE_DIR" ]]
|
|
[[ "$(resolve_app_path CUSTOM_ENV_FILE "$CUSTOM_ENV_FILE_RAW")" == "$CUSTOM_ENV_FILE_ABS" ]]
|
|
|
|
if [[ -L "$CUSTOM_ENV_FILE_ABS" || -d "$CUSTOM_ENV_FILE_ABS" ]]; then
|
|
echo "CUSTOM_ENV_FILE must be a regular non-symlink file" >&2
|
|
exit 1
|
|
fi
|
|
if [[ ! -e "$CUSTOM_ENV_FILE_ABS" ]]; then
|
|
install -m 0600 /dev/null "$CUSTOM_ENV_FILE_ABS"
|
|
fi
|
|
chmod 0600 -- "$CUSTOM_ENV_FILE_ABS"
|
|
|
|
echo "Codex Claude Workstation installed successfully."
|
|
echo ""
|
|
echo "Persistent directories:"
|
|
echo " ${WORKSPACE_DIR} -> /workspace"
|
|
echo " ${CUSTOM_ENV_FILE_ABS} -> custom env_file"
|
|
echo ""
|
|
echo "Access:"
|
|
echo " http://<server-ip>:${PANEL_APP_PORT_HTTP_VALUE}"
|
|
echo " Paseo: reverse proxy HTTPS to ${PASEO_BIND_IP_VALUE}:${PANEL_APP_PORT_PASEO_VALUE}"
|
|
echo " Login: code-server password"
|
|
echo " Switch to root: su - root (password configured from ROOT_PASSWORD)"
|
|
echo ""
|
|
echo "AI Tools (run in code-server terminal):"
|
|
echo " codex --help OpenAI Codex CLI"
|
|
echo " claude --help Anthropic Claude Code"
|
|
echo " npm install -g <pkg> Installs to persistent ~/.local"
|
|
echo ""
|
|
echo "Docker CLI (DOCKER_SOCK_SRC=/var/run/docker.sock by default; leave empty to disable):"
|
|
echo " docker ps"
|
|
echo " sudo docker ps # fallback if host socket permissions block direct access"
|
|
echo ""
|
|
echo "Codex sandbox:"
|
|
echo " doctor.sh"
|
|
echo " Host must allow kernel.unprivileged_userns_clone=1"
|
|
echo ""
|
|
echo "Proxy (manual enable):"
|
|
echo " supervisorctl start clash-meta # or sing-box, xray"
|
|
echo " Configs: ~/proxy/"
|