/* * This file is part of the ZoneMinder Project. See AUTHORS file for Copyright information * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License along * with this program. If not, see . */ #include "zm_stream_socket.h" #include "zm_config.h" #include "zm_logger.h" #include "zm_utils.h" #include #include #include #include #include #include #include #include #include #include #include #include using namespace zm::stream_socket; StreamSocket::StreamSocket(unsigned int monitor_id, std::string path) : StreamSocket(monitor_id, std::move(path), Config()) { } StreamSocket::StreamSocket(unsigned int monitor_id, std::string path, Config config) : monitor_id_(monitor_id), path_(std::move(path)), config_(std::move(config)) { } StreamSocket::~StreamSocket() { Stop(); } bool StreamSocket::Start() { if (thread_.joinable()) return true; // zm::UnixSocket copies the path with a truncating strncpy, so an over-long // path would bind one file while the chmod, chown and unlink below act on // another. Refuse it instead. if (path_.size() >= sizeof(sockaddr_un::sun_path)) { Error("StreamSocket: socket path %s is longer than the %zu bytes a unix socket" " address allows", path_.c_str(), sizeof(sockaddr_un::sun_path) - 1); return false; } ::unlink(path_.c_str()); if (!listener_.bind(path_.c_str())) { Error("StreamSocket: failed to bind %s", path_.c_str()); listener_.close(); return false; } ApplyPermissions(); if (!listener_.listen()) { Error("StreamSocket: failed to listen on %s", path_.c_str()); listener_.close(); ::unlink(path_.c_str()); return false; } listener_.setBlocking(false); if (!wake_.open()) { Error("StreamSocket: failed to open wake pipe"); listener_.close(); ::unlink(path_.c_str()); return false; } wake_.setBlocking(false); terminate_ = false; thread_ = std::thread(&StreamSocket::Run, this); Debug(1, "StreamSocket: monitor %u listening on %s", monitor_id_, path_.c_str()); return true; } void StreamSocket::Stop() { if (thread_.joinable()) { terminate_ = true; Wake(); thread_.join(); } if (listener_.isOpen()) { listener_.close(); ::unlink(path_.c_str()); } wake_.close(); } void StreamSocket::ApplyPermissions() { if (chmod(path_.c_str(), 0660) < 0) Warning("StreamSocket: chmod 0660 %s failed: %s", path_.c_str(), strerror(errno)); if (config_.group.empty()) return; long size = sysconf(_SC_GETGR_R_SIZE_MAX); std::vector buffer(size > 0 ? size : 1024); group grp = {}; group *result = nullptr; if (getgrnam_r(config_.group.c_str(), &grp, buffer.data(), buffer.size(), &result) != 0 or !result) { Warning("StreamSocket: group %s not found, leaving default group on %s", config_.group.c_str(), path_.c_str()); return; } if (chown(path_.c_str(), static_cast(-1), grp.gr_gid) < 0) Warning("StreamSocket: chown group %s on %s failed: %s", config_.group.c_str(), path_.c_str(), strerror(errno)); } StreamSocket::Config StreamSocket::ConfigFromStatic() { Config config; config.group = staticConfig.STREAM_SOCKET_GROUP; config.allowed_uids = ParseAllowedUids(staticConfig.STREAM_SOCKET_ALLOWED_UIDS); if (staticConfig.STREAM_SOCKET_MAX_CLIENTS > 0) config.max_clients = staticConfig.STREAM_SOCKET_MAX_CLIENTS; if (staticConfig.STREAM_SOCKET_QUEUE_BYTES > 0) config.queue_max_bytes = staticConfig.STREAM_SOCKET_QUEUE_BYTES; if (staticConfig.STREAM_SOCKET_QUEUE_MSGS > 0) config.queue_max_msgs = staticConfig.STREAM_SOCKET_QUEUE_MSGS; if (staticConfig.STREAM_SOCKET_STALL_SECS > 0) config.stall_timeout = std::chrono::seconds(staticConfig.STREAM_SOCKET_STALL_SECS); return config; } std::vector StreamSocket::ParseAllowedUids(const std::string &value) { std::vector uids; for (const std::string &token : Split(value, ',')) { std::string trimmed = Trim(token, " \t"); if (trimmed.empty()) continue; errno = 0; char *end = nullptr; unsigned long uid = strtoul(trimmed.c_str(), &end, 10); // strtoul wraps a leading '-' and saturates on overflow; reject both, and // any value that does not round-trip through uid_t, so a typo cannot // silently allow a different uid (e.g. 2^32 truncating to uid 0). bool out_of_range = errno == ERANGE or uid != static_cast(static_cast(uid)); if (trimmed[0] != '-' and end != trimmed.c_str() and *end == '\0' and !out_of_range) { uids.push_back(static_cast(uid)); } else { Warning("StreamSocket: ignoring malformed uid '%s' in allowed uids", trimmed.c_str()); } } return uids; } bool StreamSocket::CheckPeer(int fd, uid_t &uid, pid_t &pid) const { // Kernel-verified peer credentials: SO_PEERCRED on Linux, getpeereid on the // BSDs and macOS (which report no pid). Without either the uid allow-list // cannot be enforced and only the filesystem permissions apply. #if defined(SO_PEERCRED) ucred cred = {}; socklen_t len = sizeof(cred); if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &cred, &len) != 0) { Warning("StreamSocket: SO_PEERCRED failed on %s: %s", path_.c_str(), strerror(errno)); return config_.allowed_uids.empty(); } uid = cred.uid; pid = cred.pid; #elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) gid_t gid = 0; if (getpeereid(fd, &uid, &gid) != 0) { Warning("StreamSocket: getpeereid failed on %s: %s", path_.c_str(), strerror(errno)); return config_.allowed_uids.empty(); } pid = 0; #else if (!config_.allowed_uids.empty()) { Warning("StreamSocket: peer credentials unavailable on this platform," " ZM_STREAM_SOCKET_ALLOWED_UIDS is not enforced on %s", path_.c_str()); } return true; #endif if (config_.allowed_uids.empty() or uid == geteuid()) return true; return std::find(config_.allowed_uids.begin(), config_.allowed_uids.end(), uid) != config_.allowed_uids.end(); } namespace { // HELLO body for a stream, or empty when the source has no such stream. std::vector HelloPayload(const AVCodecParameters *par, AVRational frame_rate) { if (!par or par->codec_id == AV_CODEC_ID_NONE) return {}; return BuildHello(par, frame_rate); } } // namespace void StreamSocket::SetStreams(const AVCodecParameters *video, AVRational frame_rate, const AVCodecParameters *audio) { std::vector video_payload = HelloPayload(video, frame_rate); std::vector audio_payload = HelloPayload(audio, {0, 0}); bool changed; { std::lock_guard lock(mutex_); changed = ApplyStreamsLocked(std::move(video_payload), std::move(audio_payload)); } if (changed) Wake(); } void StreamSocket::SetVideoParams(const AVCodecParameters *par, AVRational frame_rate) { std::vector payload = HelloPayload(par, frame_rate); bool changed; { std::lock_guard lock(mutex_); changed = ApplyStreamsLocked(std::move(payload), std::vector(hello_audio_payload_)); } if (changed) Wake(); } void StreamSocket::SetAudioParams(const AVCodecParameters *par) { std::vector payload = HelloPayload(par, {0, 0}); bool changed; { std::lock_guard lock(mutex_); changed = ApplyStreamsLocked(std::vector(hello_video_payload_), std::move(payload)); } if (changed) Wake(); } void StreamSocket::ClearVideoParams() { SetVideoParams(nullptr, {0, 0}); } void StreamSocket::ClearAudioParams() { SetAudioParams(nullptr); } bool StreamSocket::ApplyStreamsLocked(std::vector video, std::vector audio) { bool video_changed = video != hello_video_payload_; bool audio_changed = audio != hello_audio_payload_; if (!video_changed and !audio_changed) return false; // A generation is complete once its video HELLO has gone out; from then on // any change - new parameters, a stream appearing, a stream disappearing - // starts a new one. Before that (the initial audio-then-video announcement) // only a change to an already announced stream does. bool bump = !hello_video_payload_.empty() or (audio_changed and !hello_audio_payload_.empty()); if (bump) { ++generation_; sequence_[0] = sequence_[1] = 0; keyframe_.reset(); // its header carries the old generation Info("StreamSocket: monitor %u stream parameters changed (video %s, audio %s)," " generation now %u", monitor_id_, video.empty() ? "none" : (video_changed ? "changed" : "same"), audio.empty() ? "none" : (audio_changed ? "changed" : "same"), generation_); } hello_video_payload_ = std::move(video); hello_audio_payload_ = std::move(audio); // On a bump every remaining stream is re-announced under the new generation; // otherwise only what is new. Audio first: the video HELLO is always the // last HELLO of a generation. if (hello_audio_payload_.empty()) { hello_audio_.reset(); } else if (bump or audio_changed) { hello_audio_ = MakeMessage(MessageType::Hello, StreamId::Audio, 0, 0, 0, std::vector(hello_audio_payload_), true); BroadcastLocked(hello_audio_); } if (hello_video_payload_.empty()) { hello_video_.reset(); } else if (bump or video_changed) { hello_video_ = MakeMessage(MessageType::Hello, StreamId::Video, 0, 0, 0, std::vector(hello_video_payload_), true); BroadcastLocked(hello_video_); } return true; } void StreamSocket::SendMedia(const AVPacket *packet, StreamId stream, bool keyframe, int64_t pts_us) { if (!packet or packet->size <= 0) return; // Only the two media streams have a sequence counter and a HELLO if (stream != StreamId::Video and stream != StreamId::Audio) return; bool video_keyframe = keyframe and stream == StreamId::Video; std::unique_lock lock(mutex_); // A stream is on the wire only once its parameters are announced. Without a // HELLO a consumer cannot decode the payload (no codec id or extradata), so // dropping here keeps the protocol's "HELLO precedes MEDIA" guarantee for // e.g. audio packets a monitor forwards before record_audio announces them. const std::vector &hello = stream == StreamId::Audio ? hello_audio_payload_ : hello_video_payload_; if (hello.empty()) return; uint32_t sequence = sequence_[static_cast(stream)]++; bool have_clients = !clients_.empty(); // With no consumers the message only counts against the sequence; skip the // packet reference and fan-out entirely. Keyframes are still cached so the // first consumer to connect gets an immediate first frame. if (!have_clients and !video_keyframe) return; Header header = {}; header.length = kHeaderLengthBytes + packet->size; header.version = kProtocolVersion; header.type = static_cast(MessageType::Media); header.stream = static_cast(stream); header.flags = video_keyframe ? kFlagKeyframe : 0; header.sequence = sequence; header.generation = generation_; header.pts_us = pts_us; // Header.pts_us is signed if (video_keyframe) { // Cache the keyframe for fast-start of late joiners; the cache references // the packet's buffer, no copy is made. av_packet_ptr cache_clone{av_packet_clone(packet)}; if (cache_clone) { auto cache = std::make_shared(); Header cache_header = header; cache_header.type = static_cast(MessageType::Keyframe); SerializeHeader(cache_header, cache->header.data()); cache->av_payload = std::move(cache_clone); keyframe_ = std::move(cache); } } if (!have_clients) return; // Reference the payload buffer; no copy of the media data is made. av_packet_ptr clone{av_packet_clone(packet)}; if (!clone) { Error("StreamSocket: av_packet_clone failed for monitor %u", monitor_id_); return; } auto message = std::make_shared(); SerializeHeader(header, message->header.data()); message->av_payload = std::move(clone); BroadcastLocked(message); lock.unlock(); Wake(); } void StreamSocket::SendMonitorEvent(std::vector payload) { std::unique_lock lock(mutex_); uint32_t sequence = event_sequence_++; if (clients_.empty()) return; // sequence still advanced, so a late joiner sees the gap MessagePtr message = MakeMessage(MessageType::Event, StreamId::Monitor, 0, sequence, 0, std::move(payload), true); BroadcastLocked(message); lock.unlock(); Wake(); } void StreamSocket::SetSnapshotEvent(std::vector payload) { std::lock_guard lock(mutex_); // The snapshot is the consumer's authoritative current status on connect. It // is not broadcast, and only the body is kept: AcceptClient frames it, so // the header carries the generation and the event-sequence baseline that are // current when the consumer connects rather than when the status last moved. snapshot_payload_ = std::move(payload); } void StreamSocket::InvalidateKeyframe() { std::lock_guard lock(mutex_); keyframe_.reset(); } StreamSocket::MessagePtr StreamSocket::MakeMessage( MessageType type, StreamId stream, uint8_t flags, uint32_t sequence, int64_t pts_us, std::vector payload, bool control) const { auto message = std::make_shared(); Header header = {}; header.length = kHeaderLengthBytes + payload.size(); header.version = kProtocolVersion; header.type = static_cast(type); header.stream = static_cast(stream); header.flags = flags; header.sequence = sequence; header.generation = generation_; header.pts_us = pts_us; // Header.pts_us is signed SerializeHeader(header, message->header.data()); message->blob_payload = std::move(payload); message->control = control; return message; } void StreamSocket::BroadcastLocked(const MessagePtr &message) { for (std::unique_ptr &client : clients_) EnqueueLocked(*client, message); } void StreamSocket::EnqueueLocked(Client &client, const MessagePtr &message) { while (!client.queue.empty() and (client.queued_bytes + message->size() > config_.queue_max_bytes or client.queue.size() + 1 > config_.queue_max_msgs)) { // Drop the oldest non-control message. The front message is unsafe to // drop once partially written (removing it would desync the framing) or // while the listener is writing it with the lock released. bool front_pinned = client.front_offset > 0 or client.front_in_flight; auto begin = client.queue.begin() + (front_pinned ? 1 : 0); auto victim = std::find_if(begin, client.queue.end(), [](const MessagePtr &m) { return !m->control; }); if (victim == client.queue.end()) break; client.queued_bytes -= (*victim)->size(); client.queue.erase(victim); ++client.dropped; } if (client.queued_bytes + message->size() > config_.queue_max_bytes or client.queue.size() + 1 > config_.queue_max_msgs) { if (!message->control) { // Nothing droppable left; drop the incoming message instead. ++client.dropped; return; } // Control messages (HELLO/BYE) always go through, even above the limit. } client.queue.push_back(message); client.queued_bytes += message->size(); } bool StreamSocket::DrainClient(Client &client) { while (true) { // Peek the front message under the lock and pin it: EnqueueLocked may // drop queued messages to make room while we are writing, but never the // one in flight, so the bookkeeping below always refers to it. MessagePtr front; size_t offset = 0; { std::lock_guard lock(mutex_); if (client.queue.empty()) return true; front = client.queue.front(); offset = client.front_offset; client.front_in_flight = true; } const Message &message = *front; iovec iov[2]; int iovcnt = 0; if (offset < kHeaderSize) { iov[iovcnt].iov_base = const_cast(message.header.data()) + offset; iov[iovcnt].iov_len = kHeaderSize - offset; ++iovcnt; if (message.payload_size() > 0) { iov[iovcnt].iov_base = const_cast(message.payload_data()); iov[iovcnt].iov_len = message.payload_size(); ++iovcnt; } } else { size_t payload_offset = offset - kHeaderSize; iov[iovcnt].iov_base = const_cast(message.payload_data()) + payload_offset; iov[iovcnt].iov_len = message.payload_size() - payload_offset; ++iovcnt; } msghdr msg = {}; msg.msg_iov = iov; msg.msg_iovlen = iovcnt; // MSG_NOSIGNAL: a disconnected client must produce EPIPE, not SIGPIPE. // This is the kernel copy of the payload; it runs unlocked. ssize_t written = ::sendmsg(client.sock->getDesc(), &msg, MSG_NOSIGNAL); int error = written < 0 ? errno : 0; std::lock_guard lock(mutex_); client.front_in_flight = false; if (written < 0) { if (error == EAGAIN or error == EWOULDBLOCK) return true; if (error == EINTR) continue; Debug(1, "StreamSocket: sendmsg to client pid %d failed: %s", client.pid, strerror(error)); return false; } client.front_offset += written; client.last_progress = std::chrono::steady_clock::now(); if (client.front_offset >= message.size()) { client.queued_bytes -= message.size(); client.queue.pop_front(); client.front_offset = 0; ++client.sent; } } } void StreamSocket::SendStatsLocked(Client &client, TimePoint now) { if (now - client.last_stats < config_.stats_interval) return; client.last_stats = now; MessagePtr stats = MakeMessage(MessageType::Stats, StreamId::Video, 0, 0, 0, BuildStats(client.sent, client.dropped), false); EnqueueLocked(client, stats); } void StreamSocket::AcceptClient() { while (true) { int fd = ::accept(listener_.getDesc(), nullptr, nullptr); if (fd < 0) { if (errno != EAGAIN and errno != EWOULDBLOCK and errno != EINTR) Warning("StreamSocket: accept on %s failed: %s", path_.c_str(), strerror(errno)); return; } auto sock = std::make_unique(listener_, fd); uid_t uid = 0; pid_t pid = 0; if (!CheckPeer(fd, uid, pid)) { Warning("StreamSocket: rejecting client uid %u pid %d on %s (not in allowed uids)", uid, pid, path_.c_str()); continue; // sock destructor closes the connection } std::lock_guard lock(mutex_); if (clients_.size() >= config_.max_clients) { Warning("StreamSocket: rejecting client uid %u pid %d on %s (%zu clients connected)", uid, pid, path_.c_str(), clients_.size()); continue; } sock->setBlocking(false); auto client = std::make_unique(); client->sock = std::move(sock); client->uid = uid; client->pid = pid; client->last_progress = std::chrono::steady_clock::now(); client->last_stats = client->last_progress; // New consumers get the stream parameters first (audio HELLO before video // HELLO, the video HELLO always completes a generation's set), then the // current-status snapshot, then the cached keyframe so they can render // immediately instead of waiting for the next GOP. if (hello_audio_) EnqueueLocked(*client, hello_audio_); if (hello_video_) EnqueueLocked(*client, hello_video_); if (!snapshot_payload_.empty()) { EnqueueLocked(*client, MakeMessage(MessageType::Event, StreamId::Monitor, 0, event_sequence_, 0, std::vector(snapshot_payload_), true)); } if (keyframe_) EnqueueLocked(*client, keyframe_); Info("StreamSocket: monitor %u client connected uid %u pid %d (%zu clients)", monitor_id_, uid, pid, clients_.size() + 1); clients_.push_back(std::move(client)); } } void StreamSocket::Wake() { if (wake_.isOpen()) { uint8_t byte = 0; if (::write(wake_.getWriteDesc(), &byte, 1) < 0 and errno != EAGAIN) { Debug(1, "StreamSocket: wake write failed: %s", strerror(errno)); } } } void StreamSocket::Run() { std::vector fds; while (!terminate_) { fds.clear(); fds.push_back({listener_.getDesc(), POLLIN, 0}); fds.push_back({wake_.getReadDesc(), POLLIN, 0}); { std::lock_guard lock(mutex_); for (std::unique_ptr &client : clients_) { short events = POLLIN; if (!client->queue.empty()) events |= POLLOUT; fds.push_back({client->sock->getDesc(), events, 0}); } } // With no clients there is nothing to drain or report; sleep until a // connection or a wake. Idle monitors cost no periodic wakeups. int timeout = fds.size() > 2 ? 100 : -1; int ready = ::poll(fds.data(), fds.size(), timeout); if (ready < 0) { if (errno == EINTR) continue; Error("StreamSocket: poll failed on %s: %s", path_.c_str(), strerror(errno)); break; } if (terminate_) break; if (fds[1].revents & POLLIN) { uint8_t buffer[64]; while (::read(wake_.getReadDesc(), buffer, sizeof(buffer)) > 0) {} } if (fds[0].revents & POLLIN) AcceptClient(); // clients_ and each Client's socket are owned by this thread; only the // queues and counters are shared with producers, so the lock is taken // just around those, never around the socket writes. TimePoint now = std::chrono::steady_clock::now(); size_t fd_index = 2; for (auto it = clients_.begin(); it != clients_.end();) { Client &client = **it; short revents = 0; if (fd_index < fds.size() and fds[fd_index].fd == client.sock->getDesc()) { revents = fds[fd_index].revents; ++fd_index; } bool remove = false; if (revents & (POLLERR | POLLHUP | POLLNVAL)) remove = true; if (!remove and (revents & POLLIN)) { // Protocol v1 has no client->server messages; discard inbound bytes, // a zero read means the peer closed. uint8_t buffer[256]; ssize_t bytes = ::recv(client.sock->getDesc(), buffer, sizeof(buffer), 0); if (bytes == 0) remove = true; } if (!remove) remove = !DrainClient(client); if (!remove) { std::lock_guard lock(mutex_); SendStatsLocked(client, now); if (client.queued_bytes > 0 and now - client.last_progress > config_.stall_timeout) { Warning("StreamSocket: monitor %u disconnecting stalled client uid %u pid %d" " (%zu bytes queued, %" PRIu64 " dropped)", monitor_id_, client.uid, client.pid, client.queued_bytes, client.dropped); remove = true; } } if (remove) { std::lock_guard lock(mutex_); Info("StreamSocket: monitor %u client uid %u pid %d disconnected" " (sent %" PRIu64 ", dropped %" PRIu64 ")", monitor_id_, client.uid, client.pid, client.sent, client.dropped); it = clients_.erase(it); } else { ++it; } } } // Best-effort BYE so consumers can tell shutdown from failure { std::lock_guard lock(mutex_); MessagePtr bye = MakeMessage(MessageType::Bye, StreamId::Video, 0, 0, 0, {}, true); for (std::unique_ptr &client : clients_) EnqueueLocked(*client, bye); } for (std::unique_ptr &client : clients_) DrainClient(*client); std::lock_guard lock(mutex_); clients_.clear(); }