Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
8d0cba8
fix: upgrade libdrm with edge mesa
hatsch Sep 26, 2026
e55ae41
fix: sync encoder bin before linking source tee
hatsch Sep 26, 2026
2425f86
fix: clean up and reindex slot state on remove_slot
hatsch Sep 26, 2026
7cd4b67
fix: drop busy loop in input delete route
hatsch Sep 26, 2026
00b7694
fix: guard playlist timers after delete
hatsch Sep 26, 2026
0f1e0d1
fix: slot filter rebuild on idle pad
hatsch Sep 26, 2026
917ca2c
fix: load initial playlist URL async
hatsch Sep 26, 2026
3c350b6
fix: always re-arm program transition debounce
hatsch Sep 26, 2026
9df0ad3
fix: strip proxy credentials and entity configs from /api/config
hatsch Sep 26, 2026
223cbea
fix: keep DOVE session out of NodeCG proxy
hatsch Sep 26, 2026
194d01d
fix: check Origin on WebSocket handshakes
hatsch Sep 26, 2026
b455a80
fix: isolate startup step failures
hatsch Sep 27, 2026
043583c
fix: pick splitmuxsink parsers from encoder codec
hatsch Sep 27, 2026
dce35a7
fix: vorbisenc needs F32LE input
hatsch Sep 27, 2026
1e7be33
fix: finalize recordings with EOS on stop instead of flushing
hatsch Sep 27, 2026
939f539
fix: finalize recordings on shutdown
hatsch Sep 27, 2026
db2b679
fix: 2s GOP in recording encoder default
hatsch Sep 27, 2026
bfdb1e9
fix: WHEP switch to source without audio drops old audio
hatsch Sep 27, 2026
abdb298
fix: tear down WHEP peer after setup timeout
hatsch Sep 27, 2026
0a3dce7
fix: WHEP switch away from a dead source
hatsch Sep 27, 2026
0d8ab5e
fix: 2s GOP in SRT and RTMP encoder defaults
hatsch Sep 27, 2026
17d6b61
fix: HLS output with default encoders and own directory
hatsch Sep 27, 2026
d806d14
chore: bump starlette to 1.7 (security fixes)
hatsch Sep 27, 2026
9c887d7
fix: never overwrite existing recording segments
hatsch Sep 27, 2026
d20f685
fix: default recording mux to matroska
hatsch Sep 27, 2026
4cdc3c4
feat: redact credentials in logs
hatsch Sep 27, 2026
850cdb8
fix: skip interpreter teardown on shutdown (libnice segfault)
hatsch Sep 27, 2026
114f498
fix: clean up WHEP sessions that never connect
hatsch Sep 27, 2026
899af02
fix: broadcast slot filter changes after applying them
hatsch Sep 27, 2026
4f3dfcd
fix: run input position queries off the main loop
hatsch Sep 27, 2026
32ad031
fix: square pixels at input exit (WHIP window size crash)
hatsch Sep 27, 2026
de44781
chore: bump GStreamer 1.28.7 -> 1.28.8
hatsch Oct 10, 2026
54d022f
refactor: Debian 13 base image
hatsch Oct 10, 2026
f3273d3
fix: glibc mmap threshold, slim Debian image
hatsch Oct 10, 2026
e3966ac
fix: input add deadlock on file sources
hatsch Oct 10, 2026
97d0bf8
fix: report rejected websocket updates to the client
hatsch Oct 10, 2026
2cbbd37
chore: update SPA bundle (dove-frontend 149ef63)
hatsch Oct 10, 2026
9866953
chore: drop alpine suffix from main image tags
hatsch Oct 10, 2026
1cbecf3
docs: Debian 13 install, pip section
hatsch Oct 10, 2026
faa222f
fix: output add rollback crash on early failure
hatsch Oct 10, 2026
3a9d697
fix: play after end of file restarts the input
hatsch Oct 10, 2026
4012b56
feat: log audio dropped by mixers
hatsch Oct 10, 2026
1832ba1
fix: program audio lost after first HTML clip
hatsch Oct 10, 2026
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 .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,6 @@ Dockerfile*
.DS_Store
*.swp
*.swo

# Local-only
.venv/
14 changes: 7 additions & 7 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
matrix:
include:
- dockerfile: Dockerfile
suffix: alpine
suffix: ""
gstreamer: "1.28"
- dockerfile: Dockerfile.trixie
suffix: debian
Expand All @@ -53,10 +53,10 @@ jobs:
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=${{ github.ref_name }}-${{ matrix.suffix }}
type=raw,value=latest-${{ matrix.suffix }}
type=raw,value=latest,enable=${{ matrix.suffix == 'alpine' }}
type=raw,value=${{ github.ref_name }},enable=${{ matrix.suffix == 'alpine' }}
type=raw,value=${{ github.ref_name }}-${{ matrix.suffix }},enable=${{ matrix.suffix != '' }}
type=raw,value=latest-${{ matrix.suffix }},enable=${{ matrix.suffix != '' }}
type=raw,value=latest,enable=${{ matrix.suffix == '' }}
type=raw,value=${{ github.ref_name }},enable=${{ matrix.suffix == '' }}

- name: Build and push Docker image
uses: docker/build-push-action@v5
Expand All @@ -66,5 +66,5 @@ jobs:
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,scope=${{ matrix.suffix }}
cache-to: type=gha,mode=max,scope=${{ matrix.suffix }}
cache-from: type=gha,scope=${{ matrix.suffix || 'main' }}
cache-to: type=gha,mode=max,scope=${{ matrix.suffix || 'main' }}
225 changes: 125 additions & 100 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,53 +1,52 @@
# ---------- Builder stage ----------
FROM alpine:3.21 AS builder
FROM debian:trixie-slim AS builder

ARG GSTREAMER_VERSION=1.28.7
ARG GSTREAMER_VERSION=1.28.8
# Vulkan Video needs headers >= 1.4.317, trixie ships 1.4.309 (headers only, the loader stays Debian's)
ARG VULKAN_HEADERS_VERSION=1.4.360

ENV DEBIAN_FRONTEND=noninteractive

# Core build deps
RUN apk add --no-cache \
build-base meson ninja pkgconfig git ca-certificates \
python3 python3-dev py3-pip gobject-introspection-dev \
glib-dev libxml2-dev libffi-dev \
libjpeg-turbo-dev libpng-dev libvorbis-dev libogg-dev opus-dev \
alsa-lib-dev pulseaudio-dev cairo-dev pango-dev freetype-dev flex bison
RUN apt-get update && apt-get install -yq --no-install-recommends \
build-essential meson ninja-build pkg-config git ca-certificates nasm \
python3 python3-dev python-gi-dev gobject-introspection libgirepository1.0-dev \
libglib2.0-dev libxml2-dev libffi-dev \
libjpeg-dev libpng-dev libvorbis-dev libogg-dev libopus-dev \
libasound2-dev libpulse-dev libcairo2-dev libpango1.0-dev libfreetype-dev flex bison \
&& rm -rf /var/lib/apt/lists/*

# Targeted plugin deps
RUN apk add --no-cache \
x264-dev \
libsrt-dev \
vulkan-loader-dev vulkan-headers \
libva-dev libvpx-dev \
ladspa-dev \
frei0r-plugins-dev \
zlib-dev openssl-dev make

WORKDIR /opt

# Upgrade Vulkan loader + headers from edge (need 1.4.x for Vulkan Video encoding)
RUN apk upgrade --no-cache \
--repository=https://dl-cdn.alpinelinux.org/alpine/edge/main \
vulkan-loader-dev vulkan-headers vulkan-loader

# librtmp for rtmpsink (classic RTMP plugin) — Alpine's package ships headers,
# shared lib, and a working pkg-config file, so no need for the upstream source build
RUN apk add --no-cache rtmpdump-dev
RUN apt-get update && apt-get install -yq --no-install-recommends \
libx264-dev libx265-dev libopenh264-dev libaom-dev libtheora-dev \
libwebp-dev librsvg2-dev \
libsrt-openssl-dev librtmp-dev libshout-dev libvo-aacenc-dev \
libvulkan-dev libshaderc-dev glslc \
libva-dev libdrm-dev libvpx-dev libgudev-1.0-dev \
ladspa-sdk frei0r-plugins-dev \
libsoup-3.0-dev libcurl4-openssl-dev \
zlib1g-dev libssl-dev bash-completion \
&& rm -rf /var/lib/apt/lists/*

# WPE/WebKit + FDO backend for wpesrc
RUN apk add --no-cache \
wpewebkit-dev libwpe-dev libwpebackend-fdo-dev \
wayland-dev libxkbcommon-dev \
libepoxy-dev mesa-dev
RUN apt-get update && apt-get install -yq --no-install-recommends \
libwpewebkit-2.0-dev libwpe-1.0-dev libwpebackend-fdo-1.0-dev \
libwayland-dev wayland-protocols libxkbcommon-dev \
libepoxy-dev libegl-dev libgles-dev libgl-dev libgbm-dev \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /opt

RUN git clone https://gitlab.freedesktop.org/gstreamer/gstreamer.git
# /usr/local/include is searched before /usr/include, so these shadow libvulkan-dev's headers
RUN git clone --depth 1 -b v${VULKAN_HEADERS_VERSION} https://github.com/KhronosGroup/Vulkan-Headers.git && \
cp -r Vulkan-Headers/include/vulkan Vulkan-Headers/include/vk_video /usr/local/include/

RUN apk add --no-cache x265-dev openh264-dev bash-completion-dev libsoup-dev curl-dev
RUN apk add --no-cache shaderc-dev
RUN git clone --depth 1 -b ${GSTREAMER_VERSION} https://gitlab.freedesktop.org/gstreamer/gstreamer.git

# Configure targeted features with namespaced options
# Installs to /usr/local: WPE pulls in Debian's GStreamer 1.26 libraries under /usr/lib,
# /usr/local/lib is searched first so the two never overwrite each other.
RUN cd gstreamer && \
git checkout ${GSTREAMER_VERSION} && \
meson setup builddir --prefix=/usr --buildtype=release \
meson setup builddir --prefix=/usr/local --libdir=lib --buildtype=release \
-Dgpl=enabled \
-Dugly=enabled \
-Dbad=enabled \
Expand All @@ -58,21 +57,18 @@ RUN cd gstreamer && \
-Dgst-plugins-bad:frei0r=enabled \
-Dtests=disabled && \
ninja -C builddir && \
ninja -C builddir install

# ---------- Rust plugin builder ----------
# Inherits /usr with GStreamer 1.28 from builder, just adds rust toolchain
FROM builder AS rust-builder
ninja -C builddir install && \
ldconfig

RUN apk add --no-cache clang gcc musl-dev curl pkgconfig openssl-dev zlib-dev
# Use rustup for newer rust (Alpine 3.21 ships 1.83, gst-plugins-rs needs 1.92+)
# Rust gst-plugins-rs, built against the GStreamer 1.28 in /usr/local
RUN apt-get update && apt-get install -yq --no-install-recommends curl clang \
&& rm -rf /var/lib/apt/lists/*
# Use rustup for newer rust (trixie ships 1.85, gst-plugins-rs needs 1.92+)
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain stable --profile minimal
ENV PATH="/root/.cargo/bin:${PATH}"
# Disable static crt so cargo-c links dynamically against system libs (avoids static lib chain)
ENV RUSTFLAGS="-C target-feature=-crt-static"
RUN cargo install --locked cargo-c

ARG GST_RS_VERSION=gstreamer-1.28.7
ARG GST_RS_VERSION=gstreamer-1.28.8
RUN git clone --depth 1 -b ${GST_RS_VERSION} \
https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs.git /opt/gst-plugins-rs

Expand All @@ -84,82 +80,111 @@ RUN cargo cinstall --libdir=/install/gst-plugins-rs --package gst-plugin-livesyn
# fallbackswitch/fallbacksrc for graceful failover
RUN cargo cinstall --libdir=/install/gst-plugins-rs --package gst-plugin-fallbackswitch

# From here on /usr/local is exactly what the runtime image gets.
# Rust plugins are stripped (61MB -> 5MB); GStreamer's own libs keep their symbols for native backtraces.
# Static libs, headers, pkg-config and GIR XML are only needed to build against GStreamer.
RUN strip --strip-unneeded /install/gst-plugins-rs/gstreamer-1.0/*.so && \
cp /install/gst-plugins-rs/gstreamer-1.0/*.so /usr/local/lib/gstreamer-1.0/ && \
rm -rf /usr/local/include /usr/local/lib/*.a /usr/local/lib/pkgconfig \
/usr/local/lib/gstreamer-1.0/pkgconfig /usr/local/lib/gstreamer-1.0/include \
/usr/local/share/gir-1.0 /usr/local/share/man /usr/local/man

# DOVE + Python deps go to /usr/local too, so the runtime image needs no pip
RUN apt-get update && apt-get install -yq --no-install-recommends python3-pip \
&& rm -rf /var/lib/apt/lists/*
COPY . /app
RUN pip install /app --ignore-installed --break-system-packages

# ---------- Runtime stage ----------
FROM alpine:3.21 AS runtime
FROM debian:trixie-slim AS runtime

ENV DEBIAN_FRONTEND=noninteractive

# non-free for intel-media-va-driver-non-free
RUN sed -i 's/^Components: main$/Components: main contrib non-free non-free-firmware/' /etc/apt/sources.list.d/debian.sources

# Skip package contents DOVE never loads: zam-plugins' LV2/VST/CLAP/standalone builds (160MB, only
# the LADSPA ones are used) and Vulkan drivers other than AMD, Intel and software (50MB)
RUN printf 'path-exclude=%s\n' \
'/usr/lib/lv2/*' '/usr/lib/vst/*' '/usr/lib/vst3/*' '/usr/lib/clap/*' '/usr/bin/Za*' \
'/usr/lib/*/libvulkan_nouveau.so' '/usr/share/vulkan/icd.d/nouveau_icd*' \
'/usr/lib/*/libvulkan_freedreno.so' '/usr/share/vulkan/icd.d/freedreno_icd*' \
'/usr/lib/*/libvulkan_asahi.so' '/usr/share/vulkan/icd.d/asahi_icd*' \
'/usr/lib/*/libvulkan_gfxstream.so' '/usr/share/vulkan/icd.d/gfxstream_vk_icd*' \
'/usr/lib/*/libvulkan_virtio.so' '/usr/share/vulkan/icd.d/virtio_icd*' \
> /etc/dpkg/dpkg.cfg.d/dove-excludes

# Runtime libs only
RUN apk add --no-cache \
python3 py3-pip py3-gobject3 \
glib libjpeg-turbo libpng libvorbis opus \
alsa-lib pulseaudio cairo pango freetype \
x264 libsrt \
vulkan-loader mesa \
libva libvpx \
zlib openssl


RUN apk add --no-cache \
python3 py3-pip py3-poetry-core \
gobject-introspection gobject-introspection-dev \
cairo cairo-dev \
graphviz curl

RUN apk add --no-cache \
bubblewrap xdg-dbus-proxy \
wpewebkit libwpe libwpebackend-fdo wayland-libs-client libxkbcommon \
fontconfig font-noto ca-certificates \
mesa mesa-egl mesa-gl mesa-dri-gallium \
mesa-vulkan-swrast mesa-vulkan-ati mesa-vulkan-intel

RUN apk add --no-cache openh264 x265 bash-completion curl libsoup
RUN apt-get update && apt-get install -yq --no-install-recommends \
python3 python3-gi python3-gi-cairo libpython3.13 \
libglib2.0-0t64 libgirepository-1.0-1 libxml2 \
libjpeg62-turbo libpng16-16t64 libvorbis0a libvorbisenc2 libogg0 libopus0 libmpg123-0t64 \
libasound2t64 libpulse0 libcairo2 libcairo-gobject2 libpango-1.0-0 libpangocairo-1.0-0 libfreetype6 \
libx264-164 libx265-215 libopenh264-8 libvpx9 libaom3 libtheora0 \
libwebp7 libwebpmux3 librsvg2-2 \
libsrt1.5-openssl librtmp1 libshout3 libvo-aacenc0 libsoup-3.0-0 libcurl4t64 \
libvulkan1 libva2 libva-drm2 libdrm2 libgudev-1.0-0 \
zlib1g libssl3t64 \
graphviz curl ca-certificates \
&& rm -rf /var/lib/apt/lists/*

# WPE/WebKit for wpesrc (pulls in Debian's GStreamer 1.26 libraries, shadowed by /usr/local/lib)
RUN apt-get update && apt-get install -yq --no-install-recommends \
libwpewebkit-2.0-1 libwpe-1.0-1 libwpebackend-fdo-1.0-1 \
bubblewrap xdg-dbus-proxy \
libwayland-client0 libwayland-server0 libwayland-egl1 libwayland-cursor0 libxkbcommon0 libepoxy0 \
fontconfig fonts-noto-core \
&& rm -rf /var/lib/apt/lists/*

# GPU: Mesa GL/EGL + VA-API + Vulkan drivers (AMD RADV, Intel ANV, software lavapipe).
# Mesa from trixie-backports: Vulkan Video encode needs Mesa 26.x, trixie ships 25.0
RUN echo 'deb http://deb.debian.org/debian trixie-backports main' > /etc/apt/sources.list.d/backports.list \
&& apt-get update && apt-get install -yq --no-install-recommends \
libegl1 libgl1 libgles2 intel-media-va-driver-non-free \
&& apt-get install -yq --no-install-recommends -t trixie-backports \
libgbm1 libgl1-mesa-dri libegl-mesa0 libglx-mesa0 mesa-va-drivers mesa-vulkan-drivers \
&& rm -rf /var/lib/apt/lists/*

# LADSPA runtime + broadcast audio plugin collections:
# zam-plugins-ladspa: Zam compressors, gate, multiband, tube
# zam-plugins: Zam compressors, gate, multiband, tube
# lsp-plugins-ladspa: LSP pro audio suite — parametric EQ, de-esser,
# multiband comp, sidechain comp, ISP limiter, gate, stereo imager
RUN apk add --no-cache ladspa zam-plugins-ladspa lsp-plugins-ladspa

# frei0r video effects (100+ filters: pixelate, cartoon, distort, glow, etc.)
RUN apk add --no-cache frei0r-plugins

RUN apk add --no-cache intel-media-driver mesa-va-gallium
COPY --from=builder /usr /usr
# Remove Alpine GStreamer 1.24 plugin leftovers (pulled by wpewebkit) that
# conflict with the source-built 1.28 plugins copied above.
RUN rm -f /usr/lib/gstreamer-1.0/libgsty4mdec.so
# Rust gst-plugins-rs (audiofx + optional livesync/fallbackswitch)
COPY --from=rust-builder /install/gst-plugins-rs/ /usr/lib/gstreamer-1.0/

# Upgrade Mesa + deps from edge for Vulkan Video encode support (Mesa 26.x+)
RUN apk upgrade --no-cache \
--repository=https://dl-cdn.alpinelinux.org/alpine/edge/main \
mesa mesa-gbm mesa-dri-gallium mesa-egl mesa-gl mesa-va-gallium \
mesa-vulkan-ati mesa-vulkan-intel mesa-vulkan-swrast \
vulkan-loader libxcb wayland-libs-client libva
RUN apt-get update && apt-get install -yq --no-install-recommends \
ladspa-sdk zam-plugins lsp-plugins-ladspa frei0r-plugins \
&& rm -rf /var/lib/apt/lists/*

COPY . /app
WORKDIR /app
RUN cp config-example.toml config.toml
# GStreamer 1.28 + Rust gst-plugins-rs (audiofx, livesync, fallbackswitch) + DOVE's Python deps
COPY --from=builder /usr/local /usr/local
RUN ldconfig

# GStreamer typelibs and gst-python overrides live under /usr/local
ENV GI_TYPELIB_PATH=/usr/local/lib/girepository-1.0
ENV PYTHONPATH=/usr/local/lib/python3/dist-packages

RUN python3 -m pip install --upgrade --break-system-packages 'pip>=26.1' 'setuptools>=78.1.1'
RUN pip install . --ignore-installed --break-system-packages
# glibc otherwise serves video-frame-sized blocks from its per-thread heaps and cannot return them:
# 30 input create/delete cycles grew DOVE from 640MB to 1.8GB. A fixed threshold sends every block
# over 128KB straight to mmap/munmap, as musl does.
ENV MALLOC_MMAP_THRESHOLD_=131072

# Non-root user with video group (GPU access via /dev/dri)
RUN addgroup -S dove && adduser -S -G dove dove \
&& addgroup dove video \
RUN useradd -r -m -G video dove \
&& mkdir -p /var/dove/hls /crashes \
&& chown -R dove:dove /app /var/dove /crashes
&& chown -R dove:dove /var/dove /crashes

COPY --chown=dove:dove . /app
WORKDIR /app

EXPOSE 5000

# Suppress harmless warnings from WPE/WebKit/Mesa in headless container
ENV EGL_LOG_LEVEL=fatal
ENV NO_AT_BRIDGE=1
ENV JSC_SIGNAL_FOR_GC=14
ENV DBUS_SESSION_BUS_ADDRESS=disabled:
ENV WEBKIT_DISABLE_SANDBOX_THIS_IS_DANGEROUS=1

USER dove
RUN cp config-example.toml config.toml

# Pre-scan GStreamer plugins at build time (baked registry = instant startup, no warnings on first run)
RUN gst-inspect-1.0 > /dev/null 2>&1
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile.trixie
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain stable
ENV PATH="/root/.cargo/bin:${PATH}"
RUN cargo install cargo-c

ARG GST_RS_VERSION=gstreamer-1.28.7
ARG GST_RS_VERSION=gstreamer-1.28.8
RUN git clone --depth 1 -b ${GST_RS_VERSION} \
https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs.git /opt/gst-plugins-rs

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,11 +178,11 @@ In-app help is available at `/help` after starting DOVE. All docs are in the [`d
- **Nuxt 4** — web frontend
- **Python 3.12 or 3.13**

> **Note:** Python 3.14 is not supported for native installs due to PyGObject/GStreamer thread interaction bugs. Use Python 3.12 or 3.13. Docker images are pinned to 3.12 and unaffected.
> **Note:** Python 3.14 is not supported for native installs due to PyGObject/GStreamer thread interaction bugs. Use Python 3.12 or 3.13. The Docker images ship Python 3.13 and are unaffected.

## Development

Running DOVE natively from a Python venv: see [`dove/docs/install.md`](dove/docs/install.md).
Running DOVE natively (pip install on Debian 13, or from a checkout): see [`dove/docs/install.md`](dove/docs/install.md).

## Contributing

Expand Down
2 changes: 1 addition & 1 deletion docker-compose.trixie.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Debian Trixie overlay — uses Dockerfile.trixie instead of Alpine
# Debian packages overlay — uses Dockerfile.trixie (Debian's own GStreamer 1.26) instead of the source-built 1.28
# Usage: docker compose -f docker-compose.yml -f docker-compose.trixie.yml -f docker-compose.local.yml up
services:
dove:
Expand Down
19 changes: 19 additions & 0 deletions dove/api/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import secrets
from dataclasses import dataclass
from typing import Optional
from urllib.parse import urlsplit

import httpx
from authlib.integrations.httpx_client import AsyncOAuth2Client
Expand Down Expand Up @@ -67,6 +68,24 @@ def _validate_redirect_base(request: Request, redirect_base: Optional[str]) -> s
return server_origin


def is_allowed_ws_origin(websocket: HTTPConnection) -> bool:
"""Reject cross-site WebSocket handshakes (CSWSH) — browsers don't enforce CORS on WS.
Allowed: no Origin (non-browser client), Origin in allowed_origins, or Origin hostname
matching Host / X-Forwarded-Host (port ignored: dev frontend :3000 → backend :5000)."""
origin = websocket.headers.get('origin')
if not origin:
return True
if origin in _get_config().get('allowed_origins', []):
return True
hosts = {urlsplit(f"//{h}").hostname for h in (websocket.headers.get('host'),
websocket.headers.get('x-forwarded-host')) if h}
if urlsplit(origin).hostname in hosts:
return True
logger.log(f"Rejected WebSocket from origin {origin} (add it to [auth] allowed_origins if legitimate)",
level='WARNING')
return False


def _get_config() -> dict:
global _auth_config, _cookie_signer
if not _auth_config:
Expand Down
Loading
Loading