ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
server/protocol.c
Go to the documentation of this file.
1
112#include <math.h>
113#include <stdarg.h>
114#include <stdatomic.h>
115#include <stdio.h>
116#include <string.h>
117#include <time.h>
118
119#include "main.h"
120#include "protocol.h"
121#include "client.h"
122#include <ascii-chat/common.h>
123#include <ascii-chat/common/buffer_sizes.h>
124#include <ascii-chat/util/endian.h>
125#include <ascii-chat/util/validation.h>
126#include <ascii-chat/util/endian.h>
127#include <ascii-chat/util/bytes.h>
128#include <ascii-chat/util/image.h>
129#include <ascii-chat/util/time.h>
130#include <ascii-chat/video/video_frame.h>
131#include <ascii-chat/audio/audio.h>
132#include <ascii-chat/video/palette.h>
133#include <ascii-chat/video/image.h>
134#include <ascii-chat/network/compression.h>
135#include <ascii-chat/network/packet_parsing.h>
136#include <ascii-chat/network/frame_validator.h>
137#include <ascii-chat/network/acip/send.h>
138#include <ascii-chat/network/acip/server.h>
139#include <ascii-chat/util/format.h>
140#include <ascii-chat/platform/system.h>
141#include <ascii-chat/audio/opus_codec.h>
142#include <ascii-chat/network/packet_parsing.h>
143#include <ascii-chat/network/logging.h>
144#include <ascii-chat/crypto/handshake/common.h>
145
146static void protocol_cleanup_thread_locals(void) {
147 // Placeholder for thread-local resources owned by the receive thread.
148 // Add cleanup logic here if future protocol changes introduce
149 // thread-local allocations that must be released before disconnecting.
150}
151
152void disconnect_client_for_bad_data(client_info_t *client, const char *format, ...) {
153 if (!client) {
154 return;
155 }
156
157 protocol_cleanup_thread_locals();
158
159 bool already_requested = atomic_exchange(&client->protocol_disconnect_requested, true);
160 if (already_requested) {
161 return;
162 }
163
164 char reason[BUFFER_SIZE_SMALL] = {0};
165 if (format) {
166 va_list args;
167 va_start(args, format);
168 safe_vsnprintf(reason, sizeof(reason), format, args);
169 va_end(args);
170 } else {
171 SAFE_STRNCPY(reason, "Protocol violation", sizeof(reason));
172 }
173
174 const char *reason_str = reason[0] != '\0' ? reason : "Protocol violation";
175 uint32_t client_id = atomic_load(&client->client_id);
176
177 socket_t socket_snapshot = INVALID_SOCKET_VALUE;
178 const crypto_context_t *crypto_ctx = NULL;
179 acip_transport_t *transport_snapshot = NULL;
180
181 mutex_lock(&client->client_state_mutex);
182 if (client->socket != INVALID_SOCKET_VALUE) {
183 socket_snapshot = client->socket;
184 if (client->crypto_initialized) {
185 crypto_ctx = crypto_handshake_get_context(&client->crypto_handshake_ctx);
186 }
187 }
188 mutex_unlock(&client->client_state_mutex);
189
190 // Get transport reference for WebSocket clients
191 mutex_lock(&client->send_mutex);
192 transport_snapshot = client->transport;
193 mutex_unlock(&client->send_mutex);
194
195 // NOTE: Disconnecting a client due to the client's own bad behavior isn't an
196 // error for us, it's desired behavior for us, so we simply warn and do not
197 // have a need for asciichat_errno here.
198 log_warn("Disconnecting client %u due to protocol violation: %s", client_id, reason_str);
199
200 if (socket_snapshot != INVALID_SOCKET_VALUE) {
201 // Protect socket writes with send_mutex to prevent race with send_thread.
202 // This receive_thread and send_thread both write to same socket.
203 mutex_lock(&client->send_mutex);
204
205 asciichat_error_t log_result =
206 log_network_message(socket_snapshot, (const struct crypto_context_t *)crypto_ctx, LOG_ERROR,
207 REMOTE_LOG_DIRECTION_SERVER_TO_CLIENT, "Protocol violation: %s", reason_str);
208 if (log_result != ASCIICHAT_OK) {
209 log_warn("Failed to send remote log to client %u: %s", client_id, asciichat_error_string(log_result));
210 }
211
212 asciichat_error_t send_result = packet_send_error(socket_snapshot, crypto_ctx, ERROR_NETWORK_PROTOCOL, reason_str);
213 if (send_result != ASCIICHAT_OK) {
214 log_warn("Failed to send error packet to client %u: %s", client_id, asciichat_error_string(send_result));
215 }
216
217 mutex_unlock(&client->send_mutex);
218 } else if (transport_snapshot) {
219 // For WebSocket clients, try to send error via transport
220 log_debug("Sending error to WebSocket client %u via transport", client_id);
221 acip_send_error(transport_snapshot, ERROR_NETWORK_PROTOCOL, reason_str);
222 }
223
225
226 log_debug("Setting active=false in disconnect_client_for_bad_data (client_id=%u, reason=%s)", client_id, reason_str);
227 atomic_store(&client->active, false);
228 atomic_store(&client->shutting_down, true);
229 atomic_store(&client->send_thread_running, false);
230 atomic_store(&client->video_render_thread_running, false);
231 atomic_store(&client->audio_render_thread_running, false);
232
233 if (client->audio_queue) {
234 packet_queue_stop(client->audio_queue);
235 }
236
237 mutex_lock(&client->client_state_mutex);
238 if (client->socket != INVALID_SOCKET_VALUE) {
239 socket_shutdown(client->socket, 2);
240 socket_close(client->socket);
241 client->socket = INVALID_SOCKET_VALUE;
242 }
243 mutex_unlock(&client->client_state_mutex);
244}
245
246/* ============================================================================
247 * Client Lifecycle Packet Handlers
248 * ============================================================================
249 */
250
288void handle_client_join_packet(client_info_t *client, const void *data, size_t len) {
289 VALIDATE_PACKET_SIZE(client, data, len, sizeof(client_info_packet_t), "CLIENT_JOIN");
290
291 const client_info_packet_t *join_info = (const client_info_packet_t *)data;
292
293 // Validate display name is present and not just whitespace
294 if (join_info->display_name[0] == '\0') {
295 disconnect_client_for_bad_data(client, "CLIENT_JOIN display_name cannot be empty");
296 return;
297 }
298
299 uint32_t capabilities = NET_TO_HOST_U32(join_info->capabilities);
300
301 // Validate at least one capability flag is set
302 const uint32_t VALID_CAP_MASK = CLIENT_CAP_VIDEO | CLIENT_CAP_AUDIO | CLIENT_CAP_COLOR | CLIENT_CAP_STRETCH;
303 VALIDATE_CAPABILITY_FLAGS(client, capabilities, VALID_CAP_MASK, "CLIENT_JOIN");
304
305 // Validate no unknown capability bits are set
306 VALIDATE_FLAGS_MASK(client, capabilities, VALID_CAP_MASK, "CLIENT_JOIN");
307
308 SAFE_STRNCPY(client->display_name, join_info->display_name, MAX_DISPLAY_NAME_LEN - 1);
309
310 client->can_send_video = (capabilities & CLIENT_CAP_VIDEO) != 0;
311 client->can_send_audio = (capabilities & CLIENT_CAP_AUDIO) != 0;
312 client->wants_stretch = (capabilities & CLIENT_CAP_STRETCH) != 0;
313
314 log_info("Client %u joined: %s (video=%d, audio=%d, stretch=%d)", atomic_load(&client->client_id),
315 client->display_name, client->can_send_video, client->can_send_audio, client->wants_stretch);
316
317 // Notify client of successful join (encrypted channel)
318 if (client->socket != INVALID_SOCKET_VALUE) {
319 log_info_client(client, "Joined as '%s' (video=%s, audio=%s)", client->display_name,
320 client->can_send_video ? "yes" : "no", client->can_send_audio ? "yes" : "no");
321 }
322}
323
355void handle_protocol_version_packet(client_info_t *client, const void *data, size_t len) {
356 if (!data) {
357 disconnect_client_for_bad_data(client, "PROTOCOL_VERSION payload missing");
358 return;
359 }
360
361 if (len != sizeof(protocol_version_packet_t)) {
362 disconnect_client_for_bad_data(client, "PROTOCOL_VERSION invalid size: %zu (expected %zu)", len,
363 sizeof(protocol_version_packet_t));
364 return;
365 }
366
367 const protocol_version_packet_t *version = (const protocol_version_packet_t *)data;
368 uint16_t client_major = NET_TO_HOST_U16(version->protocol_version);
369 uint16_t client_minor = NET_TO_HOST_U16(version->protocol_revision);
370
371 // Validate major version match (minor version can differ for backward compat)
372 if (client_major != PROTOCOL_VERSION_MAJOR) {
373 log_warn("Client %u protocol version mismatch: client=%u.%u, server=%u.%u", atomic_load(&client->client_id),
374 client_major, client_minor, PROTOCOL_VERSION_MAJOR, PROTOCOL_VERSION_MINOR);
375 // Note: We don't disconnect on version mismatch for backward compatibility
376 // Clients may be older or newer than server
377 } else if (client_minor != PROTOCOL_VERSION_MINOR) {
378 log_info("Client %u has different protocol revision: client=%u.%u, server=%u.%u", atomic_load(&client->client_id),
379 client_major, client_minor, PROTOCOL_VERSION_MAJOR, PROTOCOL_VERSION_MINOR);
380 }
381
382 // Validate reserved bytes are zero
383 for (size_t i = 0; i < sizeof(version->reserved); i++) {
384 if (version->reserved[i] != 0) {
385 log_warn("Client %u sent non-zero reserved bytes in PROTOCOL_VERSION packet", atomic_load(&client->client_id));
386 // Don't disconnect - reserved bytes may be used in future versions
387 break;
388 }
389 }
390
391 // Log supported features
392 if (ACIP_CRYPTO_HAS_ENCRYPT(version->supports_encryption)) {
393 log_debug("Client %u supports encryption", atomic_load(&client->client_id));
394 }
395 if (version->compression_algorithms != 0) {
396 log_debug("Client %u supports compression: 0x%02x", atomic_load(&client->client_id),
397 version->compression_algorithms);
398 }
399 if (version->feature_flags != 0) {
400 uint16_t feature_flags = NET_TO_HOST_U16(version->feature_flags);
401 log_debug("Client %u supports features: 0x%04x", atomic_load(&client->client_id), feature_flags);
402 }
403}
404
432void handle_client_leave_packet(client_info_t *client, const void *data, size_t len) {
433 if (!client) {
434 return;
435 }
436
437 uint32_t client_id = atomic_load(&client->client_id);
438
439 if (len == 0) {
440 // Empty reason - client disconnecting without explanation
441 log_info("Client %u sent leave notification (no reason)", client_id);
442 } else if (len <= 256) {
443 // Reason provided - extract and log it
444 if (!data) {
445 SET_ERRNO(ERROR_INVALID_STATE, "Client %u sent leave notification with non-zero length but NULL data", client_id);
446 return;
447 }
448
449 char reason[257] = {0};
450 memcpy(reason, data, len);
451 reason[len] = '\0';
452
453 // Validate reason is printable (handle potential non-UTF8 gracefully)
454 bool all_printable = true;
455 for (size_t i = 0; i < len; i++) {
456 uint8_t c = (uint8_t)reason[i];
457 if (c < 32 && c != '\t' && c != '\n') {
458 all_printable = false;
459 break;
460 }
461 }
462
463 if (all_printable) {
464 log_info("Client %u sent leave notification: %s", client_id, reason);
465 } else {
466 log_info("Client %u sent leave notification (reason contains non-printable characters)", client_id);
467 }
468 } else {
469 // Oversized reason - shouldn't happen with validation.h checks
470 log_warn("Client %u sent oversized leave reason (%zu bytes, max 256)", client_id, len);
471 }
472
473 // Deactivate client to stop processing packets
474 // Sets client->active = false immediately - triggers client cleanup procedures
475 log_debug("Setting active=false in handle_client_leave_packet (client_id=%u)", client_id);
476 atomic_store(&client->active, false);
477
478 // Note: We don't disconnect the client here - that happens when socket closes
479 // This is just a clean notification before disconnect
480}
481
518void handle_stream_start_packet(client_info_t *client, const void *data, size_t len) {
519 VALIDATE_PACKET_SIZE(client, data, len, sizeof(uint32_t), "STREAM_START");
520
521 uint32_t stream_type_net;
522 memcpy(&stream_type_net, data, sizeof(uint32_t));
523 uint32_t stream_type = NET_TO_HOST_U32(stream_type_net);
524
525 // Validate at least one stream type flag is set
526 const uint32_t VALID_STREAM_MASK = STREAM_TYPE_VIDEO | STREAM_TYPE_AUDIO;
527 VALIDATE_CAPABILITY_FLAGS(client, stream_type, VALID_STREAM_MASK, "STREAM_START");
528
529 // Validate no unknown stream type bits are set
530 VALIDATE_FLAGS_MASK(client, stream_type, VALID_STREAM_MASK, "STREAM_START");
531
532 if (stream_type & STREAM_TYPE_VIDEO) {
533 atomic_store(&client->is_sending_video, true);
534 }
535 if (stream_type & STREAM_TYPE_AUDIO) {
536 atomic_store(&client->is_sending_audio, true);
537
538 // Create Opus decoder for this client if not already created
539 if (!client->opus_decoder) {
540 client->opus_decoder = opus_codec_create_decoder(48000);
541 if (client->opus_decoder) {
542 log_info("Client %u: Opus decoder created (48kHz)", atomic_load(&client->client_id));
543 } else {
544 log_error("Client %u: Failed to create Opus decoder", atomic_load(&client->client_id));
545 }
546 }
547 }
548
549 if (stream_type & STREAM_TYPE_VIDEO) {
550 log_info("Client %u announced video stream (waiting for first frame)", atomic_load(&client->client_id));
551 }
552 if (stream_type & STREAM_TYPE_AUDIO) {
553 log_info("Client %u started audio stream", atomic_load(&client->client_id));
554 }
555
556 // Notify client of stream start acknowledgment
557 const char *streams = (stream_type & STREAM_TYPE_VIDEO) && (stream_type & STREAM_TYPE_AUDIO)
558 ? "video+audio"
559 : ((stream_type & STREAM_TYPE_VIDEO) ? "video" : "audio");
560 // Only send remote log to TCP clients (WebSocket clients have invalid socket)
561 if (client->socket != INVALID_SOCKET_VALUE) {
562 log_info_client(client, "Stream started: %s", streams);
563 }
564}
565
601void handle_stream_stop_packet(client_info_t *client, const void *data, size_t len) {
602 VALIDATE_PACKET_SIZE(client, data, len, sizeof(uint32_t), "STREAM_STOP");
603
604 uint32_t stream_type_net;
605 memcpy(&stream_type_net, data, sizeof(uint32_t));
606 uint32_t stream_type = NET_TO_HOST_U32(stream_type_net);
607
608 // Validate at least one stream type flag is set
609 const uint32_t VALID_STREAM_MASK = STREAM_TYPE_VIDEO | STREAM_TYPE_AUDIO;
610 VALIDATE_CAPABILITY_FLAGS(client, stream_type, VALID_STREAM_MASK, "STREAM_STOP");
611
612 // Validate no unknown stream type bits are set
613 VALIDATE_FLAGS_MASK(client, stream_type, VALID_STREAM_MASK, "STREAM_STOP");
614
615 if (stream_type & STREAM_TYPE_VIDEO) {
616 atomic_store(&client->is_sending_video, false);
617 }
618 if (stream_type & STREAM_TYPE_AUDIO) {
619 atomic_store(&client->is_sending_audio, false);
620 }
621
622 if (stream_type & STREAM_TYPE_VIDEO) {
623 log_info("Client %u stopped video stream", atomic_load(&client->client_id));
624 }
625 if (stream_type & STREAM_TYPE_AUDIO) {
626 log_info("Client %u stopped audio stream", atomic_load(&client->client_id));
627 }
628
629 // Notify client of stream stop acknowledgment
630 const char *streams = (stream_type & STREAM_TYPE_VIDEO) && (stream_type & STREAM_TYPE_AUDIO)
631 ? "video+audio"
632 : ((stream_type & STREAM_TYPE_VIDEO) ? "video" : "audio");
633 // Only send remote log to TCP clients (WebSocket clients have invalid socket)
634 if (client->socket != INVALID_SOCKET_VALUE) {
635 log_info_client(client, "Stream stopped: %s", streams);
636 }
637}
638
642void handle_ping_packet(client_info_t *client, const void *data, size_t len) {
643 (void)data;
644 (void)len;
645
646 // Get transport reference briefly to avoid deadlock on TCP buffer full
647 mutex_lock(&client->send_mutex);
648 if (atomic_load(&client->shutting_down) || !client->transport) {
649 mutex_unlock(&client->send_mutex);
650 return;
651 }
652 acip_transport_t *pong_transport = client->transport;
653 mutex_unlock(&client->send_mutex);
654
655 // Network I/O happens OUTSIDE the mutex
656 asciichat_error_t pong_result = acip_send_pong(pong_transport);
657 if (pong_result != ASCIICHAT_OK) {
658 SET_ERRNO(ERROR_NETWORK, "Failed to send PONG response to client %u: %s", atomic_load(&client->client_id),
659 asciichat_error_string(pong_result));
660 }
661}
662
666void handle_pong_packet(client_info_t *client, const void *data, size_t len) {
667 (void)client;
668 (void)data;
669 (void)len;
670 // No action needed - client acknowledged our PING
671}
672
673/* ============================================================================
674 * Media Data Packet Handlers
675 * ============================================================================
676 */
677
732void handle_image_frame_packet(client_info_t *client, void *data, size_t len) {
733 // Handle incoming image data from client
734 // New format: [width:4][height:4][compressed_flag:4][data_size:4][rgb_data:data_size]
735 // Old format: [width:4][height:4][rgb_data:w*h*3] (for backward compatibility)
736 // Use atomic compare-and-swap to avoid race condition - ensures thread-safe auto-enabling of video stream
737
738 log_info("RECV_IMAGE_FRAME: client_id=%u, len=%zu", atomic_load(&client->client_id), len);
739
740 if (!data || len < sizeof(uint32_t) * 2) {
741 disconnect_client_for_bad_data(client, "IMAGE_FRAME payload too small: %zu bytes", len);
742 return;
743 }
744 bool was_sending_video = atomic_load(&client->is_sending_video);
745 if (!was_sending_video) {
746 // Try to atomically enable video sending
747 // Use atomic_compare_exchange_strong to avoid spurious failures
748 if (atomic_compare_exchange_strong(&client->is_sending_video, &was_sending_video, true)) {
749 log_info("Client %u auto-enabled video stream (received IMAGE_FRAME)", atomic_load(&client->client_id));
750 // Notify client that their first video frame was received
751 if (client->socket != INVALID_SOCKET_VALUE) {
752 log_info_client(client, "First video frame received - streaming active");
753 }
754 }
755 } else {
756 // Log periodically to confirm we're receiving frames
757 // Use per-client counter protected by client_state_mutex to avoid race conditions
758 mutex_lock(&client->client_state_mutex);
759 client->frames_received_logged++;
760 if (client->frames_received_logged % 25000 == 0) {
761 char pretty[64];
762 format_bytes_pretty(len, pretty, sizeof(pretty));
763 log_debug("Client %u has sent %u IMAGE_FRAME packets (%s)", atomic_load(&client->client_id),
764 client->frames_received_logged, pretty);
765 }
766 mutex_unlock(&client->client_state_mutex);
767 }
768
769 // Parse image dimensions (use memcpy to avoid unaligned access)
770 uint32_t img_width_net, img_height_net;
771 memcpy(&img_width_net, data, sizeof(uint32_t));
772 memcpy(&img_height_net, (char *)data + sizeof(uint32_t), sizeof(uint32_t));
773 uint32_t img_width = NET_TO_HOST_U32(img_width_net);
774 uint32_t img_height = NET_TO_HOST_U32(img_height_net);
775
776 log_debug("IMAGE_FRAME packet: width=%u, height=%u, payload_len=%zu", img_width, img_height, len);
777
778 // Validate dimensions using image utility functions
779 if (image_validate_dimensions((size_t)img_width, (size_t)img_height) != ASCIICHAT_OK) {
780 log_error("IMAGE_FRAME validation failed for dimensions: %u x %u", img_width, img_height);
781 disconnect_client_for_bad_data(client, "IMAGE_FRAME invalid dimensions");
782 return;
783 }
784
785 // Calculate RGB buffer size with overflow checking
786 size_t rgb_size = 0;
787 if (image_calc_rgb_size((size_t)img_width, (size_t)img_height, &rgb_size) != ASCIICHAT_OK) {
788 disconnect_client_for_bad_data(client, "IMAGE_FRAME buffer size calculation failed");
789 return;
790 }
791
792 // Validate final buffer size against maximum
793 if (image_validate_buffer_size(rgb_size) != ASCIICHAT_OK) {
794 disconnect_client_for_bad_data(client, "IMAGE_FRAME buffer size exceeds maximum");
795 return;
796 }
797
798 // Only support legacy format: [width:4][height:4][rgb_data:w*h*3]
799 if (rgb_size > SIZE_MAX - FRAME_HEADER_SIZE_LEGACY) {
800 char size_str[32];
801 format_bytes_pretty(rgb_size, size_str, sizeof(size_str));
802 disconnect_client_for_bad_data(client, "IMAGE_FRAME legacy packet size overflow: %s", size_str);
803 return;
804 }
805 size_t expected_size = FRAME_HEADER_SIZE_LEGACY + rgb_size;
806
807 if (len != expected_size) {
808 disconnect_client_for_bad_data(client, "IMAGE_FRAME size mismatch: expected %zu bytes got %zu", expected_size, len);
809 return;
810 }
811
812 // Validate legacy format
813 asciichat_error_t validate_result = frame_validate_legacy(len, rgb_size);
814 if (validate_result != ASCIICHAT_OK) {
815 disconnect_client_for_bad_data(client, "IMAGE_FRAME legacy validation failed");
816 return;
817 }
818
819 void *rgb_data = (char *)data + FRAME_HEADER_SIZE_LEGACY;
820 size_t rgb_data_size = rgb_size;
821 bool needs_free = false;
822
823 if (client->incoming_video_buffer) {
824 // Get the write buffer
825 video_frame_t *frame = video_frame_begin_write(client->incoming_video_buffer);
826
827 if (frame && frame->data) {
828 // Build the packet in the old format for internal storage: [width:4][height:4][rgb_data:w*h*3]
829 // Use frame_check_size_overflow to validate overflow before repacking
830 asciichat_error_t overflow_check = frame_check_size_overflow(FRAME_HEADER_SIZE_LEGACY, rgb_data_size);
831 if (overflow_check != ASCIICHAT_OK) {
832 if (needs_free && rgb_data) {
833 SAFE_FREE(rgb_data);
834 }
835 disconnect_client_for_bad_data(client, "IMAGE_FRAME size overflow while repacking");
836 return;
837 }
838 size_t old_packet_size = FRAME_HEADER_SIZE_LEGACY + rgb_data_size;
839
840 if (old_packet_size <= MAX_FRAME_BUFFER_SIZE) { // Max frame buffer size
841 uint32_t width_net = HOST_TO_NET_U32(img_width);
842 uint32_t height_net = HOST_TO_NET_U32(img_height);
843
844 // Pack in old format for internal consistency
845 memcpy(frame->data, &width_net, sizeof(uint32_t));
846 memcpy((char *)frame->data + sizeof(uint32_t), &height_net, sizeof(uint32_t));
847 memcpy((char *)frame->data + sizeof(uint32_t) * 2, rgb_data, rgb_data_size);
848
849 frame->size = old_packet_size;
850 frame->width = img_width;
851 frame->height = img_height;
852 frame->capture_timestamp_ns = (uint64_t)time(NULL) * NS_PER_SEC_INT;
853 frame->sequence_number = ++client->frames_received;
854
855 // DEBUG: Compute hash of incoming RGB data to detect duplicates
856 uint32_t incoming_rgb_hash = 0;
857 for (size_t i = 0; i < rgb_data_size && i < 1000; i++) {
858 incoming_rgb_hash = (uint32_t)((uint64_t)incoming_rgb_hash * 31 + ((unsigned char *)rgb_data)[i]);
859 }
860
861 // Per-client hash tracking (not static!) to avoid cross-client interference
862 uint32_t client_id = atomic_load(&client->client_id);
863 bool is_new_frame = (incoming_rgb_hash != client->last_received_frame_hash);
864
865 if (is_new_frame) {
866 log_info("RECV_FRAME #%u NEW: Client %u size=%zu dims=%ux%u hash=0x%08x (prev=0x%08x)",
867 client->frames_received, client_id, rgb_data_size, img_width, img_height, incoming_rgb_hash,
868 client->last_received_frame_hash);
869 client->last_received_frame_hash = incoming_rgb_hash;
870 } else {
871 log_info("RECV_FRAME #%u DUP: Client %u size=%zu dims=%ux%u hash=0x%08x", client->frames_received, client_id,
872 rgb_data_size, img_width, img_height, incoming_rgb_hash);
873 }
874
875 video_frame_commit(client->incoming_video_buffer);
876 } else {
877 if (needs_free && rgb_data) {
878 SAFE_FREE(rgb_data);
879 }
880 disconnect_client_for_bad_data(client, "IMAGE_FRAME repacked frame too large (%zu bytes)", old_packet_size);
881 return;
882 }
883 } else {
884 log_warn("Failed to get write buffer for client %u (frame=%p, frame->data=%p)", atomic_load(&client->client_id),
885 (void *)frame, frame ? frame->data : NULL);
886 }
887 } else {
888 // During shutdown, this is expected - don't spam error logs
889 if (!atomic_load(&g_server_should_exit)) {
890 SET_ERRNO(ERROR_INVALID_STATE, "Client %u has no incoming video buffer!", atomic_load(&client->client_id));
891 } else {
892 log_debug("Client %u: ignoring video packet during shutdown", atomic_load(&client->client_id));
893 }
894 }
895
896 // Clean up decompressed data if allocated
897 if (needs_free && rgb_data) {
898 SAFE_FREE(rgb_data);
899 }
900}
901
943void handle_audio_packet(client_info_t *client, const void *data, size_t len) {
944 VALIDATE_NOTNULL_DATA(client, data, "AUDIO");
945 VALIDATE_AUDIO_ALIGNMENT(client, len, sizeof(float), "AUDIO");
946 VALIDATE_AUDIO_STREAM_ENABLED(client, "AUDIO");
947
948 int num_samples = (int)(len / sizeof(float));
949 VALIDATE_AUDIO_SAMPLE_COUNT(client, num_samples, AUDIO_SAMPLES_PER_PACKET, "AUDIO");
950 VALIDATE_RESOURCE_INITIALIZED(client, client->incoming_audio_buffer, "audio buffer");
951
952 const float *samples = (const float *)data;
953 asciichat_error_t result = audio_ring_buffer_write(client->incoming_audio_buffer, samples, num_samples);
954 if (result != ASCIICHAT_OK) {
955 log_error("Failed to write audio samples to buffer: %s", asciichat_error_string(result));
956 }
957}
958
959void handle_remote_log_packet_from_client(client_info_t *client, const void *data, size_t len) {
960 if (!client) {
961 return;
962 }
963
964 log_level_t remote_level = LOG_INFO;
965 remote_log_direction_t direction = REMOTE_LOG_DIRECTION_UNKNOWN;
966 uint16_t flags = 0;
967 char message[MAX_REMOTE_LOG_MESSAGE_LENGTH + 1] = {0};
968
969 asciichat_error_t parse_result =
970 packet_parse_remote_log(data, len, &remote_level, &direction, &flags, message, sizeof(message), NULL);
971 if (parse_result != ASCIICHAT_OK) {
972 disconnect_client_for_bad_data(client, "Invalid REMOTE_LOG packet: %s", asciichat_error_string(parse_result));
973 return;
974 }
975
976 if (direction != REMOTE_LOG_DIRECTION_CLIENT_TO_SERVER) {
977 disconnect_client_for_bad_data(client, "REMOTE_LOG direction mismatch: %u", direction);
978 return;
979 }
980
981 const bool truncated = (flags & REMOTE_LOG_FLAG_TRUNCATED) != 0;
982 const char *display_name = client->display_name[0] ? client->display_name : "(unnamed)";
983 uint32_t client_id = atomic_load(&client->client_id);
984
985 if (truncated) {
986 log_msg(remote_level, __FILE__, __LINE__, __func__, "[REMOTE CLIENT %u \"%s\"] %s [message truncated]", client_id,
987 display_name, message);
988 } else {
989 log_msg(remote_level, __FILE__, __LINE__, __func__, "[REMOTE CLIENT %u \"%s\"] %s", client_id, display_name,
990 message);
991 }
992}
993
1040void handle_audio_batch_packet(client_info_t *client, const void *data, size_t len) {
1041 // Log every audio batch packet reception
1042 log_debug_every(LOG_RATE_DEFAULT, "Received audio batch packet from client %u (len=%zu, is_sending_audio=%d)",
1043 atomic_load(&client->client_id), len, atomic_load(&client->is_sending_audio));
1044
1045 VALIDATE_NOTNULL_DATA(client, data, "AUDIO_BATCH");
1046 VALIDATE_MIN_SIZE(client, len, sizeof(audio_batch_packet_t), "AUDIO_BATCH");
1047 VALIDATE_AUDIO_STREAM_ENABLED(client, "AUDIO_BATCH");
1048
1049 // Parse batch header using utility function
1050 audio_batch_info_t batch_info;
1051 asciichat_error_t parse_result = audio_parse_batch_header(data, len, &batch_info);
1052 if (parse_result != ASCIICHAT_OK) {
1053 disconnect_client_for_bad_data(client, "Failed to parse audio batch header");
1054 return;
1055 }
1056
1057 uint32_t packet_batch_count = batch_info.batch_count;
1058 uint32_t total_samples = batch_info.total_samples;
1059 uint32_t sample_rate = batch_info.sample_rate;
1060
1061 (void)packet_batch_count;
1062 (void)sample_rate;
1063
1064 VALIDATE_NONZERO(client, packet_batch_count, "batch_count", "AUDIO_BATCH");
1065 VALIDATE_NONZERO(client, total_samples, "total_samples", "AUDIO_BATCH");
1066
1067 size_t samples_bytes = 0;
1068 if (safe_size_mul(total_samples, sizeof(uint32_t), &samples_bytes)) {
1069 disconnect_client_for_bad_data(client, "AUDIO_BATCH sample size overflow (samples=%u)", total_samples);
1070 return;
1071 }
1072
1073 size_t expected_size = sizeof(audio_batch_packet_t) + samples_bytes;
1074 if (len != expected_size) {
1075 disconnect_client_for_bad_data(client, "AUDIO_BATCH length mismatch: got %zu expected %zu", len, expected_size);
1076 return;
1077 }
1078
1079 // Bounds check to prevent integer overflow on allocation
1080 // Maximum allowed samples: AUDIO_BATCH_SAMPLES * 2 (2048 samples)
1081 // This prevents total_samples * sizeof(float) from exceeding 8KB
1082 const uint32_t MAX_AUDIO_SAMPLES = AUDIO_BATCH_SAMPLES * 2;
1083 if (total_samples > MAX_AUDIO_SAMPLES) {
1084 disconnect_client_for_bad_data(client, "AUDIO_BATCH too many samples: %u (max: %u)", total_samples,
1085 MAX_AUDIO_SAMPLES);
1086 return;
1087 }
1088
1089 const uint8_t *samples_ptr = (const uint8_t *)data + sizeof(audio_batch_packet_t);
1090
1091 // Safe allocation: total_samples is bounded above, so multiplication won't overflow
1092 size_t alloc_size = (size_t)total_samples * sizeof(float);
1093 float *samples = SAFE_MALLOC(alloc_size, float *);
1094 if (!samples) {
1095 SET_ERRNO(ERROR_MEMORY, "Failed to allocate memory for audio sample conversion");
1096 return;
1097 }
1098
1099 // Use helper function to dequantize samples
1100 asciichat_error_t dq_result = audio_dequantize_samples(samples_ptr, total_samples, samples);
1101 if (dq_result != ASCIICHAT_OK) {
1102 SAFE_FREE(samples);
1103 return;
1104 }
1105
1106#ifndef NDEBUG
1107 static int recv_count = 0;
1108 recv_count++;
1109 if (recv_count % 100 == 0) {
1110 uint32_t raw0 = bytes_read_u32_unaligned(samples_ptr + 0 * sizeof(uint32_t));
1111 uint32_t raw1 = bytes_read_u32_unaligned(samples_ptr + 1 * sizeof(uint32_t));
1112 uint32_t raw2 = bytes_read_u32_unaligned(samples_ptr + 2 * sizeof(uint32_t));
1113 int32_t scaled0 = (int32_t)NET_TO_HOST_U32(raw0);
1114 int32_t scaled1 = (int32_t)NET_TO_HOST_U32(raw1);
1115 int32_t scaled2 = (int32_t)NET_TO_HOST_U32(raw2);
1116 log_info("RECV: network[0]=0x%08x, network[1]=0x%08x, network[2]=0x%08x", raw0, raw1, raw2);
1117 log_info("RECV: scaled[0]=%d, scaled[1]=%d, scaled[2]=%d", scaled0, scaled1, scaled2);
1118 log_info("RECV: samples[0]=%.6f, samples[1]=%.6f, samples[2]=%.6f", samples[0], samples[1], samples[2]);
1119 }
1120#endif
1121
1122 if (client->incoming_audio_buffer) {
1123 asciichat_error_t write_result = audio_ring_buffer_write(client->incoming_audio_buffer, samples, total_samples);
1124 if (write_result != ASCIICHAT_OK) {
1125 log_error("Failed to write decoded audio batch to buffer: %s", asciichat_error_string(write_result));
1126 }
1127 }
1128
1129 SAFE_FREE(samples);
1130}
1131
1167void handle_audio_opus_batch_packet(client_info_t *client, const void *data, size_t len) {
1168 log_debug_every(LOG_RATE_SLOW, "Received Opus audio batch from client %u (len=%zu)", atomic_load(&client->client_id),
1169 len);
1170
1171 VALIDATE_NOTNULL_DATA(client, data, "AUDIO_OPUS_BATCH");
1172 VALIDATE_AUDIO_STREAM_ENABLED(client, "AUDIO_OPUS_BATCH");
1173 VALIDATE_RESOURCE_INITIALIZED(client, client->opus_decoder, "Opus decoder");
1174
1175 // Parse Opus batch packet
1176 const uint8_t *opus_data = NULL;
1177 size_t opus_size = 0;
1178 const uint16_t *frame_sizes = NULL;
1179 int sample_rate = 0;
1180 int frame_duration = 0;
1181 int frame_count = 0;
1182
1183 asciichat_error_t result = packet_parse_opus_batch(data, len, &opus_data, &opus_size, &frame_sizes, &sample_rate,
1184 &frame_duration, &frame_count);
1185
1186 if (result != ASCIICHAT_OK) {
1187 disconnect_client_for_bad_data(client, "Failed to parse AUDIO_OPUS_BATCH packet");
1188 return;
1189 }
1190
1191 VALIDATE_NONZERO(client, frame_count, "frame_count", "AUDIO_OPUS_BATCH");
1192 VALIDATE_NONZERO(client, opus_size, "opus_size", "AUDIO_OPUS_BATCH");
1193
1194 // Calculate samples per frame (20ms @ 48kHz = 960 samples)
1195 int samples_per_frame = (sample_rate * frame_duration) / 1000;
1196 VALIDATE_RANGE(client, samples_per_frame, 1, 4096, "samples_per_frame", "AUDIO_OPUS_BATCH");
1197
1198 // Use static buffer for common case to avoid malloc in hot path
1199 // Typical batches: 1-32 frames of 960 samples = up to 30,720 samples
1200 // Static buffer holds 32 frames @ 48kHz 20ms = 30,720 samples (120KB)
1201#define OPUS_DECODE_STATIC_MAX_SAMPLES (32 * 960)
1202 static float static_decode_buffer[OPUS_DECODE_STATIC_MAX_SAMPLES];
1203
1204 size_t total_samples = (size_t)samples_per_frame * (size_t)frame_count;
1205 float *decoded_samples;
1206 bool used_malloc = false;
1207
1208 if (total_samples <= OPUS_DECODE_STATIC_MAX_SAMPLES) {
1209 decoded_samples = static_decode_buffer;
1210 } else {
1211 // Unusual large batch - fall back to malloc
1212 log_warn("Client %u: Large audio batch requires malloc (%zu samples)", atomic_load(&client->client_id),
1213 total_samples);
1214 decoded_samples = SAFE_MALLOC(total_samples * sizeof(float), float *);
1215 if (!decoded_samples) {
1216 SET_ERRNO(ERROR_MEMORY, "Failed to allocate buffer for Opus decoded samples");
1217 return;
1218 }
1219 used_malloc = true;
1220 }
1221
1222 // Decode each Opus frame using frame_sizes array
1223 int total_decoded = 0;
1224 size_t opus_offset = 0;
1225
1226 for (int i = 0; i < frame_count; i++) {
1227 // Get exact frame size from frame_sizes array (convert from network byte order)
1228 size_t frame_size = (size_t)NET_TO_HOST_U16(frame_sizes[i]);
1229
1230 // DEBUG: Log the actual bytes of each Opus frame
1231 if (frame_size > 0) {
1232 log_debug_every(LOG_RATE_DEFAULT, "Client %u: Opus frame %d: size=%zu, first_bytes=[0x%02x,0x%02x,0x%02x,0x%02x]",
1233 atomic_load(&client->client_id), i, frame_size, opus_data[opus_offset] & 0xFF,
1234 frame_size > 1 ? (opus_data[opus_offset + 1] & 0xFF) : 0,
1235 frame_size > 2 ? (opus_data[opus_offset + 2] & 0xFF) : 0,
1236 frame_size > 3 ? (opus_data[opus_offset + 3] & 0xFF) : 0);
1237 }
1238
1239 if (opus_offset + frame_size > opus_size) {
1240 log_error("Client %u: Frame %d size overflow (offset=%zu, frame_size=%zu, total=%zu)",
1241 atomic_load(&client->client_id), i + 1, opus_offset, frame_size, opus_size);
1242 if (used_malloc) {
1243 SAFE_FREE(decoded_samples);
1244 }
1245 return;
1246 }
1247
1248 // SECURITY: Bounds check before writing decoded samples to prevent buffer overflow
1249 // An attacker could send malicious Opus frames that decode to more samples than expected
1250 if ((size_t)total_decoded + (size_t)samples_per_frame > total_samples) {
1251 log_error("Client %u: Opus decode would overflow buffer (decoded=%d, frame_samples=%d, max=%zu)",
1252 atomic_load(&client->client_id), total_decoded, samples_per_frame, total_samples);
1253 if (used_malloc) {
1254 SAFE_FREE(decoded_samples);
1255 }
1256 return;
1257 }
1258
1259 int decoded_count = opus_codec_decode((opus_codec_t *)client->opus_decoder, &opus_data[opus_offset], frame_size,
1260 &decoded_samples[total_decoded], samples_per_frame);
1261
1262 if (decoded_count < 0) {
1263 log_error("Client %u: Opus decoding failed for frame %d/%d (size=%zu)", atomic_load(&client->client_id), i + 1,
1264 frame_count, frame_size);
1265 if (used_malloc) {
1266 SAFE_FREE(decoded_samples);
1267 }
1268 return;
1269 }
1270
1271 total_decoded += decoded_count;
1272 opus_offset += frame_size;
1273 }
1274
1275 log_debug_every(LOG_RATE_DEFAULT, "Client %u: Decoded %d Opus frames -> %d samples", atomic_load(&client->client_id),
1276 frame_count, total_decoded);
1277
1278 // DEBUG: Log sample values to detect all-zero issue
1279 static int server_decode_count = 0;
1280 server_decode_count++;
1281 if (total_decoded > 0 && (server_decode_count <= 10 || server_decode_count % 100 == 0)) {
1282 float peak = 0.0f, rms = 0.0f;
1283 for (int i = 0; i < total_decoded && i < 100; i++) {
1284 float abs_val = fabsf(decoded_samples[i]);
1285 if (abs_val > peak)
1286 peak = abs_val;
1287 rms += decoded_samples[i] * decoded_samples[i];
1288 }
1289 rms = sqrtf(rms / (total_decoded > 100 ? 100 : total_decoded));
1290 // Log first 4 bytes of Opus data to compare with client encode
1291 log_info("SERVER OPUS DECODE #%d from client %u: decoded_rms=%.6f, opus_first4=[0x%02x,0x%02x,0x%02x,0x%02x]",
1292 server_decode_count, atomic_load(&client->client_id), rms, opus_size > 0 ? opus_data[0] : 0,
1293 opus_size > 1 ? opus_data[1] : 0, opus_size > 2 ? opus_data[2] : 0, opus_size > 3 ? opus_data[3] : 0);
1294 }
1295
1296 // Write decoded samples to client's incoming audio buffer
1297 // Note: audio_ring_buffer_write returns error code, not sample count
1298 // Buffer overflow warnings are logged inside audio_ring_buffer_write if buffer is full
1299 if (client->incoming_audio_buffer && total_decoded > 0) {
1300 asciichat_error_t result = audio_ring_buffer_write(client->incoming_audio_buffer, decoded_samples, total_decoded);
1301 if (result != ASCIICHAT_OK) {
1302 log_error("Client %u: Failed to write decoded audio to buffer: %d", atomic_load(&client->client_id), result);
1303 }
1304 }
1305
1306 if (used_malloc) {
1307 SAFE_FREE(decoded_samples);
1308 }
1309}
1310
1329void handle_audio_opus_packet(client_info_t *client, const void *data, size_t len) {
1330 log_debug_every(LOG_RATE_DEFAULT, "Received Opus audio from client %u (len=%zu)", atomic_load(&client->client_id),
1331 len);
1332
1333 if (VALIDATE_PACKET_NOT_NULL(client, data, "AUDIO_OPUS")) {
1334 return;
1335 }
1336
1337 // Minimum size: 16-byte header + at least 1 byte of Opus data
1338 if (len < 17) {
1339 disconnect_client_for_bad_data(client, "AUDIO_OPUS packet too small: %zu bytes", len);
1340 return;
1341 }
1342
1343 if (!atomic_load(&client->is_sending_audio)) {
1344 disconnect_client_for_bad_data(client, "AUDIO_OPUS received before audio stream enabled");
1345 return;
1346 }
1347
1348 if (!client->opus_decoder) {
1349 disconnect_client_for_bad_data(client, "Opus decoder not initialized");
1350 return;
1351 }
1352
1353 // Parse header (16 bytes) - convert from network byte order
1354 const uint8_t *buf = (const uint8_t *)data;
1355 uint32_t sample_rate_net, frame_duration_net;
1356 memcpy(&sample_rate_net, buf, 4);
1357 memcpy(&frame_duration_net, buf + 4, 4);
1358 uint32_t sample_rate = NET_TO_HOST_U32(sample_rate_net);
1359 uint32_t frame_duration = NET_TO_HOST_U32(frame_duration_net);
1360
1361 // Extract Opus data (after 16-byte header)
1362 const uint8_t *opus_data = buf + 16;
1363 size_t opus_size = len - 16;
1364
1365 // Validate parameters
1366 if (sample_rate == 0 || sample_rate > 192000) {
1367 disconnect_client_for_bad_data(client, "AUDIO_OPUS invalid sample_rate: %u", sample_rate);
1368 return;
1369 }
1370
1371 if (frame_duration == 0 || frame_duration > 120) {
1372 disconnect_client_for_bad_data(client, "AUDIO_OPUS invalid frame_duration: %u ms", frame_duration);
1373 return;
1374 }
1375
1376 // Calculate expected samples per frame
1377 int samples_per_frame = (int)((sample_rate * frame_duration) / 1000);
1378 if (samples_per_frame <= 0 || samples_per_frame > 5760) { // Max 120ms @ 48kHz
1379 disconnect_client_for_bad_data(client, "AUDIO_OPUS invalid samples_per_frame: %d", samples_per_frame);
1380 return;
1381 }
1382
1383 // Decode Opus frame
1384 float decoded_samples[5760]; // Max Opus frame size (120ms @ 48kHz)
1385 int decoded_count =
1386 opus_codec_decode((opus_codec_t *)client->opus_decoder, opus_data, opus_size, decoded_samples, samples_per_frame);
1387
1388 if (decoded_count < 0) {
1389 log_error("Client %u: Opus decoding failed (size=%zu)", atomic_load(&client->client_id), opus_size);
1390 return;
1391 }
1392
1393 log_debug_every(LOG_RATE_VERY_FAST, "Client %u: Decoded Opus frame -> %d samples", atomic_load(&client->client_id),
1394 decoded_count);
1395
1396 // Write decoded samples to client's incoming audio buffer
1397 if (client->incoming_audio_buffer && decoded_count > 0) {
1398 asciichat_error_t write_result =
1399 audio_ring_buffer_write(client->incoming_audio_buffer, decoded_samples, decoded_count);
1400 if (write_result != ASCIICHAT_OK) {
1401 log_error("Failed to write decoded Opus samples to buffer: %s", asciichat_error_string(write_result));
1402 }
1403 }
1404}
1405
1406/* ============================================================================
1407 * Client Configuration Packet Handlers
1408 * ============================================================================
1409 */
1410
1475void handle_client_capabilities_packet(client_info_t *client, const void *data, size_t len) {
1476 log_debug("CLIENT_CAPABILITIES: client_id=%u, data=%p, len=%zu", atomic_load(&client->client_id), data, len);
1477
1478 VALIDATE_PACKET_SIZE(client, data, len, sizeof(terminal_capabilities_packet_t), "CLIENT_CAPABILITIES");
1479
1480 const terminal_capabilities_packet_t *caps = (const terminal_capabilities_packet_t *)data;
1481
1482 // Extract and validate dimensions
1483 uint16_t width = NET_TO_HOST_U16(caps->width);
1484 uint16_t height = NET_TO_HOST_U16(caps->height);
1485 log_error("CLIENT_CAPABILITIES: dimensions=%ux%u", width, height);
1486
1487 VALIDATE_NONZERO(client, width, "width", "CLIENT_CAPABILITIES");
1488 VALIDATE_NONZERO(client, height, "height", "CLIENT_CAPABILITIES");
1489 VALIDATE_RANGE(client, width, 1, 4096, "width", "CLIENT_CAPABILITIES");
1490 VALIDATE_RANGE(client, height, 1, 4096, "height", "CLIENT_CAPABILITIES");
1491
1492 // Extract and validate color level (0=none, 1=16, 2=256, 3=truecolor)
1493 uint32_t color_level = NET_TO_HOST_U32(caps->color_level);
1494 VALIDATE_RANGE(client, color_level, 0, 3, "color_level", "CLIENT_CAPABILITIES");
1495
1496 // Extract and validate render mode (0=foreground, 1=background, 2=half-block)
1497 uint32_t render_mode = NET_TO_HOST_U32(caps->render_mode);
1498 VALIDATE_RANGE(client, render_mode, 0, 2, "render_mode", "CLIENT_CAPABILITIES");
1499
1500 // Extract and validate palette type (0-5 are valid, 5=PALETTE_CUSTOM)
1501 uint32_t palette_type = NET_TO_HOST_U32(caps->palette_type);
1502 VALIDATE_RANGE(client, palette_type, 0, 5, "palette_type", "CLIENT_CAPABILITIES");
1503
1504 // Validate desired FPS (1-144)
1505 VALIDATE_RANGE(client, caps->desired_fps, 1, 144, "desired_fps", "CLIENT_CAPABILITIES");
1506
1507 mutex_lock(&client->client_state_mutex);
1508
1509 atomic_store(&client->width, width);
1510 atomic_store(&client->height, height);
1511
1512 log_debug("Client %u dimensions: %ux%u, desired_fps=%u", atomic_load(&client->client_id), client->width,
1513 client->height, caps->desired_fps);
1514
1515 client->terminal_caps.capabilities = NET_TO_HOST_U32(caps->capabilities);
1516 client->terminal_caps.color_level = color_level;
1517 client->terminal_caps.color_count = NET_TO_HOST_U32(caps->color_count);
1518 client->terminal_caps.render_mode = render_mode;
1519 client->terminal_caps.detection_reliable = caps->detection_reliable;
1520 client->terminal_caps.wants_background = (render_mode == RENDER_MODE_BACKGROUND);
1521
1522 SAFE_STRNCPY(client->terminal_caps.term_type, caps->term_type, sizeof(client->terminal_caps.term_type));
1523 SAFE_STRNCPY(client->terminal_caps.colorterm, caps->colorterm, sizeof(client->terminal_caps.colorterm));
1524
1525 client->terminal_caps.utf8_support = NET_TO_HOST_U32(caps->utf8_support);
1526 client->terminal_caps.palette_type = palette_type;
1527 SAFE_STRNCPY(client->terminal_caps.palette_custom, caps->palette_custom,
1528 sizeof(client->terminal_caps.palette_custom));
1529
1530 client->terminal_caps.desired_fps = caps->desired_fps;
1531
1532 // Extract wants_padding flag (1=padding enabled, 0=no padding for snapshot/piped modes)
1533 client->terminal_caps.wants_padding = (caps->wants_padding != 0);
1534
1535 const char *custom_chars =
1536 (client->terminal_caps.palette_type == PALETTE_CUSTOM && client->terminal_caps.palette_custom[0])
1537 ? client->terminal_caps.palette_custom
1538 : NULL;
1539
1540 if (initialize_client_palette((palette_type_t)client->terminal_caps.palette_type, custom_chars,
1541 client->client_palette_chars, &client->client_palette_len,
1542 client->client_luminance_palette) == 0) {
1543 client->client_palette_type = (palette_type_t)client->terminal_caps.palette_type;
1544 client->client_palette_initialized = true;
1545 log_info("Client %d palette initialized: type=%u, %zu chars, utf8=%u", atomic_load(&client->client_id),
1546 client->terminal_caps.palette_type, client->client_palette_len, client->terminal_caps.utf8_support);
1547 } else {
1548 SET_ERRNO(ERROR_INVALID_STATE, "Failed to initialize palette for client %d", atomic_load(&client->client_id));
1549 client->client_palette_initialized = false;
1550 }
1551
1552 client->has_terminal_caps = true;
1553
1554 log_info("Client %u capabilities: %ux%u, color_level=%s (%u colors), caps=0x%x, term=%s, colorterm=%s, "
1555 "render_mode=%s, reliable=%s, fps=%u, wants_padding=%d",
1556 atomic_load(&client->client_id), client->width, client->height,
1557 terminal_color_level_name(client->terminal_caps.color_level), client->terminal_caps.color_count,
1558 client->terminal_caps.capabilities, client->terminal_caps.term_type, client->terminal_caps.colorterm,
1559 (client->terminal_caps.render_mode == RENDER_MODE_HALF_BLOCK
1560 ? "half-block"
1561 : (client->terminal_caps.render_mode == RENDER_MODE_BACKGROUND ? "background" : "foreground")),
1562 client->terminal_caps.detection_reliable ? "yes" : "no", client->terminal_caps.desired_fps,
1563 client->terminal_caps.wants_padding);
1564
1565 // Send capabilities acknowledgment to client
1566 if (client->socket != INVALID_SOCKET_VALUE) {
1567 log_info_client(client, "Terminal configured: %ux%u, %s, %s mode, %u fps", client->width, client->height,
1568 terminal_color_level_name(client->terminal_caps.color_level),
1569 (client->terminal_caps.render_mode == RENDER_MODE_HALF_BLOCK
1570 ? "half-block"
1571 : (client->terminal_caps.render_mode == RENDER_MODE_BACKGROUND ? "background" : "foreground")),
1572 client->terminal_caps.desired_fps);
1573 }
1574
1575 mutex_unlock(&client->client_state_mutex);
1576}
1577
1612void handle_size_packet(client_info_t *client, const void *data, size_t len) {
1613 VALIDATE_PACKET_SIZE(client, data, len, sizeof(size_packet_t), "SIZE");
1614
1615 const size_packet_t *size_pkt = (const size_packet_t *)data;
1616
1617 // Extract and validate new dimensions
1618 uint16_t width = NET_TO_HOST_U16(size_pkt->width);
1619 uint16_t height = NET_TO_HOST_U16(size_pkt->height);
1620
1621 VALIDATE_NONZERO(client, width, "width", "SIZE");
1622 VALIDATE_NONZERO(client, height, "height", "SIZE");
1623 VALIDATE_RANGE(client, width, 1, 4096, "width", "SIZE");
1624 VALIDATE_RANGE(client, height, 1, 4096, "height", "SIZE");
1625
1626 mutex_lock(&client->client_state_mutex);
1627 client->width = width;
1628 client->height = height;
1629 mutex_unlock(&client->client_state_mutex);
1630
1631 log_info("Client %u updated terminal size: %ux%u", atomic_load(&client->client_id), width, height);
1632}
1633
1634/* ============================================================================
1635 * Protocol Control Packet Handlers
1636 * ============================================================================
1637 */
1638
1639/* ============================================================================
1640 * Protocol Utility Functions
1641 * ============================================================================
1642 */
1643
1686int send_server_state_to_client(client_info_t *client) {
1687 if (!client) {
1688 return -1;
1689 }
1690
1691 // Count active clients - LOCK OPTIMIZATION: Use atomic reads, no rwlock needed
1692 int active_count = 0;
1693 for (int i = 0; i < MAX_CLIENTS; i++) {
1694 if (atomic_load(&g_client_manager.clients[i].active)) {
1695 active_count++;
1696 }
1697 }
1698
1699 // Prepare server state packet
1700 server_state_packet_t state;
1701 state.connected_client_count = active_count;
1702 state.active_client_count = active_count; // For now, all connected are active
1703 memset(state.reserved, 0, sizeof(state.reserved));
1704
1705 // Convert to network byte order
1706 server_state_packet_t net_state;
1707 net_state.connected_client_count = HOST_TO_NET_U32(state.connected_client_count);
1708 net_state.active_client_count = HOST_TO_NET_U32(state.active_client_count);
1709 memset(net_state.reserved, 0, sizeof(net_state.reserved));
1710
1711 // Send server state via ACIP transport
1712 // Protect socket writes with send_mutex to prevent race with send_thread.
1713 mutex_lock(&client->send_mutex);
1714 asciichat_error_t result = acip_send_server_state(client->transport, &net_state);
1715 mutex_unlock(&client->send_mutex);
1716
1717 if (result != ASCIICHAT_OK) {
1718 SET_ERRNO(ERROR_NETWORK, "Failed to send server state to client %u: %s", client->client_id,
1719 asciichat_error_string(result));
1720 return -1;
1721 }
1722
1723 log_debug("Sent server state to client %u: %u connected, %u active", client->client_id, state.connected_client_count,
1724 state.active_client_count);
1725 return 0;
1726}
1727
1753// NOTE: This function is no longer used - CLEAR_CONSOLE is now sent directly
1754// from each client's render thread when it detects a grid layout change.
1755// Keeping this for reference but it should not be called.
1757 SET_ERRNO(ERROR_INVALID_STATE, "broadcast_clear_console_to_all_clients() called - unexpected usage");
1758 log_warn("CLEAR_CONSOLE is now sent from render threads, not broadcast");
1759}
Per-client state management and lifecycle orchestration.
const crypto_context_t * crypto_handshake_get_context(const crypto_handshake_context_t *ctx)
void handle_audio_opus_packet(client_info_t *client, const void *data, size_t len)
Process AUDIO_OPUS packet - decode single Opus frame from client.
void handle_audio_opus_batch_packet(client_info_t *client, const void *data, size_t len)
Process AUDIO_OPUS_BATCH packet - efficient Opus-encoded audio batch from client.
int socket_t
asciichat_error_t audio_ring_buffer_write(audio_ring_buffer_t *rb, const float *data, int samples)
asciichat_error_t audio_parse_batch_header(const void *data, size_t len, audio_batch_info_t *out_batch)
asciichat_error_t audio_dequantize_samples(const uint8_t *samples_ptr, uint32_t total_samples, float *out_samples)
asciichat_error_t acip_send_server_state(acip_transport_t *transport, const server_state_packet_t *state)
void log_msg(log_level_t level, const char *file, int line, const char *func, const char *fmt,...)
asciichat_error_t log_network_message(socket_t sockfd, const struct crypto_context_t *crypto_ctx, log_level_t level, remote_log_direction_t direction, const char *fmt,...)
asciichat_error_t frame_validate_legacy(size_t len, size_t expected_rgb_size)
asciichat_error_t frame_check_size_overflow(size_t header_size, size_t data_size)
action_args_t args
opus_codec_t * opus_codec_create_decoder(int sample_rate)
Definition opus_codec.c:62
int opus_codec_decode(opus_codec_t *codec, const uint8_t *data, size_t data_len, float *out_samples, int out_num_samples)
Definition opus_codec.c:128
asciichat_error_t packet_parse_remote_log(const void *data, size_t len, log_level_t *out_level, remote_log_direction_t *out_direction, uint16_t *out_flags, char *message_buffer, size_t message_buffer_size, size_t *out_message_length)
Definition packet.c:950
asciichat_error_t packet_send_error(socket_t sockfd, const crypto_context_t *crypto_ctx, asciichat_error_t error_code, const char *message)
Definition packet.c:804
asciichat_error_t packet_parse_opus_batch(const void *packet_data, size_t packet_len, const uint8_t **out_opus_data, size_t *out_opus_size, const uint16_t **out_frame_sizes, int *out_sample_rate, int *out_frame_duration, int *out_frame_count)
void packet_queue_stop(packet_queue_t *queue)
int initialize_client_palette(palette_type_t palette_type, const char *custom_chars, char client_palette_chars[256], size_t *client_palette_len, char client_luminance_palette[256])
Definition palette.c:299
void platform_sleep_ms(unsigned int ms)
asciichat_error_t acip_send_error(acip_transport_t *transport, uint32_t error_code, const char *message)
Definition send.c:233
asciichat_error_t acip_send_pong(acip_transport_t *transport)
Definition send.c:212
atomic_bool g_server_should_exit
Global atomic shutdown flag shared across all threads.
ascii-chat Server Mode Entry Point Header
void handle_client_join_packet(client_info_t *client, const void *data, size_t len)
Process CLIENT_JOIN packet - client announces identity and capabilities.
void handle_pong_packet(client_info_t *client, const void *data, size_t len)
Handle PONG packet - client acknowledged our PING.
void handle_protocol_version_packet(client_info_t *client, const void *data, size_t len)
Process PROTOCOL_VERSION packet - validate protocol compatibility.
void handle_image_frame_packet(client_info_t *client, void *data, size_t len)
Process IMAGE_FRAME packet - store client's video data for rendering.
void handle_audio_batch_packet(client_info_t *client, const void *data, size_t len)
Process AUDIO_BATCH packet - store efficiently batched audio samples.
void handle_size_packet(client_info_t *client, const void *data, size_t len)
Process terminal size update packet - handle client window resize.
void handle_audio_packet(client_info_t *client, const void *data, size_t len)
Process AUDIO packet - store single audio sample batch (legacy format)
void handle_client_leave_packet(client_info_t *client, const void *data, size_t len)
Process CLIENT_LEAVE packet - handle clean client disconnect.
#define OPUS_DECODE_STATIC_MAX_SAMPLES
void handle_stream_stop_packet(client_info_t *client, const void *data, size_t len)
Process STREAM_STOP packet - client requests to halt media transmission.
int send_server_state_to_client(client_info_t *client)
Send current server state to a specific client.
void handle_client_capabilities_packet(client_info_t *client, const void *data, size_t len)
Process CLIENT_CAPABILITIES packet - configure client-specific rendering.
void broadcast_clear_console_to_all_clients(void)
Signal all active clients to clear their displays before next video frame.
void handle_stream_start_packet(client_info_t *client, const void *data, size_t len)
Process STREAM_START packet - client requests to begin media transmission.
void handle_remote_log_packet_from_client(client_info_t *client, const void *data, size_t len)
void disconnect_client_for_bad_data(client_info_t *client, const char *format,...)
void handle_ping_packet(client_info_t *client, const void *data, size_t len)
Handle PING packet - respond with PONG.
Server packet processing and protocol implementation.
client_manager_t g_client_manager
Global client manager singleton - central coordination point.
client_info_t clients[MAX_CLIENTS]
Array of client_info_t structures (backing storage)
Definition client.h:65
int safe_vsnprintf(char *buffer, size_t buffer_size, const char *format, va_list ap)
Safe formatted string printing with va_list.
Definition system.c:507
void format_bytes_pretty(size_t bytes, char *out, size_t out_capacity)
Definition util/format.c:10
asciichat_error_t image_validate_buffer_size(size_t requested_size)
Definition util/image.c:115
asciichat_error_t image_calc_rgb_size(size_t width, size_t height, size_t *out_size)
Definition util/image.c:54
asciichat_error_t image_validate_dimensions(size_t width, size_t height)
Definition video.c:12
video_frame_t * video_frame_begin_write(video_frame_buffer_t *vfb)
void video_frame_commit(video_frame_buffer_t *vfb)