#define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* * onvif_kiosk_cam.c * * Minimal ONVIF shim for a synthetic kiosk camera. * * Features: * - WS-Discovery listener on UDP 3702 * - SOAP device endpoint: /onvif/device_service * - SOAP media endpoint: /onvif/media_service * - One fixed H.264 profile * - Returns RTSP URI for the live stream * - Optional snapshot URI support * * Build: * gcc -O2 -Wall -Wextra -pthread -o onvif_kiosk_cam onvif_kiosk_cam.c -lmicrohttpd * * Example: * ./onvif_kiosk_cam \ * --http-port 8080 \ * --xaddr-host 10.1.10.50 \ * --rtsp-uri rtsp://10.1.10.50:8554/kiosk \ * --snapshot-uri http://10.1.10.50:8080/snapshot.jpg \ * --manufacturer CPRD \ * --model DashboardCam-1080p \ * --debug */ #define SOAP_DEVICE_PATH "/onvif/device_service" #define SOAP_MEDIA_PATH "/onvif/media_service" #define SNAPSHOT_PATH "/snapshot.jpg" #define MULTICAST_GROUP "239.255.255.250" #define WSD_PORT 3702 #define DEFAULT_HTTP_PORT 8080 #define UUID_BUFSZ 128 #define XML_BUFSZ 65536 #define RECV_BUFSZ 16384 static volatile sig_atomic_t g_running = 1; struct config { int http_port; char xaddr_host[256]; char rtsp_uri[1024]; char snapshot_uri[1024]; char manufacturer[128]; char model[128]; char firmware[64]; char serial[64]; char hardware_id[64]; char scopes[1024]; char endpoint_uuid[UUID_BUFSZ]; int width; int height; int fps; int bitrate_kbps; int discovery_ttl; int debug; }; struct request_ctx { char *body; size_t body_len; }; static struct config g_cfg = { .http_port = DEFAULT_HTTP_PORT, .xaddr_host = "127.0.0.1", .rtsp_uri = "rtsp://127.0.0.1:8554/kiosk", .snapshot_uri = "", .manufacturer = "CPRD", .model = "DashboardCam-1080p", .firmware = "1.1", .serial = "KIOSKCAM001", .hardware_id = "KCAM-1080P", .scopes = "onvif://www.onvif.org/type/video_encoder onvif://www.onvif.org/Profile/Streaming onvif://www.onvif.org/name/DashboardCam onvif://www.onvif.org/location/virtual", .endpoint_uuid = "550e8400-e29b-41d4-a716-446655440000", .width = 1920, .height = 1080, .fps = 15, .bitrate_kbps = 2048, .discovery_ttl = 1, .debug = 0, }; static void logmsg(const char *fmt, ...) { va_list ap; va_start(ap, fmt); vfprintf(stderr, fmt, ap); fprintf(stderr, " "); va_end(ap); } static void debugf(const char *fmt, ...) { if (!g_cfg.debug) return; va_list ap; va_start(ap, fmt); vfprintf(stderr, fmt, ap); fprintf(stderr, " "); va_end(ap); } static void on_signal(int sig) { (void)sig; g_running = 0; } static void xml_escape(const char *src, char *dst, size_t dstsz) { size_t di = 0; if (!src || dstsz == 0) return; for (size_t i = 0; src[i] && di + 8 < dstsz; i++) { switch (src[i]) { case '&': di += snprintf(dst + di, dstsz - di, "&"); break; case '<': di += snprintf(dst + di, dstsz - di, "<"); break; case '>': di += snprintf(dst + di, dstsz - di, ">"); break; case '"': di += snprintf(dst + di, dstsz - di, """); break; case '\'': di += snprintf(dst + di, dstsz - di, "'"); break; default: dst[di++] = src[i]; dst[di] = ''; break; } } dst[di] = ''; } static void build_xaddr(char *buf, size_t bufsz, const char *path) { snprintf(buf, bufsz, "http://%s:%d%s", g_cfg.xaddr_host, g_cfg.http_port, path); } static void build_default_snapshot_uri_if_needed(void) { if (g_cfg.snapshot_uri[0] != '') return; snprintf(g_cfg.snapshot_uri, sizeof(g_cfg.snapshot_uri), "http://%s:%d%s", g_cfg.xaddr_host, g_cfg.http_port, SNAPSHOT_PATH); } static void find_message_id(const char *xml, char *out, size_t outsz) { const char *p = strstr(xml, ""); if (!p) p = strstr(xml, ""); if (!p) { snprintf(out, outsz, "uuid:%s", g_cfg.endpoint_uuid); return; } p = strchr(p, '>'); if (!p) { snprintf(out, outsz, "uuid:%s", g_cfg.endpoint_uuid); return; } p++; const char *e = strchr(p, '<'); if (!e) { snprintf(out, outsz, "uuid:%s", g_cfg.endpoint_uuid); return; } size_t n = (size_t)(e - p); if (n >= outsz) n = outsz - 1; memcpy(out, p, n); out[n] = ''; } static int has_probe(const char *xml) { return xml && strstr(xml, "Probe") != NULL; } static int wants_onvif_video(const char *xml) { if (!xml) return 0; if (strstr(xml, "NetworkVideoTransmitter")) return 1; if (strstr(xml, "dn:NetworkVideoTransmitter")) return 1; if (strstr(xml, "tds:Device")) return 1; if (strstr(xml, "onvif") || strstr(xml, "Onvif") || strstr(xml, "ONVIF")) return 1; return 0; } static char *soap_envelope(const char *body) { char *xml = malloc(XML_BUFSZ); if (!xml) return NULL; snprintf( xml, XML_BUFSZ, "" "" "%s" "", body ? body : "" ); return xml; } static char *build_get_device_information_response(void) { char body[4096]; snprintf(body, sizeof(body), "" "%s" "%s" "%s" "%s" "%s" "", g_cfg.manufacturer, g_cfg.model, g_cfg.firmware, g_cfg.serial, g_cfg.hardware_id ); return soap_envelope(body); } static char *build_get_services_response(void) { char device_xaddr[512], media_xaddr[512]; build_xaddr(device_xaddr, sizeof(device_xaddr), SOAP_DEVICE_PATH); build_xaddr(media_xaddr, sizeof(media_xaddr), SOAP_MEDIA_PATH); char body[8192]; snprintf(body, sizeof(body), "" "" "http://www.onvif.org/ver10/device/wsdl" "%s" "210" "" "" "http://www.onvif.org/ver10/media/wsdl" "%s" "210" "" "", device_xaddr, media_xaddr ); return soap_envelope(body); } static char *build_get_capabilities_response(void) { char device_xaddr[512], media_xaddr[512]; build_xaddr(device_xaddr, sizeof(device_xaddr), SOAP_DEVICE_PATH); build_xaddr(media_xaddr, sizeof(media_xaddr), SOAP_MEDIA_PATH); char body[16384]; snprintf(body, sizeof(body), "" "" "" "" "false" "false" "false" "false" "" "" "false" "false" "false" "false" "false" "false" "" "00" "" "false" "false" "false" "false" "false" "false" "false" "false" "" "" "" "" "false" "true" "true" "" "1" "" "" "", device_xaddr, media_xaddr ); return soap_envelope(body); } static char *build_get_profiles_response(void) { char body[16384]; snprintf(body, sizeof(body), "" "" "MainProfile" "" "VideoSource" "1" "source_1" "" "" "" "H264" "1" "H264" "%d%d" "5" "" "%d" "1" "%d" "" "%dHigh" "" "IPv40.0.0.0" "01false" "" "PT60S" "" "" "", g_cfg.width, g_cfg.height, g_cfg.width, g_cfg.height, g_cfg.fps, g_cfg.bitrate_kbps, g_cfg.fps * 2 ); return soap_envelope(body); } static char *build_get_stream_uri_response(void) { char uri_esc[2048]; xml_escape(g_cfg.rtsp_uri, uri_esc, sizeof(uri_esc)); char body[4096]; snprintf(body, sizeof(body), "" "" "%s" "false" "false" "PT60S" "" "", uri_esc ); return soap_envelope(body); } static char *build_get_snapshot_uri_response(void) { char uri_esc[2048]; build_default_snapshot_uri_if_needed(); xml_escape(g_cfg.snapshot_uri, uri_esc, sizeof(uri_esc)); char body[4096]; snprintf(body, sizeof(body), "" "" "%s" "false" "false" "PT60S" "" "", uri_esc ); return soap_envelope(body); } static char *build_fault_response(const char *reason) { char reason_esc[512]; xml_escape(reason ? reason : "Unsupported action", reason_esc, sizeof(reason_esc)); char body[2048]; snprintf(body, sizeof(body), "" "s:Sender" "%s" "", reason_esc ); return soap_envelope(body); } static char *dispatch_device_action(const char *body) { if (strstr(body, "GetDeviceInformation")) return build_get_device_information_response(); if (strstr(body, "GetServices")) return build_get_services_response(); if (strstr(body, "GetCapabilities")) return build_get_capabilities_response(); return build_fault_response("Unsupported device action"); } static char *dispatch_media_action(const char *body) { if (strstr(body, "GetProfiles")) return build_get_profiles_response(); if (strstr(body, "GetStreamUri")) return build_get_stream_uri_response(); if (strstr(body, "GetSnapshotUri")) return build_get_snapshot_uri_response(); return build_fault_response("Unsupported media action"); } static int send_text_response(struct MHD_Connection *connection, unsigned int status, const char *ctype, char *payload, int must_free) { struct MHD_Response *response = MHD_create_response_from_buffer(strlen(payload), (void *)payload, must_free ? MHD_RESPMEM_MUST_FREE : MHD_RESPMEM_PERSISTENT); if (!response) { if (must_free) free(payload); return MHD_NO; } MHD_add_response_header(response, "Content-Type", ctype); MHD_add_response_header(response, "Server", "onvif-kiosk-cam/1.1"); int ret = MHD_queue_response(connection, status, response); MHD_destroy_response(response); return ret; } static enum MHD_Result ahc_handler( void *cls, struct MHD_Connection *connection, const char *url, const char *method, const char *version, const char *upload_data, size_t *upload_data_size, void **con_cls ) { (void)cls; (void)version; struct request_ctx *ctx = *con_cls; if (!ctx) { ctx = calloc(1, sizeof(*ctx)); if (!ctx) return MHD_NO; *con_cls = ctx; return MHD_YES; } if (strcmp(method, "GET") == 0 && strcmp(url, "/") == 0) { const char *msg = "onvif_kiosk_cam alive "; return send_text_response(connection, MHD_HTTP_OK, "text/plain", (char *)msg, 0); } if (strcmp(method, "GET") == 0 && strcmp(url, SNAPSHOT_PATH) == 0) { const char *msg = "snapshot placeholder "; return send_text_response(connection, MHD_HTTP_NOT_IMPLEMENTED, "text/plain", (char *)msg, 0); } if (strcmp(method, "POST") != 0) { const char *msg = "Method not allowed "; return send_text_response(connection, MHD_HTTP_METHOD_NOT_ALLOWED, "text/plain", (char *)msg, 0); } if (*upload_data_size > 0) { char *newbuf = realloc(ctx->body, ctx->body_len + *upload_data_size + 1); if (!newbuf) return MHD_NO; ctx->body = newbuf; memcpy(ctx->body + ctx->body_len, upload_data, *upload_data_size); ctx->body_len += *upload_data_size; ctx->body[ctx->body_len] = ''; *upload_data_size = 0; return MHD_YES; } char *response_xml = NULL; if (strcmp(url, SOAP_DEVICE_PATH) == 0) { debugf("HTTP POST %s %s", url, ctx->body ? ctx->body : ""); response_xml = dispatch_device_action(ctx->body ? ctx->body : ""); } else if (strcmp(url, SOAP_MEDIA_PATH) == 0) { debugf("HTTP POST %s %s", url, ctx->body ? ctx->body : ""); response_xml = dispatch_media_action(ctx->body ? ctx->body : ""); } else { const char *msg = "Not found "; return send_text_response(connection, MHD_HTTP_NOT_FOUND, "text/plain", (char *)msg, 0); } if (!response_xml) { const char *msg = "Internal error "; return send_text_response(connection, MHD_HTTP_INTERNAL_SERVER_ERROR, "text/plain", (char *)msg, 0); } return send_text_response(connection, MHD_HTTP_OK, "application/soap+xml; charset=utf-8", response_xml, 1); } static void request_completed(void *cls, struct MHD_Connection *connection, void **con_cls, enum MHD_RequestTerminationCode toe) { (void)cls; (void)connection; (void)toe; struct request_ctx *ctx = *con_cls; if (ctx) { free(ctx->body); free(ctx); *con_cls = NULL; } } static char *build_probe_match_response(const char *relates_to) { char xaddr[512], scopes_esc[2048]; build_xaddr(xaddr, sizeof(xaddr), SOAP_DEVICE_PATH); xml_escape(g_cfg.scopes, scopes_esc, sizeof(scopes_esc)); char *xml = malloc(XML_BUFSZ); if (!xml) return NULL; snprintf(xml, XML_BUFSZ, "" "" "" "http://schemas.xmlsoap.org/ws/2005/04/discovery/ProbeMatches" "uuid:%s" "%s" "http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous" "" "" "" "" "urn:uuid:%s" "dn:NetworkVideoTransmitter tdn:Device" "%s" "%s" "1" "" "" "" "", g_cfg.endpoint_uuid, relates_to, g_cfg.endpoint_uuid, scopes_esc, xaddr ); return xml; } static void *discovery_thread(void *arg) { (void)arg; int sock = socket(AF_INET, SOCK_DGRAM, 0); if (sock < 0) { logmsg("discovery socket failed: %s", strerror(errno)); return NULL; } int yes = 1; setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); struct sockaddr_in bind_addr; memset(&bind_addr, 0, sizeof(bind_addr)); bind_addr.sin_family = AF_INET; bind_addr.sin_port = htons(WSD_PORT); bind_addr.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(sock, (struct sockaddr *)&bind_addr, sizeof(bind_addr)) < 0) { logmsg("bind UDP 3702 failed: %s", strerror(errno)); close(sock); return NULL; } struct ip_mreq mreq; memset(&mreq, 0, sizeof(mreq)); mreq.imr_multiaddr.s_addr = inet_addr(MULTICAST_GROUP); mreq.imr_interface.s_addr = htonl(INADDR_ANY); if (setsockopt(sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) { logmsg("IP_ADD_MEMBERSHIP failed: %s", strerror(errno)); close(sock); return NULL; } unsigned char ttl = (unsigned char)g_cfg.discovery_ttl; setsockopt(sock, IPPROTO_IP, IP_MULTICAST_TTL, &ttl, sizeof(ttl)); struct timeval tv = {.tv_sec = 1, .tv_usec = 0}; setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); logmsg("WS-Discovery listening on UDP %d", WSD_PORT); while (g_running) { char buf[RECV_BUFSZ]; struct sockaddr_in src; socklen_t slen = sizeof(src); ssize_t n = recvfrom(sock, buf, sizeof(buf) - 1, 0, (struct sockaddr *)&src, &slen); if (n < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) continue; logmsg("recvfrom failed: %s", strerror(errno)); continue; } buf[n] = ''; char src_ip[64]; inet_ntop(AF_INET, &src.sin_addr, src_ip, sizeof(src_ip)); debugf("WSD from %s:%u %s", src_ip, ntohs(src.sin_port), buf); if (!has_probe(buf)) continue; if (!wants_onvif_video(buf)) continue; char relates_to[512]; find_message_id(buf, relates_to, sizeof(relates_to)); char *resp = build_probe_match_response(relates_to); if (!resp) continue; ssize_t sent = sendto(sock, resp, strlen(resp), 0, (struct sockaddr *)&src, slen); if (sent < 0) { logmsg("sendto failed: %s", strerror(errno)); } else { debugf("Sent ProbeMatch to %s:%u", src_ip, ntohs(src.sin_port)); } free(resp); } close(sock); return NULL; } static int parse_arg_value(int *i, int argc, char **argv, const char **out) { if (*i + 1 >= argc) return -1; *out = argv[++(*i)]; return 0; } static void usage(const char *prog) { fprintf(stderr, "Usage: %s [options] " " --http-port N " " --xaddr-host HOST_OR_IP " " --rtsp-uri URI " " --snapshot-uri URI " " --manufacturer TEXT " " --model TEXT " " --firmware TEXT " " --serial TEXT " " --hardware-id TEXT " " --uuid UUID " " --width N " " --height N " " --fps N " " --bitrate-kbps N " " --scopes TEXT " " --debug ", prog ); } static int parse_args(int argc, char **argv) { for (int i = 1; i < argc; i++) { const char *v = NULL; if (strcmp(argv[i], "--http-port") == 0) { if (parse_arg_value(&i, argc, argv, &v) < 0) return -1; g_cfg.http_port = atoi(v); } else if (strcmp(argv[i], "--xaddr-host") == 0) { if (parse_arg_value(&i, argc, argv, &v) < 0) return -1; snprintf(g_cfg.xaddr_host, sizeof(g_cfg.xaddr_host), "%s", v); } else if (strcmp(argv[i], "--rtsp-uri") == 0) { if (parse_arg_value(&i, argc, argv, &v) < 0) return -1; snprintf(g_cfg.rtsp_uri, sizeof(g_cfg.rtsp_uri), "%s", v); } else if (strcmp(argv[i], "--snapshot-uri") == 0) { if (parse_arg_value(&i, argc, argv, &v) < 0) return -1; snprintf(g_cfg.snapshot_uri, sizeof(g_cfg.snapshot_uri), "%s", v); } else if (strcmp(argv[i], "--manufacturer") == 0) { if (parse_arg_value(&i, argc, argv, &v) < 0) return -1; snprintf(g_cfg.manufacturer, sizeof(g_cfg.manufacturer), "%s", v); } else if (strcmp(argv[i], "--model") == 0) { if (parse_arg_value(&i, argc, argv, &v) < 0) return -1; snprintf(g_cfg.model, sizeof(g_cfg.model), "%s", v); } else if (strcmp(argv[i], "--firmware") == 0) { if (parse_arg_value(&i, argc, argv, &v) < 0) return -1; snprintf(g_cfg.firmware, sizeof(g_cfg.firmware), "%s", v); } else if (strcmp(argv[i], "--serial") == 0) { if (parse_arg_value(&i, argc, argv, &v) < 0) return -1; snprintf(g_cfg.serial, sizeof(g_cfg.serial), "%s", v); } else if (strcmp(argv[i], "--hardware-id") == 0) { if (parse_arg_value(&i, argc, argv, &v) < 0) return -1; snprintf(g_cfg.hardware_id, sizeof(g_cfg.hardware_id), "%s", v); } else if (strcmp(argv[i], "--uuid") == 0) { if (parse_arg_value(&i, argc, argv, &v) < 0) return -1; snprintf(g_cfg.endpoint_uuid, sizeof(g_cfg.endpoint_uuid), "%s", v); } else if (strcmp(argv[i], "--width") == 0) { if (parse_arg_value(&i, argc, argv, &v) < 0) return -1; g_cfg.width = atoi(v); } else if (strcmp(argv[i], "--height") == 0) { if (parse_arg_value(&i, argc, argv, &v) < 0) return -1; g_cfg.height = atoi(v); } else if (strcmp(argv[i], "--fps") == 0) { if (parse_arg_value(&i, argc, argv, &v) < 0) return -1; g_cfg.fps = atoi(v); } else if (strcmp(argv[i], "--bitrate-kbps") == 0) { if (parse_arg_value(&i, argc, argv, &v) < 0) return -1; g_cfg.bitrate_kbps = atoi(v); } else if (strcmp(argv[i], "--scopes") == 0) { if (parse_arg_value(&i, argc, argv, &v) < 0) return -1; snprintf(g_cfg.scopes, sizeof(g_cfg.scopes), "%s", v); } else if (strcmp(argv[i], "--debug") == 0) { g_cfg.debug = 1; } else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { usage(argv[0]); exit(0); } else { fprintf(stderr, "Unknown option: %s ", argv[i]); return -1; } } build_default_snapshot_uri_if_needed(); return 0; } int main(int argc, char **argv) { signal(SIGINT, on_signal); signal(SIGTERM, on_signal); if (parse_args(argc, argv) < 0) { usage(argv[0]); return 1; } logmsg("Starting onvif_kiosk_cam"); logmsg("HTTP: 0.0.0.0:%d", g_cfg.http_port); logmsg("Device XAddr host: %s", g_cfg.xaddr_host); logmsg("RTSP URI: %s", g_cfg.rtsp_uri); logmsg("Snapshot URI: %s", g_cfg.snapshot_uri); pthread_t tid; if (pthread_create(&tid, NULL, discovery_thread, NULL) != 0) { logmsg("pthread_create failed"); return 1; } struct MHD_Daemon *httpd = MHD_start_daemon( MHD_USE_INTERNAL_POLLING_THREAD, (uint16_t)g_cfg.http_port, NULL, NULL, &ahc_handler, NULL, MHD_OPTION_NOTIFY_COMPLETED, request_completed, NULL, MHD_OPTION_END ); if (!httpd) { logmsg("Failed to start HTTP daemon"); g_running = 0; pthread_join(tid, NULL); return 1; } while (g_running) { sleep(1); } MHD_stop_daemon(httpd); pthread_join(tid, NULL); logmsg("Stopped"); return 0; }