With ZM_AUTH_HASH_IPS on, the client address bound into the auth hash was
taken from the left-most X-Forwarded-For value whenever the header was
present, both when PHP generated the hash (getRemoteAddr()) and when PHP or
zms validated it (getAuthUser(), zmLoadAuthUser()). The header is client
controlled, so anyone holding a leaked hash could replay it from anywhere by
sending the address it was bound to.
Add ZM_AUTH_TRUSTED_PROXIES, a list of exact reverse proxy addresses.
X-Forwarded-For is now used only when REMOTE_ADDR is one of them, and is read
from the right, skipping hops that are themselves listed proxies, so values a
client prepends are never chosen. With the option empty, the default, the
header is ignored and REMOTE_ADDR is used.
PHP (web/includes/Network.php getRemoteAddr()) and C++ (ClientAddress() in
zm_utils, used by zmLoadAuthUser()) implement the same rule so generation and
validation continue to agree. Every PHP caller already routes through
getRemoteAddr(), so session.php and auth.php need no change.
Reverse proxy users who enable ZM_AUTH_HASH_IPS must list their proxy in the
new option; until they do, hashes bind to the proxy address, which still
validates but no longer distinguishes clients. This is the behaviour change
that the #4921 work avoided by trusting the header.
The option is added through ConfigData only, like other recent options;
zmupdate.pl --freshen inserts it, zms falls back to the compiled-in default and
PHP treats an undefined constant as empty, so no schema migration is needed.
Tests: ClientAddress Catch2 case; tests/php/test_remote_addr.php updated for
the trusted-proxy rule.
refs GHSA-72rf-54rm-798c
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit acea5889dec0826f595cb736147a5fdc9c94a2a9)
The cached snapshot was framed when the status last changed, so after a
generation bump or further events a new consumer received it stamped
with a stale generation and an old event-sequence baseline. Keep only
the body and frame it in AcceptClient, so the header carries the
generation and sequence in effect at the moment of connection.
Tests: a snapshot cached at generation 0 with no events is delivered to
a later consumer with the current generation and sequence.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Two problems with announcing streams one at a time. SetAudioParams only
bumped the generation when audio had been announced before, so audio
joining a video-only stream was appended to the current generation after
its video HELLO, which the protocol says is the last HELLO of a
generation. And a re-prime that changed both streams bumped twice:
consumers saw, and a connecting consumer was handed, an intermediate
generation pairing the new audio with the old video, which
zm_rtsp_server built a session for and tore down again.
Add StreamSocket::SetStreams(video, audio), which applies the whole set
under one lock: unchanged is a no-op, the first announcement stays in
generation 0, and any change once a video HELLO has gone out - new
parameters, a stream appearing, a stream disappearing - is exactly one
bump with every remaining stream re-announced, audio first. The single
stream setters and the new ClearVideoParams are wrappers over the same
logic, and a null or codec-less parameter set means "no such stream".
PrimeCapture passes both streams in one call.
Tests: audio joining an announced video stream bumps the generation;
SetStreams keeps the initial announcement in generation 0, changes both
streams in one generation with consistent pairing, and drops a video
stream the source no longer has.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Correct three problems in the stream socket generation tracking added by
the previous review fix-up:
- ClearAudioParams bumped the generation without restarting the video
sequence or dropping the cached keyframe, so a late joiner received a
HELLO at generation N+1 followed by a KEYFRAME still stamped N, and
sequences did not restart as documented. It now does the same full bump
as SetVideoParams/SetAudioParams.
- zm_rtsp_server rebuilt the xop session whenever the generation changed,
even with identical codec parameters. Generations restart at 0 when zmc
restarts, so every zmc restart dropped the RTSP clients; the original
code kept the session in that case. Unchanged parameters now just adopt
the new generation without a teardown.
- Deciding whether audio belongs to the current generation by comparing
per-stream generation numbers raced the two HELLOs of a generation
(double rebuild when Update() ran between them) and cannot tell a
producer restart apart. The producer now sends the audio HELLO before
the video HELLO within a generation (on connect and on every bump, and
PrimeCapture announces audio before video), so the video HELLO always
completes a generation's parameter set. The consumer forgets the
previous generation's HELLOs when a new generation starts and on
disconnect, and builds only once the video HELLO of the latest
generation is in. It also records whether audio was announced at build
time rather than whether a packer was created, so an unsupported audio
codec no longer triggers a rebuild on every pass.
The ordering guarantee is documented in the protocol header and the
stream socket docs. Tests pin the audio-first order on connect and on a
video reconfigure, and the keyframe drop and sequence restart on audio
removal.
refs #5143
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T4UcdJLt1bxwdpcigGxZRD
- Describe pts_us as signed microseconds with AV_NOPTS_VALUE for unknown.
- Document that the socket path is published in the shared-memory
stream_socket_path field, not only derivable from the convention.
- Clarify the snapshot event's sequence: it equals the next EVENT's
sequence, and the snapshot is a state message a consumer must not treat
as a duplicate of that following EVENT.
refs #5143
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T4UcdJLt1bxwdpcigGxZRD
The MEDIA description promised Annex B for H.264/H.265, but zmc forwards
packets as the camera's demuxer produced them: RTSP sources give Annex B,
MP4/MKV files give AVCC length-prefixed NALs. Say so, explain how a
consumer tells them apart from the HELLO extradata, and note that
zm_rtsp_server handles Annex B only. Also document that image-only
cameras announce no media stream, that the keyframe cache is cleared
when the camera closes, that the uid check uses getpeereid off Linux,
and that the C++ consumer now surfaces EVENT frames.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Wire the monitor lifecycle events into zmc. Monitor::SetState() replaces
the scattered shared_data->state assignments in Analyse() and connect(),
publishing a state_changed event (with previous and new state id and name)
whenever the analysis state actually changes. Monitor::SendStreamHealthEvent()
emits the capture-fault edges and tracks the active fault for the snapshot;
the snapshot is refreshed on every state or health change and after a
successful prime, so a consumer connecting mid-fault learns current status.
zmc's capture loop emits the six health transitions once per edge:
connection failed/restored around connect(), prime_capture failed/restored
around PrimeCapture(), capture_failed on pre/capture/post failure, and a
single capture_resumed once the pipeline recovers. Because the stream
socket survives camera reconnects, these are observable exactly when media
has stopped. A small mutex guards the health fields shared between the
capture thread (health events) and analysis thread (state events).
Documents the EVENT frame in docs/stream_socket.rst and decodes it in
tools/zm_stream_socket_dump.py. Full build and ctest pass (121 tests).
refs #2875
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Adds docs/stream_socket.rst covering the socket path convention, access
control and queue tuning settings, and the version 1 wire protocol
(header layout, HELLO TLVs, MEDIA/KEYFRAME/STATS/BYE semantics,
sequence/generation loss and discontinuity signalling), with pointers
to the reference encoder/decoder, the C++ consumer class and the python
dump tool. Linked from the documentation index.
This also serves as the migration reference for external consumers of
the removed media FIFOs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The guide was written from a verified build and install, but the steps
past that point were derived from the Debian packaging rather than run.
Working through them on Apple Silicon to a running system turned up
three things that were wrong or missing.
mysql -u root does not work. A Homebrew MariaDB authenticates root with
the unix_socket plugin, so only the operating system's root matches it;
your own account gets the administrative account, which is what plain
mariadb uses.
Nothing said ZoneMinder has to be started as ZM_WEB_USER. zmpkg.pl
compares the current user against it and tries sudo -u and two forms of
su to become it, all of which fail for an ordinary user, and stops with
"Unable to find valid su syntax". systemd hides this on Linux by
starting the unit as the web user. Now says so, and gives the
workstation alternative of running everything as one account.
The web server section did not mention that zms is a CGI binary. Apache
runs those with mod_cgi; nginx needs a wrapper such as fcgiwrap, which
Homebrew does not package, so with nginx alone the console works and
live streams do not. Also noted that the generated samples reference
/etc/pki, /etc/nginx and /run/fcgiwrap.sock, none of which exist here.
Added the hardcoded taint-safe PATH to the known gaps, since it is what
breaks zmupdate.pl on Apple Silicon: seventeen scripts pin
/bin:/usr/bin:/usr/local/bin, so the database client in the Homebrew
prefix cannot be found. Worth making configurable - it would equally
affect a --prefix=/opt install on Linux. Sys::CPU is listed too; it has
been removed from CPAN and only zmtelemetry.pl uses it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B5KL9Xbi7K5aGsauLtd8tG
ZoneMinder now builds and installs on macOS, but the steps that make it
work existed nowhere. The installation guide has pages for Debian,
Ubuntu, Redhat and WSL, and nothing for macOS.
Covers the parts that are not guessable, each one walked through on a
real machine rather than reasoned about:
- Sys::Mmap, Date::Manip and a DBD driver are missing from the Perl Apple
ships, and configure only warns, so it is easy to get a clean build and
a system whose Perl daemons cannot read monitor state at all. A
Homebrew Perl additionally needs DBI and LWP::UserAgent, since it has
none of the extras Apple bundles.
- DBD::MariaDB does not install without help. mysql_config hands its
configure step -lzstd -lssl -lcrypto, but a Homebrew Perl's ldflags
carry no -L for the Homebrew prefix, so it fails with "Can't
link/include C library 'zstd', 'ssl', 'crypto', aborting". The guide
gives the --libs and --cflags that get it through.
- ZoneMinder's Perl modules land in vendorlib and survive Perl upgrades;
the CPAN modules land in sitelib, which resolves into the Cellar and
does not. Worth knowing before an upgrade quietly breaks the daemons.
- ZM_NO_MMAP is not a way around Sys::Mmap. macOS caps SysV shared memory
at 4 MiB per segment across 8 segments, and a 1080p RGB frame does not
fit in one.
- mysql-client is keg-only, so the build needs an explicit -I or it stops
at "'mysql/mysql.h' file not found".
- Nothing creates the runtime directories. On Linux the package does it.
- The test binary has to run from build/tests, because zm_font.cpp loads
its fixtures by relative path.
- zmpkg.pl falls back to zmdc.pl without systemd, so there is a way to
start ZoneMinder before a launchd plist exists.
Honest about what is missing rather than papering over it: no plist, no
Homebrew formula, no log rotation, no local cameras, and the arp-scan and
ip warnings configure emits. Also separates Homebrew's prefix from
ZoneMinder's install prefix, which differ on Apple Silicon and are easy
to conflate.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B5KL9Xbi7K5aGsauLtd8tG
The Recording tab docs said only that 'the parameters to the command
will be the event id and the monitor id', which is true only when the
command contains no '%' - and hides several surprises discovered by
reading zm_monitor.cpp:
- without '%' the whole string is exec'd as ONE executable path with
event id / monitor id appended; a command with inline arguments
('/path/script.sh start') is treated as a single file name and fails;
- with '%' the string runs via /bin/sh -c after %EID%/%MID%/%EC%
substitution, and nothing is appended automatically (%EC% exists for
the start command only);
- the child runs in the background as the capture-process user with all
file descriptors closed, so any output silently disappears;
- the end command fires after the event is finalized, so the video file
is complete by then.
Document all of that where the two options are described.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a ZM_DB_SSL_VERIFY_SERVER_CERT setting so a database connection that uses
ZM_DB_SSL_CA_CERT can talk to a server with a self-signed or otherwise
non-matching certificate. When enabled, verification is by identity (the cert
must chain to the CA and its CN/SAN must match ZM_DB_HOST), consistent across
the C++ daemons, the PHP web interface, the CakePHP API and the Perl scripts.
This re-does the reverted #3817. That PR broke the build because it called
mysql_options(MYSQL_OPT_SSL_VERIFY_SERVER_CERT, ...), and that enum was removed
from the MySQL 8.0 C client in favour of MYSQL_OPT_SSL_MODE; it also passed a
c_str() where a my_bool* was expected, and referenced the PHP constant
unconditionally (fatal on PHP 8 for an upgraded install whose zm.conf predates
the option).
The option that controls server-cert verification differs by client library and
the symbols are enum values, not macros, so CMake feature-detects them by
compiling:
- HAVE_MYSQL_OPT_SSL_MODE (MySQL 5.7.11+/8.0, MariaDB Connector/C 3.1+)
- HAVE_MYSQL_OPT_SSL_VERIFY_SERVER_CERT (older MariaDB/MySQL)
zm_db.cpp uses SSL_MODE_VERIFY_IDENTITY / SSL_MODE_REQUIRED when the former is
available, else falls back to the latter with a proper my_bool.
Value handling is three-way in every layer: a truthy value verifies, a false-y
value (0/false/no/off) skips verification, and an empty/unset value leaves the
client default in place so existing installs are unchanged on upgrade. PHP, the
API datasource (via PDO flags) and the Perl DSN are all guarded with defined()
checks. Fresh installs default to 1.
Documents the full ZM_DB_* connection and SSL settings, including the hostname
verification gotcha when connecting by IP, in docs/userguide/configfiles.rst.
refs #3816
Adds a curated, per-encoder parameter-template library to ZoneMinder:
- Monitor edit page: a new Template row above the EncoderParameters
textarea offers per-encoder templates (Balanced / Archival / Low
Power / Low CPU). Apply merges the template's params into the
textarea, preserving user-only keys. Advisory lint flags option
keys that aren't recognised for the selected encoder. Switching
encoders offers a same-name template on the new encoder via a
native confirm.
- Options page: a new Encoder Templates tab with full CRUD —
list / edit / copy / delete — backed by a new CakePHP REST API
at /api/encoder_templates.
- Storage: a new EncoderTemplates DB table seeded with 14 shipped
defaults across libx264 / libx265 / h264_nvenc / hevc_nvenc /
h264_vaapi / hevc_vaapi. The table is mutable; ZM upgrades do not
re-seed user-edited rows.
- valid_keys (the lint allow-list) stays in PHP code as ffmpeg
vocabulary, not user data.
- Default params explicitly include pix_fmt to avoid the yuvj420p
HEVC HW-decode rejection issue we hit earlier.
No C++ change. The textarea content is parsed by the existing
av_dict_parse_string call in src/zm_videostore.cpp.
version.txt -> 1.39.6.
Specs: docs/superpowers/specs/2026-05-0{1,2}-*.md
Plans: docs/superpowers/plans/2026-05-0{1,2}-*.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the uninformative "Any ONVIF options required. This is an
optional field." with a full reference covering:
- key=value,key=value format with an example
- pull_timeout, subscription_timeout, max_retries,
timestamp_validity, soap_log, renewal_enabled, expire_alarms,
closes_event — including defaults, valid ranges, and when to use each
Also fixes pre-existing typo: "alam" → "alarm".
Agent-Logs-Url: https://github.com/ZoneMinder/zoneminder/sessions/149f7873-ca12-424f-85d4-4bdc179d2488
Co-authored-by: connortechnology <925519+connortechnology@users.noreply.github.com>
Add dedicated documentation page for the zma binary covering synopsis,
all command-line options, operating modes (update-existing vs create-events),
save-analysis functionality, and usage examples. Add zma entry to the
components page and user guide toctree.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Document the need to add www-data to the video group for local camera
access. The package postinst now does this automatically, but source
installs still require the manual step.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Failing to install apache2 results in subsequent calls to a2enconf and a2enmod failing because they might not have been installed. (in my case, I was using a minimal Debian 12 install which doesn't install apache2)
As written, you can't redirect sudo output as desired. Wrapping the "echo deb..." in quotes will permit a regular user to redirect stdout to /etc/apt/sources.list