diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml new file mode 100644 index 000000000..ae361f656 --- /dev/null +++ b/.github/workflows/docker-build.yml @@ -0,0 +1,80 @@ +name: Docker Build and Push + +on: + push: + branches: + - master + - develop + +jobs: + docker-build: + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + with: + platforms: all + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + platforms: linux/amd64,linux/arm64 + + - name: Create and use multi-platform builder + run: | + docker buildx create --name multiarch --use --bootstrap + docker buildx inspect multiarch + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: x86_64-unknown-linux-musl,aarch64-unknown-linux-musl,wasm32-unknown-unknown + + - name: Cache cargo tools + id: cache-tools + uses: actions/cache@v3 + with: + path: ~/.cargo/bin + key: ${{ runner.os }}-cargo-tools-cross-trunk + + - name: Install cross + if: steps.cache-tools.outputs.cache-hit != 'true' + run: cargo install cross --git https://github.com/cross-rs/cross + + - name: Install trunk + if: steps.cache-tools.outputs.cache-hit != 'true' + run: cargo install trunk + + - name: Cache cargo registry + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry/index + ~/.cargo/registry/cache + ~/.cargo/git/db + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + ${{ runner.os }}-cargo- + + - name: Extract branch name + shell: bash + run: echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_OUTPUT + id: extract_branch + + - name: Create GHCR token file + run: | + mkdir -p $HOME + echo "GHCR_IO_TOKEN=${{ secrets.GHCR_IO_TOKEN }}" > $HOME/.ghcr.io + + - name: Make build script executable + run: chmod +x ./bin/build_docker.sh + + - name: Build and push Docker images + run: ./bin/build_docker.sh ${{ steps.extract_branch.outputs.branch }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d5d25541..5936209a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ - EPG Config View - Fixed loading users for WebUI from user DB - Fixed auto EPG for batch inputs +- ContentSecurityPolicy can be enabled # 3.1.5 (2025-08-14) - Hot reload for config diff --git a/README.md b/README.md index 2121f6939..9b775882f 100644 --- a/README.md +++ b/README.md @@ -323,6 +323,8 @@ log: ### 1.10 `web_ui` - enabled: default is true, if set to false the web_ui is disabled - user_ui_enabled, true or false, for user bouquet editor +- content_security_policy: default false; when true, sends a Content-Security-Policy (CSP) header to help protect against cross-site scripting (XSS). + Not: enabling CSP may block external images/logos unless allowed via the img-src directive. - path is for web_ui path like `/ui` for reverse proxy integration if necessary. - auth for authentication settings - `enabled` can be deactivated if `enabled` is set to `false`. If not set default is `true`. diff --git a/backend/src/api/endpoints/web_index.rs b/backend/src/api/endpoints/web_index.rs index ce6004b3c..d86a3f200 100644 --- a/backend/src/api/endpoints/web_index.rs +++ b/backend/src/api/endpoints/web_index.rs @@ -162,11 +162,14 @@ async fn index( new_content = new_content.replacen(script_tag, &new_tag, 1); } - return try_unwrap_body!(axum::response::Response::builder() - .header("Content-Type", mime::TEXT_HTML_UTF_8.as_ref()) - .header("Content-Security-Policy", - format!("default-src 'self'; script-src 'self' 'unsafe-eval' 'nonce-{nonce_b64}'; style-src 'self' 'nonce-{nonce_b64}'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'") - ).body(new_content)); + let mut builder = axum::response::Response::builder() + .header("Content-Type", mime::TEXT_HTML_UTF_8.as_ref()); + if config.web_ui.as_ref().is_some_and(|w| w.content_security_policy) { + builder = builder.header("Content-Security-Policy", + format!("default-src 'self'; script-src 'self' 'unsafe-eval' 'nonce-{nonce_b64}'; style-src 'self' 'nonce-{nonce_b64}'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'")); + + } + return try_unwrap_body!(builder.body(new_content)); } Err(err) => { error!("Failed to read web ui index.html: {err}"); diff --git a/backend/src/model/config/web_ui.rs b/backend/src/model/config/web_ui.rs index 1e9caf72a..8474783c6 100644 --- a/backend/src/model/config/web_ui.rs +++ b/backend/src/model/config/web_ui.rs @@ -6,6 +6,7 @@ use crate::model::{macros, WebAuthConfig}; pub struct WebUiConfig { pub enabled: bool, pub user_ui_enabled: bool, + pub content_security_policy: bool, pub path: Option, pub auth: Option, pub player_server: Option, @@ -30,6 +31,7 @@ impl From<&WebUiConfigDto> for WebUiConfig { Self { enabled: dto.enabled, user_ui_enabled: dto.user_ui_enabled, + content_security_policy: dto.content_security_policy, path: dto.path.clone(), auth: dto.auth.as_ref().map(Into::into), player_server: dto.player_server.clone(), @@ -41,6 +43,7 @@ impl From<&WebUiConfig> for WebUiConfigDto { Self { enabled: instance.enabled, user_ui_enabled: instance.user_ui_enabled, + content_security_policy: instance.content_security_policy, path: instance.path.clone(), auth: instance.auth.as_ref().map(Into::into), player_server: instance.player_server.clone(), diff --git a/bin/build_docker.sh b/bin/build_docker.sh new file mode 100644 index 000000000..a9368caf5 --- /dev/null +++ b/bin/build_docker.sh @@ -0,0 +1,168 @@ +#!/bin/bash +set -euo pipefail + +# Check for required environment variable +if [ ! -f "${HOME}/.ghcr.io" ]; then + echo "๐Ÿงจ Error: ${HOME}/.ghcr.io file not found" + exit 1 +fi +source "${HOME}/.ghcr.io" + +# Function to print usage instructions +print_usage() { + echo "Usage: $(basename "$0") " + echo + echo "Arguments:" + echo " branch Git branch name (only 'master' and 'develop' are supported)" + echo + echo "Examples:" + echo " $(basename "$0") master # Builds and pushes with :latest tag" + echo " $(basename "$0") develop # Builds and pushes with :dev tag" + exit 1 +} + +# Validate arguments +if [ $# -ne 1 ]; then + echo "๐Ÿงจ Error: Exactly one argument required" + print_usage +fi + +BRANCH="$1" + +# Validate branch +case "$BRANCH" in + master) + TAG_SUFFIX="latest" + ;; + develop) + TAG_SUFFIX="dev" + ;; + *) + echo "๐Ÿงจ Error: Branch '$BRANCH' is not supported. Only 'master' and 'develop' are allowed." + exit 1 + ;; +esac + +echo "๐Ÿš€ Building Docker images for branch: $BRANCH (tag: $TAG_SUFFIX)" + +# Set up directories +WORKING_DIR=$(pwd) +BIN_DIR="${WORKING_DIR}/bin" +RESOURCES_DIR="${WORKING_DIR}/resources" +DOCKER_DIR="${WORKING_DIR}/docker" +BACKEND_DIR="${WORKING_DIR}/backend" +FRONTEND_DIR="${WORKING_DIR}/frontend" +FRONTEND_BUILD_DIR="${FRONTEND_DIR}/dist" + +# Define architectures and their corresponding builds +declare -A ARCHITECTURES=( + [LINUX]=x86_64-unknown-linux-musl + [AARCH64]=aarch64-unknown-linux-musl +) + +# Images to build with multi-platform support +declare -A MULTI_PLATFORM_IMAGES=( + [tuliprox]="scratch-final" + [tuliprox-alpine]="alpine-final" +) + +# Get version from Cargo.toml +VERSION=$(grep -Po '^version\s*=\s*"\K[0-9\.]+' "${BACKEND_DIR}/Cargo.toml") +if [ -z "${VERSION}" ]; then + echo "๐Ÿงจ Error: Failed to determine the version from Cargo.toml" + exit 1 +fi + +echo "๐Ÿ“ฆ Version: ${VERSION}" + +# Build resources if needed +if [ ! -f "${BIN_DIR}/build_resources.sh" ]; then + echo "๐Ÿ› ๏ธ Building resources..." + "${BIN_DIR}/build_resources.sh" +fi + +# Build frontend +echo "๐ŸŽจ Building frontend..." +rm -rf "${FRONTEND_BUILD_DIR}" +cd "${FRONTEND_DIR}" && env RUSTFLAGS="--remap-path-prefix $HOME=~" trunk build --release + +# Check if the frontend build directory exists +if [ ! -d "${FRONTEND_BUILD_DIR}" ]; then + echo "๐Ÿงจ Error: Frontend build directory '${FRONTEND_BUILD_DIR}' does not exist" + exit 1 +fi + +cd "$WORKING_DIR" + +# Build binaries for all architectures first +echo "๐Ÿ—๏ธ Building binaries for all architectures..." +for PLATFORM in "${!ARCHITECTURES[@]}"; do + ARCHITECTURE=${ARCHITECTURES[$PLATFORM]} + + echo "๐Ÿ”จ Building binary for architecture: $ARCHITECTURE" + + cargo clean || true + env RUSTFLAGS="--remap-path-prefix $HOME=~" cross build -p tuliprox --release --target "$ARCHITECTURE" + + BINARY_PATH="${WORKING_DIR}/target/${ARCHITECTURE}/release/tuliprox" + if [ ! -f "$BINARY_PATH" ]; then + echo "๐Ÿงจ Error: Binary $BINARY_PATH does not exist" + exit 1 + fi + + # Copy binary with architecture suffix for multi-platform build + mkdir -p "${DOCKER_DIR}/binaries" + cp "$BINARY_PATH" "${DOCKER_DIR}/binaries/tuliprox-${ARCHITECTURE}" +done + +# Prepare common Docker context +echo "๐Ÿ“‹ Preparing Docker context..." +rm -rf "${DOCKER_DIR}/web" +rm -rf "${DOCKER_DIR}/resources" +cp -r "${FRONTEND_BUILD_DIR}" "${DOCKER_DIR}/web" +cp -r "${RESOURCES_DIR}" "${DOCKER_DIR}/resources" + +cd "${DOCKER_DIR}" + +# Login to GitHub Container Registry (needed before buildx push) +echo "๐Ÿ”‘ Logging into GitHub Container Registry..." +docker login ghcr.io -u euzu -p "${GHCR_IO_TOKEN}" + +declare -a BUILT_IMAGES=() + +# Build multi-platform images +for IMAGE_NAME in "${!MULTI_PLATFORM_IMAGES[@]}"; do + BUILD_TARGET="${MULTI_PLATFORM_IMAGES[$IMAGE_NAME]}" + + echo "๐ŸŽฏ Building multi-platform image: ${IMAGE_NAME} with target ${BUILD_TARGET}" + + # Build and push multi-platform image directly + docker buildx build -f Dockerfile-manual \ + -t "ghcr.io/euzu/${IMAGE_NAME}:${VERSION}" \ + -t "ghcr.io/euzu/${IMAGE_NAME}:${TAG_SUFFIX}" \ + --target "$BUILD_TARGET" \ + --platform "linux/amd64,linux/arm64" \ + --push \ + . + + BUILT_IMAGES+=("ghcr.io/euzu/${IMAGE_NAME}:${VERSION}") + BUILT_IMAGES+=("ghcr.io/euzu/${IMAGE_NAME}:${TAG_SUFFIX}") +done + +# Clean up Docker context +echo "๐Ÿ—‘๏ธ Cleaning up binaries..." +rm -rf "${DOCKER_DIR}/binaries" + +cd "$WORKING_DIR" + +# Final cleanup +echo "๐Ÿ—‘๏ธ Cleaning up Docker context..." +rm -rf "${DOCKER_DIR}/web" +rm -f "${DOCKER_DIR}/tuliprox" +rm -rf "${DOCKER_DIR}/resources" + +echo "๐ŸŽ‰ Docker images for branch '$BRANCH' (version ${VERSION}) have been successfully built and pushed!" +echo "๐Ÿ“‹ Built images:" +for img in "${BUILT_IMAGES[@]}"; do + echo " - $img" +done diff --git a/bin/build_github_docker.sh b/bin/build_github_docker.sh deleted file mode 100755 index e848b0e5f..000000000 --- a/bin/build_github_docker.sh +++ /dev/null @@ -1,103 +0,0 @@ -#!/bin/bash -set -euo pipefail -source "${HOME}/.ghcr.io" - -WORKING_DIR=$(pwd) -BIN_DIR="${WORKING_DIR}/bin" -RESOURCES_DIR="${WORKING_DIR}/resources" -DOCKER_DIR="${WORKING_DIR}/docker" -BACKEND_DIR="${WORKING_DIR}/backend" -FRONTEND_DIR="${WORKING_DIR}/frontend" -FRONTEND_BUILD_DIR="${FRONTEND_DIR}/dist" -declare -A ARCHITECTURES=( - [LINUX]=x86_64-unknown-linux-musl - [AARCH64]=aarch64-unknown-linux-musl -) - -declare -A BUILDS=( - [LINUX]="tuliprox:scratch-final tuliprox-alpine:alpine-final" - [AARCH64]="tuliprox-aarch64:scratch-final" -) - - -if [ ! -f "${BIN_DIR}/build_resources.sh" ]; then - "${BIN_DIR}/build_resources.sh" -fi - -rm -rf "${FRONTEND_BUILD_DIR}" -cd "${FRONTEND_DIR}" && env RUSTFLAGS="--remap-path-prefix $HOME=~" trunk build --release - -# Check if the frontend build directory exists -if [ ! -d "${FRONTEND_BUILD_DIR}" ]; then - echo "Error: Web directory '${FRONTEND_BUILD_DIR}' does not exist." - exit 1 -fi - -cd "$WORKING_DIR" - - -VERSION=$(grep -Po '^version\s*=\s*"\K[0-9\.]+' "${BACKEND_DIR}/Cargo.toml") -if [ -z "${VERSION}" ]; then - echo "๐Ÿงจ Error: Failed to determine the version." - exit 1 -fi - -declare -a BUILT_IMAGES=() - -# Start build loop per platform -for PLATFORM in "${!ARCHITECTURES[@]}"; do - ARCHITECTURE=${ARCHITECTURES[$PLATFORM]} - - echo "๐Ÿ› ๏ธ Building binary for architecture: $ARCHITECTURE" - - cargo clean || true - env RUSTFLAGS="--remap-path-prefix $HOME=~" cross build -p tuliprox --release --target "$ARCHITECTURE" - - BINARY_PATH="${WORKING_DIR}/target/${ARCHITECTURE}/release/tuliprox" - if [ ! -f "$BINARY_PATH" ]; then - echo "๐Ÿงจ Error: Binary $BINARY_PATH does not exist." - exit 1 - fi - - # Prepare Docker context - cp "$BINARY_PATH" "${DOCKER_DIR}/" - rm -rf "${DOCKER_DIR}/web" - cp -r "${FRONTEND_BUILD_DIR}" "${DOCKER_DIR}/web" - cp -r "${RESOURCES_DIR}" "${DOCKER_DIR}/resources" - - cd "${DOCKER_DIR}" - echo "๐Ÿ› ๏ธ Building Docker images for platform: $PLATFORM, version: ${VERSION}" - - for pair in ${BUILDS[$PLATFORM]}; do - IMAGE_NAME="${pair%%:*}" - BUILD_TARGET="${pair##*:}" - - echo "๐ŸŽฏ Building ${IMAGE_NAME} with target ${BUILD_TARGET}" - - docker build -f Dockerfile-manual \ - -t "ghcr.io/euzu/${IMAGE_NAME}:${VERSION}" \ - --target "$BUILD_TARGET" . - - docker tag "ghcr.io/euzu/${IMAGE_NAME}:${VERSION}" "ghcr.io/euzu/${IMAGE_NAME}:latest" - - BUILT_IMAGES+=("ghcr.io/euzu/${IMAGE_NAME}:${VERSION}") - BUILT_IMAGES+=("ghcr.io/euzu/${IMAGE_NAME}:latest") - done -done - -cd "$WORKING_DIR" - -echo "๐Ÿ”‘ Logging into GitHub Container Registry..." -docker login ghcr.io -u euzu -p "${GHCR_IO_TOKEN}" -for img in "${BUILT_IMAGES[@]}"; do - echo "๐Ÿ“ฆ Pushing $img" - docker push "$img" -done - -# Clean up -echo "๐Ÿ—‘ Cleaning up Docker context..." -rm -rf "${DOCKER_DIR}/web" -rm -f "${DOCKER_DIR}/tuliprox" -rm -rf "${DOCKER_DIR}/resources" - -echo "๐ŸŽ‰ Docker images for version ${VERSION} have been successfully built, tagged, and pushed." diff --git a/bin/build_github_docker_aarch64.sh b/bin/build_github_docker_aarch64.sh deleted file mode 100755 index 96ee731ae..000000000 --- a/bin/build_github_docker_aarch64.sh +++ /dev/null @@ -1,106 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -source "${HOME}/.ghcr.io" - -# Function to print usage instructions -print_usage() { - echo "Usage: $(basename "$0") [-r] [-h]" - echo - echo "Options:" - echo " -r Enable release image mode (default: false)" - echo " -h Display this help message" - exit 0 -} - -beta_image=true - -# parse options -while getopts "rh" opt; do - case $opt in - r) beta_image=false ;; - h) print_usage ;; - \?) echo "Unknown option: -$OPTARG" >&2 ;; - esac -done - -WORKING_DIR=$(pwd) -BIN_DIR="${WORKING_DIR}/bin" -RESOURCES_DIR="${WORKING_DIR}/resources" -DOCKER_DIR="${WORKING_DIR}/docker" -BACKEND_DIR="${WORKING_DIR}/backend" -FRONTEND_DIR="${WORKING_DIR}/frontend" -FRONTEND_BUILD_DIR="${FRONTEND_DIR}/dist" -TARGET=aarch64-unknown-linux-musl -VERSION=$(grep -Po '^version\s*=\s*"\K[0-9\.]+' "${BACKEND_DIR}/Cargo.toml" || true) -if [ -z "${VERSION}" ]; then - echo "Error: Failed to determine the version from Cargo.toml." - exit 1 -fi - -if [ "$beta_image" = true ]; then - # Split the version into its components using '.' as a delimiter - IFS='.' read -r major minor patch <<< "$VERSION" - # Increment the patch version - patch=$((patch + 1)) - # Combine the components back into a version string - VERSION="$major.$minor.${patch}-beta" -fi - -if [ ! -f "${BIN_DIR}/build_resources.sh" ]; then - "${BIN_DIR}/build_resources.sh" -fi - -rm -rf "${FRONTEND_BUILD_DIR}" -cd "${FRONTEND_DIR}" && env RUSTFLAGS="--remap-path-prefix $HOME=~" trunk build --release - -# Check if the frontend build directory exists -if [ ! -d "${FRONTEND_BUILD_DIR}" ]; then - echo "Error: Web directory '${FRONTEND_BUILD_DIR}' does not exist." - exit 1 -fi - -cd "$WORKING_DIR" - -cargo clean -env RUSTFLAGS="--remap-path-prefix $HOME=~" cross build -p tuliprox --release --target "$TARGET" - -# Check if the binary exists -if [ ! -f "${WORKING_DIR}/target/${TARGET}/release/tuliprox" ]; then - echo "Error: Static binary '${WORKING_DIR}/target/${TARGET}/release/tuliprox' does not exist." - exit 1 -fi - -cd "$WORKING_DIR" - -# Prepare Docker build context -cp "${WORKING_DIR}/target/${TARGET}/release/tuliprox" "${DOCKER_DIR}/" -rm -rf "${DOCKER_DIR}/web" -cp -r "${FRONTEND_BUILD_DIR}" "${DOCKER_DIR}/web" -cp -r "${RESOURCES_DIR}" "${DOCKER_DIR}/" - -cd "${DOCKER_DIR}" -echo "Building Docker images for version ${VERSION}" -if [ "$beta_image" = true ]; then - SCRATCH_IMAGE_NAME=tuliprox-aarch64-beta -else - SCRATCH_IMAGE_NAME=tuliprox-aarch64 -fi - -# Build scratch image and tag as "latest" -docker build -f Dockerfile-manual -t ghcr.io/euzu/${SCRATCH_IMAGE_NAME}:"${VERSION}" --target scratch-final . -docker tag ghcr.io/euzu/${SCRATCH_IMAGE_NAME}:"${VERSION}" ghcr.io/euzu/${SCRATCH_IMAGE_NAME}:latest - -echo "Logging into GitHub Container Registry..." -docker login ghcr.io -u euzu -p "${GHCR_IO_TOKEN}" - -# Push scratch -docker push ghcr.io/euzu/${SCRATCH_IMAGE_NAME}:"${VERSION}" -docker push ghcr.io/euzu/${SCRATCH_IMAGE_NAME}:latest - -# Clean up -echo "Cleaning up build artifacts..." -rm -rf "${DOCKER_DIR}/web" -rm -f "${DOCKER_DIR}/tuliprox" -rm -rf "${DOCKER_DIR}/resources" - -echo "Docker images ghcr.io/euzu/${SCRATCH_IMAGE_NAME}:${VERSION} have been successfully built, tagged, and pushed." diff --git a/bin/build_github_docker_beta.sh b/bin/build_github_docker_beta.sh deleted file mode 100755 index cae303531..000000000 --- a/bin/build_github_docker_beta.sh +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -source "${HOME}/.ghcr.io" - -WORKING_DIR=$(pwd) -BIN_DIR="${WORKING_DIR}/bin" -RESOURCES_DIR="${WORKING_DIR}/resources" -DOCKER_DIR="${WORKING_DIR}/docker" -BACKEND_DIR="${WORKING_DIR}/backend" -FRONTEND_DIR="${WORKING_DIR}/frontend" -FRONTEND_BUILD_DIR="${FRONTEND_DIR}/dist" -TARGET=x86_64-unknown-linux-musl -VERSION=$(grep -Po '^version\s*=\s*"\K[0-9\.]+' "${BACKEND_DIR}/Cargo.toml" || true) -if [ -z "${VERSION}" ]; then - echo "Error: Failed to determine the version from Cargo.toml." - exit 1 -fi - -# Split the version into its components using '.' as a delimiter -IFS='.' read -r major minor patch <<< "$VERSION" -# Increment the patch version -patch=$((patch + 1)) -# Combine the components back into a version string -VERSION="$major.$minor.${patch}-beta" - -if [ ! -f "${BIN_DIR}/build_resources.sh" ]; then - "${BIN_DIR}/build_resources.sh" -fi - -rm -rf "${FRONTEND_BUILD_DIR}" -cd "${FRONTEND_DIR}" && env RUSTFLAGS="--remap-path-prefix $HOME=~" trunk build --release - -# Check if the frontend build directory exists -if [ ! -d "${FRONTEND_BUILD_DIR}" ]; then - echo "Error: Web directory '${FRONTEND_BUILD_DIR}' does not exist." - exit 1 -fi - -cd "${WORKING_DIR}" - -cargo clean -env RUSTFLAGS="--remap-path-prefix $HOME=~" cross build -p tuliprox --release --target "$TARGET" - -# Check if the binary exists -if [ ! -f "${WORKING_DIR}/target/${TARGET}/release/tuliprox" ]; then - echo "Error: Static binary '${WORKING_DIR}/target/${TARGET}/release/tuliprox' does not exist." - exit 1 -fi - -# Prepare Docker build context -cp "${WORKING_DIR}/target/${TARGET}/release/tuliprox" "${DOCKER_DIR}/" -rm -rf "${DOCKER_DIR}/web" -cp -r "${FRONTEND_BUILD_DIR}" "${DOCKER_DIR}/web" -cp -r "${RESOURCES_DIR}" "${DOCKER_DIR}/" - -cd "${DOCKER_DIR}" -echo "Building Docker images for version ${VERSION}" -BETA_IMAGE_NAME=tuliprox-beta - -# Build alpine image and tag as "latest" -docker build -f Dockerfile-manual -t ghcr.io/euzu/${BETA_IMAGE_NAME}:"${VERSION}" --target alpine-final . -docker tag ghcr.io/euzu/${BETA_IMAGE_NAME}:"${VERSION}" ghcr.io/euzu/${BETA_IMAGE_NAME}:latest - -echo "Logging into GitHub Container Registry..." -docker login ghcr.io -u euzu -p "${GHCR_IO_TOKEN}" - -# Push alpine -docker push ghcr.io/euzu/${BETA_IMAGE_NAME}:"${VERSION}" -docker push ghcr.io/euzu/${BETA_IMAGE_NAME}:latest - -# Clean up -echo "Cleaning up build artifacts..." -rm -rf "${DOCKER_DIR}/web" -rm -f "${DOCKER_DIR}/tuliprox" -rm -rf "${DOCKER_DIR}/resources" - -echo "Docker images ghcr.io/euzu/${BETA_IMAGE_NAME} with version ${VERSION} have been successfully built, tagged, and pushed." diff --git a/docker/Dockerfile-manual b/docker/Dockerfile-manual index 9a9d9c530..ce65c1adf 100644 --- a/docker/Dockerfile-manual +++ b/docker/Dockerfile-manual @@ -11,6 +11,23 @@ RUN apk add --no-cache tzdata \ && mkdir -p /output/etc/ssl/certs \ && cp /etc/ssl/certs/ca-certificates.crt /output/etc/ssl/certs/ca-certificates.crt +# Binary selector stage +FROM alpine:latest AS binary-selector + +ARG TARGETARCH + +WORKDIR /app + +# Copy all binaries +COPY ./binaries ./binaries + +# Select the appropriate binary based on target architecture +RUN case "${TARGETARCH}" in \ + "amd64") cp ./binaries/tuliprox-x86_64-unknown-linux-musl ./tuliprox ;; \ + "arm64") cp ./binaries/tuliprox-aarch64-unknown-linux-musl ./tuliprox ;; \ + *) echo "Unsupported architecture: ${TARGETARCH}" && exit 1 ;; \ + esac + # Scratch Final container FROM scratch AS scratch-final @@ -26,7 +43,8 @@ COPY --from=tz-prep /output/etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca- WORKDIR /app -COPY ./tuliprox ./ +# Copy the selected binary from binary-selector stage +COPY --from=binary-selector /app/tuliprox ./tuliprox COPY ./web ./web COPY ./resources ./resources @@ -49,7 +67,8 @@ RUN ln -sf /usr/share/zoneinfo/${TZ} /etc/localtime WORKDIR /app -COPY ./tuliprox ./ +# Copy the selected binary from binary-selector stage +COPY --from=binary-selector /app/tuliprox ./tuliprox COPY ./web ./web COPY ./resources ./resources # config should be mounted as volume diff --git a/frontend/public/assets/i18n/en.json b/frontend/public/assets/i18n/en.json index 592de3ad4..135fca0a7 100644 --- a/frontend/public/assets/i18n/en.json +++ b/frontend/public/assets/i18n/en.json @@ -243,7 +243,8 @@ "NAME_PREFIX_SEPARATOR":"Name Prefix Separator", "STRIP": "Strip", "COPY_LINK_TULIPROX": "Copy Virtual Id", - "COPY_LINK_PROVIDER": "Copy Provider Url" + "COPY_LINK_PROVIDER": "Copy Provider Url", + "CONTENT_SECURITY_POLICY" : "Content Security Policy" }, "TABLE": { "EMPTY": "", diff --git a/frontend/src/app/components/config/ipcheck_config_view.rs b/frontend/src/app/components/config/ipcheck_config_view.rs index e8ddbe9b4..9b8bcb922 100644 --- a/frontend/src/app/components/config/ipcheck_config_view.rs +++ b/frontend/src/app/components/config/ipcheck_config_view.rs @@ -1,14 +1,25 @@ use yew::prelude::*; use yew_i18n::use_translation; -use crate::app::components::{NoContent}; use crate::app::context::ConfigContext; -use crate::{config_field_optional}; +use crate::{config_field_empty, config_field_optional}; #[function_component] pub fn IpCheckConfigView() -> Html { let translate = use_translation(); let config_ctx = use_context::().expect("ConfigContext not found"); + let render_empty = || { + html! { + <> + { config_field_empty!(translate.t("LABEL.URL")) } + { config_field_empty!(translate.t("LABEL.URL_IPV4")) } + { config_field_empty!(translate.t("LABEL.URL_IPV6")) } + { config_field_empty!(translate.t("LABEL.PATTERN_IPV4")) } + { config_field_empty!(translate.t("LABEL.PATTERN_IPV6")) } + + } + }; + html! {
@@ -25,10 +36,10 @@ pub fn IpCheckConfigView() -> Html { } } else { - html! { } + { render_empty() } } } else { - html! { } + { render_empty() } } }
diff --git a/frontend/src/app/components/config/webui_config_view.rs b/frontend/src/app/components/config/webui_config_view.rs index 51147b2bc..cb6b58943 100644 --- a/frontend/src/app/components/config/webui_config_view.rs +++ b/frontend/src/app/components/config/webui_config_view.rs @@ -1,6 +1,9 @@ -use crate::app::components::{Card, NoContent}; +use crate::app::components::Card; use crate::app::context::ConfigContext; -use crate::{config_field, config_field_bool, config_field_empty, config_field_hide, config_field_optional}; +use crate::{ + config_field, config_field_bool, config_field_bool_empty, config_field_empty, + config_field_hide, config_field_optional, +}; use yew::prelude::*; use yew_i18n::use_translation; @@ -9,6 +12,32 @@ pub fn WebUiConfigView() -> Html { let translate = use_translation(); let config_ctx = use_context::().expect("ConfigContext not found"); + let render_empty_auth = || { + html! { + +

{translate.t("LABEL.AUTH")}

+ { config_field_empty!(translate.t("LABEL.ENABLED")) } + { config_field_empty!(translate.t("LABEL.ISSUER")) } + { config_field_empty!(translate.t("LABEL.SECRET")) } + { config_field_empty!(translate.t("LABEL.TOKEN_TTL_MINS")) } + { config_field_empty!(translate.t("LABEL.USERFILE")) } +
+ } + }; + + let render_empty = || { + html! { + <> + { config_field_bool_empty!(translate.t("LABEL.ENABLED")) } + { config_field_bool_empty!(translate.t("LABEL.USER_UI_ENABLED")) } + { config_field_bool_empty!(translate.t("LABEL.CONTENT_SECURITY_POLICY")) } + { config_field_empty!(translate.t("LABEL.PATH")) } + { config_field_empty!(translate.t("LABEL.PLAYER_SERVER")) } + { render_empty_auth()} + + } + }; + html! {
@@ -19,6 +48,7 @@ pub fn WebUiConfigView() -> Html { <> { config_field_bool!(web_ui, translate.t("LABEL.ENABLED"), enabled) } { config_field_bool!(web_ui, translate.t("LABEL.USER_UI_ENABLED"), user_ui_enabled) } + { config_field_bool!(web_ui, translate.t("LABEL.CONTENT_SECURITY_POLICY"), content_security_policy) } { config_field_optional!(web_ui, translate.t("LABEL.PATH"), path) } { config_field_optional!(web_ui, translate.t("LABEL.PLAYER_SERVER"), player_server) } @@ -34,27 +64,19 @@ pub fn WebUiConfigView() -> Html { { config_field_optional!(auth, translate.t("LABEL.USERFILE"), userfile) } }, - None => html!{ - <> - { config_field_empty!(translate.t("LABEL.ENABLED")) } - { config_field_empty!(translate.t("LABEL.ISSUER")) } - { config_field_empty!(translate.t("LABEL.SECRET")) } - { config_field_empty!(translate.t("LABEL.TOKEN_TTL_MINS")) } - { config_field_empty!(translate.t("LABEL.USERFILE")) } - - } + None => render_empty_auth(), }} } } else { - html! { } + { render_empty() } } } else { - html! { } + { render_empty() } } }
} -} \ No newline at end of file +} diff --git a/frontend/src/app/components/playlist/playlist_explorer.rs b/frontend/src/app/components/playlist/playlist_explorer.rs index b48143d70..ebba99b88 100644 --- a/frontend/src/app/components/playlist/playlist_explorer.rs +++ b/frontend/src/app/components/playlist/playlist_explorer.rs @@ -240,8 +240,11 @@ pub fn PlaylistExplorer() -> Html { } else { html! { {"n/a"}().unwrap(); - img.set_src("assets/missing-logo.svg"); + if let Some(target) = e.target() { + if let Ok(img) = target.dyn_into::() { + img.set_src("assets/missing-logo.svg"); + } + } })} />} } diff --git a/shared/src/model/config/web_ui.rs b/shared/src/model/config/web_ui.rs index e64d3e175..cf79c63e3 100644 --- a/shared/src/model/config/web_ui.rs +++ b/shared/src/model/config/web_ui.rs @@ -16,6 +16,8 @@ pub struct WebUiConfigDto { pub enabled: bool, #[serde(default = "default_as_true")] pub user_ui_enabled: bool, + #[serde(default)] + pub content_security_policy: bool, #[serde(default, skip_serializing_if = "Option::is_none")] pub path: Option, #[serde(default, skip_serializing_if = "Option::is_none")]