diff --git a/apps/opencloud/9.0.0/.env.sample b/apps/opencloud/9.0.0/.env.sample index 2b82bb1bc..a50b5cb8e 100644 --- a/apps/opencloud/9.0.0/.env.sample +++ b/apps/opencloud/9.0.0/.env.sample @@ -4,5 +4,4 @@ PANEL_APP_PORT_HTTP=4080 OC_DOMAIN=cloud.opencloud.test INITIAL_ADMIN_PASSWORD=replace-with-a-strong-password APP_DATA_DIR=./data -DOCKER_SOCKET_PATH=/var/run/docker.sock TZ=Asia/Shanghai diff --git a/apps/opencloud/9.0.0/config/traefik/opencloud.yml b/apps/opencloud/9.0.0/config/traefik/opencloud.yml new file mode 100644 index 000000000..d8ff05c2e --- /dev/null +++ b/apps/opencloud/9.0.0/config/traefik/opencloud.yml @@ -0,0 +1,13 @@ +http: + routers: + opencloud: + entryPoints: + - https + rule: "Host(`@OC_DOMAIN@`)" + service: opencloud + tls: {} + services: + opencloud: + loadBalancer: + servers: + - url: http://opencloud:9200 diff --git a/apps/opencloud/9.0.0/data.yml b/apps/opencloud/9.0.0/data.yml index dbfbbf012..d6b7221f9 100644 --- a/apps/opencloud/9.0.0/data.yml +++ b/apps/opencloud/9.0.0/data.yml @@ -84,22 +84,6 @@ additionalProperties: pt-br: Diretório de dados required: true type: text - - default: /var/run/docker.sock - edit: true - envKey: DOCKER_SOCKET_PATH - labelEn: Docker Socket Path - labelZh: Docker Socket 路径 - label: - en: Docker Socket Path - zh: Docker Socket 路径 - zh-Hant: Docker Socket 路徑 - ja: Docker Socket パス - ko: Docker Socket 경로 - ru: Путь к Docker Socket - ms: Laluan Docker Socket - pt-br: Caminho do Docker Socket - required: true - type: text - default: Asia/Shanghai edit: true envKey: TZ diff --git a/apps/opencloud/9.0.0/docker-compose.yml b/apps/opencloud/9.0.0/docker-compose.yml index 560f3cf15..f18f8987d 100644 --- a/apps/opencloud/9.0.0/docker-compose.yml +++ b/apps/opencloud/9.0.0/docker-compose.yml @@ -14,10 +14,6 @@ services: - --entryPoints.https.transport.respondingTimeouts.readTimeout=12h - --entryPoints.https.transport.respondingTimeouts.writeTimeout=12h - --entryPoints.https.transport.respondingTimeouts.idleTimeout=3m - - --providers.docker.endpoint=unix:///var/run/docker.sock - - --providers.docker.exposedByDefault=false - - --providers.docker.network=${CONTAINER_NAME}-network - - "--providers.docker.constraints=Label(`opencloud.instance`,`${CONTAINER_NAME}`)" - --providers.file.directory=/etc/traefik/dynamic - --providers.file.watch=true networks: @@ -29,7 +25,6 @@ services: - "${PANEL_APP_PORT_HTTP}:${PANEL_APP_PORT_HTTP}" - "${PANEL_APP_PORT_HTTPS}:${PANEL_APP_PORT_HTTPS}" volumes: - - "${DOCKER_SOCKET_PATH}:/var/run/docker.sock:ro" - "${APP_DATA_DIR}/traefik/dynamic:/etc/traefik/dynamic:ro" - "${APP_DATA_DIR}/traefik/certs:/certs:ro" healthcheck: @@ -95,13 +90,6 @@ services: retries: 5 labels: createdBy: "Apps" - opencloud.instance: "${CONTAINER_NAME}" - traefik.enable: "true" - traefik.http.routers.opencloud.entrypoints: https - traefik.http.routers.opencloud.rule: Host(`${OC_DOMAIN}`) - traefik.http.routers.opencloud.service: opencloud - traefik.http.routers.opencloud.tls: "true" - traefik.http.services.opencloud.loadbalancer.server.port: "9200" networks: opencloud-network: diff --git a/apps/opencloud/9.0.0/scripts/init.sh b/apps/opencloud/9.0.0/scripts/init.sh index 9bc6c554a..8ffe591f0 100755 --- a/apps/opencloud/9.0.0/scripts/init.sh +++ b/apps/opencloud/9.0.0/scripts/init.sh @@ -9,6 +9,36 @@ fail() { exit 1 } +ensure_no_symlink_components() { + local path="$1" + local current="/" + local part + local -a components + + [[ "$path" == /* ]] || fail "Path must be absolute: $path" + IFS='/' read -r -a components <<< "$path" + for part in "${components[@]}"; do + case "$part" in + "" | .) continue ;; + ..) + if [[ "$current" == /*/* ]]; then + current="${current%/*}" + else + current="/" + fi + ;; + *) + if [[ "$current" == / ]]; then + current="/$part" + else + current="$current/$part" + fi + ;; + esac + [[ ! -L "$current" ]] || fail "Data path must not contain a symbolic link: $current" + done +} + read_env_value() { local key="$1" local value="" @@ -31,14 +61,24 @@ else fi APP_DATA_DIR_RAW="${APP_DATA_DIR_RAW:-./data}" +if [[ ${OC_DOMAIN+x} ]]; then + OC_DOMAIN_RAW="$OC_DOMAIN" +else + OC_DOMAIN_RAW="$(read_env_value OC_DOMAIN)" +fi + [[ -n "$APP_DATA_DIR_RAW" ]] || fail "APP_DATA_DIR must not be empty" case "$APP_DATA_DIR_RAW" in *$'\n'* | *$'\r'* | *'$'* | *'#'*) fail "APP_DATA_DIR contains unsupported dotenv characters" ;; esac case "$APP_DATA_DIR_RAW" in - /*) APP_DATA_DIR_ABS="$(realpath -m -- "$APP_DATA_DIR_RAW")" ;; + /*) + ensure_no_symlink_components "$APP_DATA_DIR_RAW" + APP_DATA_DIR_ABS="$(realpath -m -- "$APP_DATA_DIR_RAW")" + ;; *) + ensure_no_symlink_components "${ROOT_DIR}/${APP_DATA_DIR_RAW#./}" APP_DATA_DIR_ABS="$(realpath -m -- "${ROOT_DIR}/${APP_DATA_DIR_RAW#./}")" case "$APP_DATA_DIR_ABS" in "$ROOT_DIR" | "$ROOT_DIR"/*) ;; @@ -63,8 +103,38 @@ traefik_paths=( "$APP_DATA_DIR_ABS/traefik/certs" ) +for path in "${opencloud_paths[@]}" "${traefik_paths[@]}"; do + ensure_no_symlink_components "$path" +done + mkdir -p -- "${opencloud_paths[@]}" "${traefik_paths[@]}" for path in "${opencloud_paths[@]}"; do - [[ ! -L "$path" ]] || fail "OpenCloud data subdirectory must not be a symbolic link: $path" chown 1000:1000 -- "$path" 2>/dev/null || true done + +[[ "$OC_DOMAIN_RAW" =~ ^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*$ ]] || fail "OC_DOMAIN must be a hostname without a scheme, port, or path" +[[ ${#OC_DOMAIN_RAW} -le 253 ]] || fail "OC_DOMAIN is too long" + +route_template="$ROOT_DIR/config/traefik/opencloud.yml" +route_file="$APP_DATA_DIR_ABS/traefik/dynamic/1panel-opencloud-route.yml" +[[ -f "$route_template" && ! -L "$route_template" ]] || fail "OpenCloud Traefik route template is missing or invalid" +[[ ! -L "$route_file" ]] || fail "OpenCloud Traefik route must not be a symbolic link: $route_file" +if [[ -e "$route_file" && ! -f "$route_file" ]]; then + fail "OpenCloud Traefik route must be a regular file: $route_file" +fi + +if [[ ! -e "$route_file" ]]; then + route_tmp="$(mktemp "${route_file}.tmp.XXXXXX")" + if ! sed "s/@OC_DOMAIN@/${OC_DOMAIN_RAW}/g" "$route_template" > "$route_tmp"; then + rm -f -- "$route_tmp" + fail "Could not create OpenCloud Traefik route" + fi + chmod 0644 -- "$route_tmp" + if ! ln -- "$route_tmp" "$route_file" 2>/dev/null; then + if [[ ! -f "$route_file" || -L "$route_file" ]]; then + rm -f -- "$route_tmp" + fail "Could not create OpenCloud Traefik route" + fi + fi + rm -f -- "$route_tmp" +fi diff --git a/apps/opencloud/9.1.0/.env.sample b/apps/opencloud/9.1.0/.env.sample new file mode 100644 index 000000000..a50b5cb8e --- /dev/null +++ b/apps/opencloud/9.1.0/.env.sample @@ -0,0 +1,7 @@ +CONTAINER_NAME=opencloud +PANEL_APP_PORT_HTTPS=4443 +PANEL_APP_PORT_HTTP=4080 +OC_DOMAIN=cloud.opencloud.test +INITIAL_ADMIN_PASSWORD=replace-with-a-strong-password +APP_DATA_DIR=./data +TZ=Asia/Shanghai diff --git a/apps/opencloud/9.1.0/config/opencloud/banned-password-list.txt b/apps/opencloud/9.1.0/config/opencloud/banned-password-list.txt new file mode 100644 index 000000000..934b1818f --- /dev/null +++ b/apps/opencloud/9.1.0/config/opencloud/banned-password-list.txt @@ -0,0 +1,5 @@ +password +12345678 +123 +OpenCloud +OpenCloud-1 diff --git a/apps/opencloud/9.1.0/config/opencloud/csp.yaml b/apps/opencloud/9.1.0/config/opencloud/csp.yaml new file mode 100644 index 000000000..8b80dde2b --- /dev/null +++ b/apps/opencloud/9.1.0/config/opencloud/csp.yaml @@ -0,0 +1,51 @@ +directives: + child-src: + - '''self''' + connect-src: + - '''self''' + - 'blob:' + - 'https://${COMPANION_DOMAIN|companion.opencloud.test}${TRAEFIK_PORT_HTTPS}/' + - 'wss://${COMPANION_DOMAIN|companion.opencloud.test}${TRAEFIK_PORT_HTTPS}/' + - 'https://raw.githubusercontent.com/opencloud-eu/awesome-apps/' + - 'https://${IDP_DOMAIN|keycloak.opencloud.test}${TRAEFIK_PORT_HTTPS}/' + - 'https://update.opencloud.eu/' + - 'https://tile.openstreetmap.org/' + default-src: + - '''none''' + font-src: + - '''self''' + frame-ancestors: + - '''self''' + frame-src: + - '''self''' + - 'blob:' + - 'https://embed.diagrams.net/' + - 'https://${COLLABORA_DOMAIN|collabora.opencloud.test}${TRAEFIK_PORT_HTTPS}/' + - 'https://${EURO_OFFICE_DOMAIN|euro-office.opencloud.test}${TRAEFIK_PORT_HTTPS}/' + - 'https://docs.opencloud.eu' + - 'https://${IDP_DOMAIN|keycloak.opencloud.test}${TRAEFIK_PORT_HTTPS}/' + img-src: + - '''self''' + - 'data:' + - 'blob:' + - 'https://raw.githubusercontent.com/opencloud-eu/awesome-apps/' + - 'https://tile.openstreetmap.org/' + - 'https://${COLLABORA_DOMAIN|collabora.opencloud.test}${TRAEFIK_PORT_HTTPS}/' + - 'https://${EURO_OFFICE_DOMAIN|euro-office.opencloud.test}${TRAEFIK_PORT_HTTPS}/' + manifest-src: + - '''self''' + media-src: + - '''self''' + object-src: + - '''self''' + - 'blob:' + script-src: + - '''self''' + - '''unsafe-inline''' + - 'https://${IDP_DOMAIN|keycloak.opencloud.test}${TRAEFIK_PORT_HTTPS}/' + style-src: + - '''self''' + - '''unsafe-inline''' + worker-src: + - "'self'" + - 'blob:' diff --git a/apps/opencloud/9.1.0/config/traefik/opencloud.yml b/apps/opencloud/9.1.0/config/traefik/opencloud.yml new file mode 100644 index 000000000..d8ff05c2e --- /dev/null +++ b/apps/opencloud/9.1.0/config/traefik/opencloud.yml @@ -0,0 +1,13 @@ +http: + routers: + opencloud: + entryPoints: + - https + rule: "Host(`@OC_DOMAIN@`)" + service: opencloud + tls: {} + services: + opencloud: + loadBalancer: + servers: + - url: http://opencloud:9200 diff --git a/apps/opencloud/9.1.0/data.yml b/apps/opencloud/9.1.0/data.yml new file mode 100644 index 000000000..d6b7221f9 --- /dev/null +++ b/apps/opencloud/9.1.0/data.yml @@ -0,0 +1,102 @@ +additionalProperties: + formFields: + - default: 4443 + edit: true + envKey: PANEL_APP_PORT_HTTPS + labelEn: HTTPS Port + labelZh: HTTPS 端口 + label: + en: HTTPS Port + zh: HTTPS 端口 + zh-Hant: HTTPS 連接埠 + ja: HTTPS ポート + ko: HTTPS 포트 + ru: HTTPS-порт + ms: Port HTTPS + pt-br: Porta HTTPS + required: true + rule: paramPort + type: number + - default: 4080 + edit: true + envKey: PANEL_APP_PORT_HTTP + labelEn: HTTP Redirect Port + labelZh: HTTP 跳转端口 + label: + en: HTTP Redirect Port + zh: HTTP 跳转端口 + zh-Hant: HTTP 重新導向連接埠 + ja: HTTP リダイレクトポート + ko: HTTP 리디렉션 포트 + ru: Порт перенаправления HTTP + ms: Port Ubah Hala HTTP + pt-br: Porta de redirecionamento HTTP + required: true + rule: paramPort + type: number + - default: cloud.opencloud.test + edit: true + envKey: OC_DOMAIN + labelEn: OpenCloud Domain + labelZh: OpenCloud 域名 + label: + en: OpenCloud Domain + zh: OpenCloud 域名 + zh-Hant: OpenCloud 網域 + ja: OpenCloud ドメイン + ko: OpenCloud 도메인 + ru: Домен OpenCloud + ms: Domain OpenCloud + pt-br: Domínio do OpenCloud + required: true + type: text + - default: '' + edit: true + envKey: INITIAL_ADMIN_PASSWORD + labelEn: Initial Administrator Password + labelZh: 初始管理员密码 + label: + en: Initial Administrator Password + zh: 初始管理员密码 + zh-Hant: 初始管理員密碼 + ja: 初期管理者パスワード + ko: 초기 관리자 비밀번호 + ru: Начальный пароль администратора + ms: Kata Laluan Pentadbir Awal + pt-br: Senha inicial do administrador + random: true + required: true + rule: paramComplexity + type: password + - default: ./data + edit: true + envKey: APP_DATA_DIR + labelEn: Data Directory + labelZh: 数据目录 + label: + en: Data Directory + zh: 数据目录 + zh-Hant: 資料目錄 + ja: データディレクトリ + ko: 데이터 디렉터리 + ru: Каталог данных + ms: Direktori Data + pt-br: Diretório de dados + required: true + type: text + - default: Asia/Shanghai + edit: true + envKey: TZ + labelEn: Timezone + labelZh: 时区 + label: + en: Timezone + zh: 时区 + zh-Hant: 時區 + ja: タイムゾーン + ko: 시간대 + ru: Часовой пояс + ms: Zon waktu + pt-br: Fuso horário + required: true + type: text diff --git a/apps/opencloud/9.1.0/docker-compose.yml b/apps/opencloud/9.1.0/docker-compose.yml new file mode 100644 index 000000000..4c308cb96 --- /dev/null +++ b/apps/opencloud/9.1.0/docker-compose.yml @@ -0,0 +1,98 @@ +services: + traefik: + image: "traefik:v3.7.17" + container_name: ${CONTAINER_NAME}-traefik + restart: unless-stopped + command: + - --log.level=ERROR + - --api.dashboard=false + - --ping=true + - --entryPoints.http.address=:${PANEL_APP_PORT_HTTP} + - --entryPoints.http.http.redirections.entryPoint.to=https + - --entryPoints.http.http.redirections.entryPoint.scheme=https + - --entryPoints.https.address=:${PANEL_APP_PORT_HTTPS} + - --entryPoints.https.transport.respondingTimeouts.readTimeout=12h + - --entryPoints.https.transport.respondingTimeouts.writeTimeout=12h + - --entryPoints.https.transport.respondingTimeouts.idleTimeout=3m + - --providers.file.directory=/etc/traefik/dynamic + - --providers.file.watch=true + networks: + opencloud-network: + aliases: + - ${OC_DOMAIN} + 1panel-network: + ports: + - "${PANEL_APP_PORT_HTTP}:${PANEL_APP_PORT_HTTP}" + - "${PANEL_APP_PORT_HTTPS}:${PANEL_APP_PORT_HTTPS}" + volumes: + - "${APP_DATA_DIR}/traefik/dynamic:/etc/traefik/dynamic:ro" + - "${APP_DATA_DIR}/traefik/certs:/certs:ro" + healthcheck: + test: + - CMD + - traefik + - healthcheck + - --ping + interval: 15s + timeout: 5s + start_period: 15s + retries: 5 + labels: + createdBy: "Apps" + + opencloud: + image: "opencloudeu/opencloud-rolling:9.1.0" + container_name: ${CONTAINER_NAME} + restart: unless-stopped + user: "1000:1000" + depends_on: + traefik: + condition: service_healthy + entrypoint: + - /bin/sh + command: + - -c + - opencloud init || true; exec opencloud server + environment: + - OC_URL=https://${OC_DOMAIN}:${PANEL_APP_PORT_HTTPS} + - OC_LOG_LEVEL=info + - OC_LOG_COLOR=false + - OC_LOG_PRETTY=false + - OC_INSECURE=true + - OC_ADD_RUN_SERVICES= + - PROXY_TLS=false + - PROXY_HTTP_ADDR=0.0.0.0:9200 + - PROXY_ENABLE_BASIC_AUTH=false + - PROXY_CSP_CONFIG_FILE_LOCATION=/etc/opencloud/csp.yaml + - IDM_CREATE_DEMO_USERS=false + - IDM_ADMIN_PASSWORD=${INITIAL_ADMIN_PASSWORD} + - OC_PASSWORD_POLICY_BANNED_PASSWORDS_LIST=banned-password-list.txt + - OC_SHARING_PUBLIC_SHARE_MUST_HAVE_PASSWORD=true + - OC_SHARING_PUBLIC_WRITEABLE_SHARE_MUST_HAVE_PASSWORD=false + - FRONTEND_CHECK_FOR_UPDATES=true + - TRAEFIK_PORT_HTTPS=:${PANEL_APP_PORT_HTTPS} + - TZ=${TZ} + networks: + - opencloud-network + volumes: + - "${APP_DATA_DIR}/config:/etc/opencloud" + - "${APP_DATA_DIR}/storage:/var/lib/opencloud" + - "${APP_DATA_DIR}/apps:/var/lib/opencloud/web/assets/apps" + - "./config/opencloud/csp.yaml:/etc/opencloud/csp.yaml:ro" + - "./config/opencloud/banned-password-list.txt:/etc/opencloud/banned-password-list.txt:ro" + healthcheck: + test: + - CMD-SHELL + - curl --fail --silent --show-error http://127.0.0.1:9200/status.php >/dev/null + interval: 30s + timeout: 10s + start_period: 120s + retries: 5 + labels: + createdBy: "Apps" + +networks: + opencloud-network: + name: ${CONTAINER_NAME}-network + 1panel-network: + external: true diff --git a/apps/opencloud/9.1.0/scripts/init.sh b/apps/opencloud/9.1.0/scripts/init.sh new file mode 100755 index 000000000..8ffe591f0 --- /dev/null +++ b/apps/opencloud/9.1.0/scripts/init.sh @@ -0,0 +1,140 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" +ENV_FILE="${ENV_FILE:-${ROOT_DIR}/.env}" + +fail() { + printf '%s\n' "$1" >&2 + exit 1 +} + +ensure_no_symlink_components() { + local path="$1" + local current="/" + local part + local -a components + + [[ "$path" == /* ]] || fail "Path must be absolute: $path" + IFS='/' read -r -a components <<< "$path" + for part in "${components[@]}"; do + case "$part" in + "" | .) continue ;; + ..) + if [[ "$current" == /*/* ]]; then + current="${current%/*}" + else + current="/" + fi + ;; + *) + if [[ "$current" == / ]]; then + current="/$part" + else + current="$current/$part" + fi + ;; + esac + [[ ! -L "$current" ]] || fail "Data path must not contain a symbolic link: $current" + done +} + +read_env_value() { + local key="$1" + local value="" + + if [[ -f "$ENV_FILE" ]]; then + value="$(grep -E "^${key}=" "$ENV_FILE" | tail -n 1 | cut -d '=' -f 2- || true)" + fi + if [[ ${#value} -ge 2 && "${value:0:1}" == '"' && "${value: -1}" == '"' ]]; then + value="${value:1:${#value}-2}" + elif [[ ${#value} -ge 2 && "${value:0:1}" == "'" && "${value: -1}" == "'" ]]; then + value="${value:1:${#value}-2}" + fi + printf '%s\n' "$value" +} + +if [[ ${APP_DATA_DIR+x} ]]; then + APP_DATA_DIR_RAW="$APP_DATA_DIR" +else + APP_DATA_DIR_RAW="$(read_env_value APP_DATA_DIR)" +fi +APP_DATA_DIR_RAW="${APP_DATA_DIR_RAW:-./data}" + +if [[ ${OC_DOMAIN+x} ]]; then + OC_DOMAIN_RAW="$OC_DOMAIN" +else + OC_DOMAIN_RAW="$(read_env_value OC_DOMAIN)" +fi + +[[ -n "$APP_DATA_DIR_RAW" ]] || fail "APP_DATA_DIR must not be empty" +case "$APP_DATA_DIR_RAW" in + *$'\n'* | *$'\r'* | *'$'* | *'#'*) fail "APP_DATA_DIR contains unsupported dotenv characters" ;; +esac + +case "$APP_DATA_DIR_RAW" in + /*) + ensure_no_symlink_components "$APP_DATA_DIR_RAW" + APP_DATA_DIR_ABS="$(realpath -m -- "$APP_DATA_DIR_RAW")" + ;; + *) + ensure_no_symlink_components "${ROOT_DIR}/${APP_DATA_DIR_RAW#./}" + APP_DATA_DIR_ABS="$(realpath -m -- "${ROOT_DIR}/${APP_DATA_DIR_RAW#./}")" + case "$APP_DATA_DIR_ABS" in + "$ROOT_DIR" | "$ROOT_DIR"/*) ;; + *) fail "Relative APP_DATA_DIR must stay inside the application directory" ;; + esac + ;; +esac + +[[ "$APP_DATA_DIR_ABS" != / ]] || fail "APP_DATA_DIR must not be the filesystem root" +[[ ! -L "$APP_DATA_DIR_ABS" ]] || fail "APP_DATA_DIR must not be a symbolic link" +if [[ -e "$APP_DATA_DIR_ABS" && ! -d "$APP_DATA_DIR_ABS" ]]; then + fail "APP_DATA_DIR must be a directory" +fi + +opencloud_paths=( + "$APP_DATA_DIR_ABS/config" + "$APP_DATA_DIR_ABS/storage" + "$APP_DATA_DIR_ABS/apps" +) +traefik_paths=( + "$APP_DATA_DIR_ABS/traefik/dynamic" + "$APP_DATA_DIR_ABS/traefik/certs" +) + +for path in "${opencloud_paths[@]}" "${traefik_paths[@]}"; do + ensure_no_symlink_components "$path" +done + +mkdir -p -- "${opencloud_paths[@]}" "${traefik_paths[@]}" +for path in "${opencloud_paths[@]}"; do + chown 1000:1000 -- "$path" 2>/dev/null || true +done + +[[ "$OC_DOMAIN_RAW" =~ ^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*$ ]] || fail "OC_DOMAIN must be a hostname without a scheme, port, or path" +[[ ${#OC_DOMAIN_RAW} -le 253 ]] || fail "OC_DOMAIN is too long" + +route_template="$ROOT_DIR/config/traefik/opencloud.yml" +route_file="$APP_DATA_DIR_ABS/traefik/dynamic/1panel-opencloud-route.yml" +[[ -f "$route_template" && ! -L "$route_template" ]] || fail "OpenCloud Traefik route template is missing or invalid" +[[ ! -L "$route_file" ]] || fail "OpenCloud Traefik route must not be a symbolic link: $route_file" +if [[ -e "$route_file" && ! -f "$route_file" ]]; then + fail "OpenCloud Traefik route must be a regular file: $route_file" +fi + +if [[ ! -e "$route_file" ]]; then + route_tmp="$(mktemp "${route_file}.tmp.XXXXXX")" + if ! sed "s/@OC_DOMAIN@/${OC_DOMAIN_RAW}/g" "$route_template" > "$route_tmp"; then + rm -f -- "$route_tmp" + fail "Could not create OpenCloud Traefik route" + fi + chmod 0644 -- "$route_tmp" + if ! ln -- "$route_tmp" "$route_file" 2>/dev/null; then + if [[ ! -f "$route_file" || -L "$route_file" ]]; then + rm -f -- "$route_tmp" + fail "Could not create OpenCloud Traefik route" + fi + fi + rm -f -- "$route_tmp" +fi diff --git a/apps/opencloud/9.1.0/scripts/uninstall.sh b/apps/opencloud/9.1.0/scripts/uninstall.sh new file mode 100755 index 000000000..5c199242b --- /dev/null +++ b/apps/opencloud/9.1.0/scripts/uninstall.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail + +if command -v docker-compose >/dev/null 2>&1; then + docker-compose down --remove-orphans +else + docker compose down --remove-orphans +fi diff --git a/apps/opencloud/9.1.0/scripts/upgrade.sh b/apps/opencloud/9.1.0/scripts/upgrade.sh new file mode 100755 index 000000000..593b31667 --- /dev/null +++ b/apps/opencloud/9.1.0/scripts/upgrade.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +exec bash "${SCRIPT_DIR}/init.sh" diff --git a/apps/opencloud/README.md b/apps/opencloud/README.md index 3e28621a2..d3d32126b 100644 --- a/apps/opencloud/README.md +++ b/apps/opencloud/README.md @@ -14,8 +14,8 @@ OpenCloud 是一个开源的文件管理、共享与协作平台,提供 Web ## 来源与版本 - OpenCloud 源码仓库由 `opencloud-eu` 官方组织维护,采用 Apache License 2.0。 -- 本应用基于官方 `opencloud-compose` 的基础服务与 Traefik TLS 部署方式,Compose 证据固定到提交 `8d2d89f283faa410bc9ed9f63e8247f6518d5c43`。 -- OpenCloud [`v8.1.0`](https://github.com/opencloud-eu/opencloud/releases/tag/v8.1.0) 于 2026-10-05 作为 rolling release 发布。本应用使用官方镜像 `opencloudeu/opencloud-rolling:8.1.0` 的版本化 tag,不使用浮动 `latest` tag。 +- 本应用基于官方 `opencloud-compose` 的基础服务与 Traefik TLS 部署方式,并在初始化时将随包提供的 file-provider 路由配置安全地创建到持久化目录;Compose 证据固定到提交 `8d2d89f283faa410bc9ed9f63e8247f6518d5c43`。 +- 本应用使用官方版本化镜像 tag,不使用浮动 `latest` tag。 - OpenCloud 与 Traefik 镜像均提供 `linux/amd64` 和 `linux/arm64` manifest。 ## 部署拓扑 @@ -25,7 +25,7 @@ OpenCloud 是一个开源的文件管理、共享与协作平台,提供 Web - `opencloud`:使用官方 OpenCloud 镜像,以 UID/GID `1000:1000` 运行并持久化配置、文件数据和 Web 扩展。 - `traefik`:使用官方 OpenCloud Compose 指定的 Traefik 版本,为 OpenCloud 提供必须的 HTTPS 入口和 HTTP 到 HTTPS 跳转。 -两个 service 通过以 1Panel 实例容器名命名的专用 Docker 网络联动;OpenCloud 后端只加入该专用网络,Traefik 另行加入 `1panel-network` 作为商店入口。每个 Traefik 通过实例标签只发现对应的 OpenCloud 容器,不同安装不会互相加载或覆盖同名路由。 +两个 service 通过以 1Panel 实例容器名命名的专用 Docker 网络联动;OpenCloud 后端只加入该专用网络,Traefik 另行加入 `1panel-network` 作为商店入口。Traefik 从应用包内的 file-provider 配置读取路由,并通过专用网络访问 `opencloud:9200`,不需要发现或控制 Docker 容器。 OpenCloud 的 IDM、IDP、NATS、存储和 Web 服务由同一个官方镜像内部管理,不需要 MySQL、PostgreSQL、Redis 或其他 1Panel Runtime。可选的 Collabora、全文检索、杀毒、外部 LDAP/Keycloak 等扩展没有包含在此基础应用中。 @@ -57,15 +57,17 @@ tls: - `${APP_DATA_DIR}/config`:OpenCloud 生成的配置和随机密钥。 - `${APP_DATA_DIR}/storage`:用户文件、元数据和应用状态。 - `${APP_DATA_DIR}/apps`:本地 Web 扩展。 -- `${APP_DATA_DIR}/traefik`:TLS 动态配置和证书。 +- `${APP_DATA_DIR}/traefik/dynamic/1panel-opencloud-route.yml`:应用首次初始化时创建的 OpenCloud 路由。已存在的文件不会被覆盖。 +- `${APP_DATA_DIR}/traefik/dynamic`:可选 TLS 动态配置。 +- `${APP_DATA_DIR}/traefik/certs`:TLS 证书。 初始管理员密码只在空数据目录第一次启动时生效,后续修改 `.env` 不会重置密码。升级前必须整体备份 `${APP_DATA_DIR}`;配置、存储和证书应作为同一个一致性边界备份和恢复。 -`scripts/upgrade.sh` 只复用安装初始化逻辑:创建缺失目录,并校正 OpenCloud 持久化目录顶层的 UID/GID,不递归改写文件、不删除数据、不重建配置。卸载脚本只停止 Compose service,不删除 `${APP_DATA_DIR}`。 +`scripts/upgrade.sh` 复用安装初始化逻辑:创建缺失目录和缺失的 OpenCloud Traefik 路由,并校正 OpenCloud 持久化目录顶层的 UID/GID,不递归改写已有文件、不删除数据。卸载脚本只停止 Compose service,不删除 `${APP_DATA_DIR}`。 ## 权限与安全 -Traefik 按官方 Compose 方案以只读方式挂载 Docker Socket,用于读取容器标签和发现 OpenCloud 路由。即使挂载标记为只读,Docker Socket 仍属于高权限主机接口;只应在可信主机上使用,并保持 Traefik 镜像及时更新。Compose 已关闭 `exposedByDefault`,Traefik 只处理明确标记的 OpenCloud service。 +Traefik 使用应用包生成并保存在 `${APP_DATA_DIR}` 中的静态路由配置,不挂载 Docker Socket,也不读取其他容器的标签。HTTP 请求经 Traefik 转发至同一专用网络中的 OpenCloud 服务。 OpenCloud service 不使用 privileged、host network、额外 capability 或设备映射。请使用强管理员密码、受信任 TLS 证书和主机防火墙,并限制不必要的公网入口。 @@ -79,9 +81,9 @@ OpenCloud service 不使用 privileged、host network、额外 capability 或设 ## Introduction -OpenCloud is an open-source platform for file management, sharing, and collaboration. This package follows the official OpenCloud Compose topology: one fixed-version OpenCloud container and one Traefik container providing the HTTPS endpoint required by the built-in identity provider. +OpenCloud is an open-source platform for file management, sharing, and collaboration. This package follows the official OpenCloud Compose service topology and creates a package-provided Traefik file-provider route in persistent app data to serve OpenCloud over HTTPS without Docker API access. Existing route files are preserved. -OpenCloud [v8.1.0](https://github.com/opencloud-eu/opencloud/releases/tag/v8.1.0) is an official rolling release published on 2026-10-05. This package uses the versioned official image tag `opencloudeu/opencloud-rolling:8.1.0` rather than a floating `latest` tag. +This package uses versioned official image tags rather than a floating `latest` tag. A trusted certificate is required for production. Back up the complete `${APP_DATA_DIR}` before upgrading. The lifecycle scripts do not delete persisted data or recursively rewrite existing files. diff --git a/apps/opencloud/latest/.env.sample b/apps/opencloud/latest/.env.sample index 2b82bb1bc..a50b5cb8e 100644 --- a/apps/opencloud/latest/.env.sample +++ b/apps/opencloud/latest/.env.sample @@ -4,5 +4,4 @@ PANEL_APP_PORT_HTTP=4080 OC_DOMAIN=cloud.opencloud.test INITIAL_ADMIN_PASSWORD=replace-with-a-strong-password APP_DATA_DIR=./data -DOCKER_SOCKET_PATH=/var/run/docker.sock TZ=Asia/Shanghai diff --git a/apps/opencloud/latest/config/traefik/opencloud.yml b/apps/opencloud/latest/config/traefik/opencloud.yml new file mode 100644 index 000000000..d8ff05c2e --- /dev/null +++ b/apps/opencloud/latest/config/traefik/opencloud.yml @@ -0,0 +1,13 @@ +http: + routers: + opencloud: + entryPoints: + - https + rule: "Host(`@OC_DOMAIN@`)" + service: opencloud + tls: {} + services: + opencloud: + loadBalancer: + servers: + - url: http://opencloud:9200 diff --git a/apps/opencloud/latest/data.yml b/apps/opencloud/latest/data.yml index dbfbbf012..d6b7221f9 100644 --- a/apps/opencloud/latest/data.yml +++ b/apps/opencloud/latest/data.yml @@ -84,22 +84,6 @@ additionalProperties: pt-br: Diretório de dados required: true type: text - - default: /var/run/docker.sock - edit: true - envKey: DOCKER_SOCKET_PATH - labelEn: Docker Socket Path - labelZh: Docker Socket 路径 - label: - en: Docker Socket Path - zh: Docker Socket 路径 - zh-Hant: Docker Socket 路徑 - ja: Docker Socket パス - ko: Docker Socket 경로 - ru: Путь к Docker Socket - ms: Laluan Docker Socket - pt-br: Caminho do Docker Socket - required: true - type: text - default: Asia/Shanghai edit: true envKey: TZ diff --git a/apps/opencloud/latest/docker-compose.yml b/apps/opencloud/latest/docker-compose.yml index 560f3cf15..f18f8987d 100644 --- a/apps/opencloud/latest/docker-compose.yml +++ b/apps/opencloud/latest/docker-compose.yml @@ -14,10 +14,6 @@ services: - --entryPoints.https.transport.respondingTimeouts.readTimeout=12h - --entryPoints.https.transport.respondingTimeouts.writeTimeout=12h - --entryPoints.https.transport.respondingTimeouts.idleTimeout=3m - - --providers.docker.endpoint=unix:///var/run/docker.sock - - --providers.docker.exposedByDefault=false - - --providers.docker.network=${CONTAINER_NAME}-network - - "--providers.docker.constraints=Label(`opencloud.instance`,`${CONTAINER_NAME}`)" - --providers.file.directory=/etc/traefik/dynamic - --providers.file.watch=true networks: @@ -29,7 +25,6 @@ services: - "${PANEL_APP_PORT_HTTP}:${PANEL_APP_PORT_HTTP}" - "${PANEL_APP_PORT_HTTPS}:${PANEL_APP_PORT_HTTPS}" volumes: - - "${DOCKER_SOCKET_PATH}:/var/run/docker.sock:ro" - "${APP_DATA_DIR}/traefik/dynamic:/etc/traefik/dynamic:ro" - "${APP_DATA_DIR}/traefik/certs:/certs:ro" healthcheck: @@ -95,13 +90,6 @@ services: retries: 5 labels: createdBy: "Apps" - opencloud.instance: "${CONTAINER_NAME}" - traefik.enable: "true" - traefik.http.routers.opencloud.entrypoints: https - traefik.http.routers.opencloud.rule: Host(`${OC_DOMAIN}`) - traefik.http.routers.opencloud.service: opencloud - traefik.http.routers.opencloud.tls: "true" - traefik.http.services.opencloud.loadbalancer.server.port: "9200" networks: opencloud-network: diff --git a/apps/opencloud/latest/scripts/init.sh b/apps/opencloud/latest/scripts/init.sh index 9bc6c554a..8ffe591f0 100755 --- a/apps/opencloud/latest/scripts/init.sh +++ b/apps/opencloud/latest/scripts/init.sh @@ -9,6 +9,36 @@ fail() { exit 1 } +ensure_no_symlink_components() { + local path="$1" + local current="/" + local part + local -a components + + [[ "$path" == /* ]] || fail "Path must be absolute: $path" + IFS='/' read -r -a components <<< "$path" + for part in "${components[@]}"; do + case "$part" in + "" | .) continue ;; + ..) + if [[ "$current" == /*/* ]]; then + current="${current%/*}" + else + current="/" + fi + ;; + *) + if [[ "$current" == / ]]; then + current="/$part" + else + current="$current/$part" + fi + ;; + esac + [[ ! -L "$current" ]] || fail "Data path must not contain a symbolic link: $current" + done +} + read_env_value() { local key="$1" local value="" @@ -31,14 +61,24 @@ else fi APP_DATA_DIR_RAW="${APP_DATA_DIR_RAW:-./data}" +if [[ ${OC_DOMAIN+x} ]]; then + OC_DOMAIN_RAW="$OC_DOMAIN" +else + OC_DOMAIN_RAW="$(read_env_value OC_DOMAIN)" +fi + [[ -n "$APP_DATA_DIR_RAW" ]] || fail "APP_DATA_DIR must not be empty" case "$APP_DATA_DIR_RAW" in *$'\n'* | *$'\r'* | *'$'* | *'#'*) fail "APP_DATA_DIR contains unsupported dotenv characters" ;; esac case "$APP_DATA_DIR_RAW" in - /*) APP_DATA_DIR_ABS="$(realpath -m -- "$APP_DATA_DIR_RAW")" ;; + /*) + ensure_no_symlink_components "$APP_DATA_DIR_RAW" + APP_DATA_DIR_ABS="$(realpath -m -- "$APP_DATA_DIR_RAW")" + ;; *) + ensure_no_symlink_components "${ROOT_DIR}/${APP_DATA_DIR_RAW#./}" APP_DATA_DIR_ABS="$(realpath -m -- "${ROOT_DIR}/${APP_DATA_DIR_RAW#./}")" case "$APP_DATA_DIR_ABS" in "$ROOT_DIR" | "$ROOT_DIR"/*) ;; @@ -63,8 +103,38 @@ traefik_paths=( "$APP_DATA_DIR_ABS/traefik/certs" ) +for path in "${opencloud_paths[@]}" "${traefik_paths[@]}"; do + ensure_no_symlink_components "$path" +done + mkdir -p -- "${opencloud_paths[@]}" "${traefik_paths[@]}" for path in "${opencloud_paths[@]}"; do - [[ ! -L "$path" ]] || fail "OpenCloud data subdirectory must not be a symbolic link: $path" chown 1000:1000 -- "$path" 2>/dev/null || true done + +[[ "$OC_DOMAIN_RAW" =~ ^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*$ ]] || fail "OC_DOMAIN must be a hostname without a scheme, port, or path" +[[ ${#OC_DOMAIN_RAW} -le 253 ]] || fail "OC_DOMAIN is too long" + +route_template="$ROOT_DIR/config/traefik/opencloud.yml" +route_file="$APP_DATA_DIR_ABS/traefik/dynamic/1panel-opencloud-route.yml" +[[ -f "$route_template" && ! -L "$route_template" ]] || fail "OpenCloud Traefik route template is missing or invalid" +[[ ! -L "$route_file" ]] || fail "OpenCloud Traefik route must not be a symbolic link: $route_file" +if [[ -e "$route_file" && ! -f "$route_file" ]]; then + fail "OpenCloud Traefik route must be a regular file: $route_file" +fi + +if [[ ! -e "$route_file" ]]; then + route_tmp="$(mktemp "${route_file}.tmp.XXXXXX")" + if ! sed "s/@OC_DOMAIN@/${OC_DOMAIN_RAW}/g" "$route_template" > "$route_tmp"; then + rm -f -- "$route_tmp" + fail "Could not create OpenCloud Traefik route" + fi + chmod 0644 -- "$route_tmp" + if ! ln -- "$route_tmp" "$route_file" 2>/dev/null; then + if [[ ! -f "$route_file" || -L "$route_file" ]]; then + rm -f -- "$route_tmp" + fail "Could not create OpenCloud Traefik route" + fi + fi + rm -f -- "$route_tmp" +fi