mirror of
https://github.com/ZoneMinder/zoneminder.git
synced 2026-10-02 15:35:09 -04:00
Merge pull request #5051 from AJ0070/fix/go2rtc-encoding
fix: percent-encode non-ASCII bytes in UriEncode and escape go2rtc JSON fields
This commit is contained in:
3 files changed
+115
-22
No files matched your search
@@ -69,8 +69,12 @@ Monitor::Go2RTCManager::Go2RTCManager(Monitor *parent_)
|
||||
rtsp_second_path = parent->GetSecondPath();
|
||||
|
||||
if (!parent->user.empty()) {
|
||||
rtsp_username = escape_json_string(parent->user);
|
||||
rtsp_password = escape_json_string(parent->pass);
|
||||
// Stored raw. These end up in an RTSP URL, not in JSON, and the JSON
|
||||
// payloads that embed the assembled path escape it themselves at the point
|
||||
// it becomes JSON. Escaping here as well meant a password containing a
|
||||
// backslash reached go2rtc doubled.
|
||||
rtsp_username = parent->user;
|
||||
rtsp_password = parent->pass;
|
||||
if (rtsp_path.find("rtsp://") == 0) {
|
||||
rtsp_path = "rtsp://" + rtsp_username + ":" + rtsp_password + "@" + rtsp_path.substr(7, std::string::npos);
|
||||
} else {
|
||||
@@ -227,7 +231,7 @@ int Monitor::Go2RTCManager::add_to_Go2RTC() {
|
||||
Debug(1, "Go2RTC: Adding primary stream (monitor ID) - path: %s%s",
|
||||
primary_path.c_str(), Use_RTSP_Restream ? " (via RTSP restreamer)" : "");
|
||||
std::string endpoint = Go2RTC_endpoint + "/streams?name=" + id_str + "&src=" + UriEncode(primary_path);
|
||||
std::string postData = "{\"name\" : \"" + std::string(parent->Name()) + "\", \"src\": \"" + primary_path + "\" }";
|
||||
std::string postData = "{\"name\" : \"" + escape_json_string(parent->Name()) + "\", \"src\": \"" + escape_json_string(primary_path) + "\" }";
|
||||
Debug(2, "Go2RTC: PUT to %s with data: %s", endpoint.c_str(), postData.c_str());
|
||||
std::pair<CURLcode, std::string> response = CURL_PUT(endpoint, postData);
|
||||
if (response.first != CURLE_OK) {
|
||||
@@ -247,7 +251,7 @@ int Monitor::Go2RTCManager::add_to_Go2RTC() {
|
||||
std::string transcode_src = "ffmpeg:" + id_str + "#video=h264";
|
||||
Debug(1, "Go2RTC: Adding H.264 transcode stream - src: %s", transcode_src.c_str());
|
||||
endpoint = Go2RTC_endpoint + "/streams?name=" + id_str + "_h264&src=" + UriEncode(transcode_src);
|
||||
postData = "{\"name\" : \"" + std::string(parent->Name()) + " H264\", \"src\": \"" + transcode_src + "\" }";
|
||||
postData = "{\"name\" : \"" + escape_json_string(parent->Name()) + " H264\", \"src\": \"" + escape_json_string(transcode_src) + "\" }";
|
||||
Debug(2, "Go2RTC: PUT to %s", endpoint.c_str());
|
||||
response = CURL_PUT(endpoint, postData);
|
||||
if (response.first == CURLE_OK) {
|
||||
@@ -259,7 +263,7 @@ int Monitor::Go2RTCManager::add_to_Go2RTC() {
|
||||
if (Use_RTSP_Restream) {
|
||||
Debug(1, "Go2RTC: Adding ZoneMinderPrimary stream - path: %s", rtsp_restream_path.c_str());
|
||||
endpoint = Go2RTC_endpoint + "/streams?name=" + id_str + "_ZoneMinderPrimary&src=" + UriEncode(rtsp_restream_path);
|
||||
postData = "{\"name\" : \"" + std::string(parent->Name()) + " ZoneMinder Primary\", \"src\": \"" + rtsp_restream_path + "\" }";
|
||||
postData = "{\"name\" : \"" + escape_json_string(parent->Name()) + " ZoneMinder Primary\", \"src\": \"" + escape_json_string(rtsp_restream_path) + "\" }";
|
||||
Debug(2, "Go2RTC: PUT to %s", endpoint.c_str());
|
||||
response = CURL_PUT(endpoint, postData);
|
||||
if (response.first == CURLE_OK) {
|
||||
@@ -271,7 +275,7 @@ int Monitor::Go2RTCManager::add_to_Go2RTC() {
|
||||
if (!rtsp_path.empty()) {
|
||||
Debug(1, "Go2RTC: Adding CameraDirectPrimary stream - path: %s", rtsp_path.c_str());
|
||||
endpoint = Go2RTC_endpoint + "/streams?name=" + id_str + "_CameraDirectPrimary&src=" + UriEncode(rtsp_path);
|
||||
postData = "{\"name\" : \"" + std::string(parent->Name()) + " Camera Direct Primary\", \"src\": \"" + rtsp_path + "\" }";
|
||||
postData = "{\"name\" : \"" + escape_json_string(parent->Name()) + " Camera Direct Primary\", \"src\": \"" + escape_json_string(rtsp_path) + "\" }";
|
||||
Debug(2, "Go2RTC: PUT to %s", endpoint.c_str());
|
||||
response = CURL_PUT(endpoint, postData);
|
||||
if (response.first == CURLE_OK) {
|
||||
@@ -282,7 +286,7 @@ int Monitor::Go2RTCManager::add_to_Go2RTC() {
|
||||
if (!rtsp_second_path.empty()) {
|
||||
Debug(1, "Go2RTC: Adding CameraDirectSecondary stream - path: %s", rtsp_second_path.c_str());
|
||||
endpoint = Go2RTC_endpoint + "/streams?name=" + id_str + "_CameraDirectSecondary&src=" + UriEncode(rtsp_second_path);
|
||||
postData = "{\"name\" : \"" + std::string(parent->Name()) + " Camera Direct Secondary\", \"src\": \"" + rtsp_second_path + "\" }";
|
||||
postData = "{\"name\" : \"" + escape_json_string(parent->Name()) + " Camera Direct Secondary\", \"src\": \"" + escape_json_string(rtsp_second_path) + "\" }";
|
||||
Debug(2, "Go2RTC: PUT to %s", endpoint.c_str());
|
||||
response = CURL_PUT(endpoint, postData);
|
||||
if (response.first == CURLE_OK) {
|
||||
|
||||
+44
-15
@@ -24,6 +24,7 @@
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <fcntl.h> /* Definition of AT_* constants */
|
||||
#include <unistd.h>
|
||||
@@ -403,15 +404,19 @@ std::string UriEncode(const std::string &value) {
|
||||
std::string retbuf;
|
||||
retbuf.reserve(value.length() * 3); // at most all characters get replaced with the escape
|
||||
|
||||
char tmp[5] = "";
|
||||
char tmp[4] = "";
|
||||
while (*src) {
|
||||
std::string::value_type c = *src;
|
||||
// Must be unsigned: a signed char sign-extends bytes >= 0x80 when promoted
|
||||
// for isalnum()/snprintf(), which mangles every non-ASCII UTF-8 byte.
|
||||
const unsigned char c = static_cast<unsigned char>(*src);
|
||||
if (c == ' ') {
|
||||
retbuf.append("%20");
|
||||
} else if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
|
||||
retbuf.push_back(c);
|
||||
} else if ((c < 0x80) && (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~')) {
|
||||
// The unreserved set is ASCII-only, so bytes >= 0x80 are always escaped
|
||||
// rather than left to a locale-dependent isalnum().
|
||||
retbuf.push_back(static_cast<char>(c));
|
||||
} else {
|
||||
snprintf(tmp, 4, "%%%02X", c);
|
||||
snprintf(tmp, sizeof(tmp), "%%%02X", c);
|
||||
retbuf.append(tmp);
|
||||
}
|
||||
src++;
|
||||
@@ -545,14 +550,38 @@ std::string remove_newlines( std::string str ) {
|
||||
return str;
|
||||
}
|
||||
|
||||
std::string escape_json_string( std::string input ) {
|
||||
std::string tmp;
|
||||
tmp = regex_replace(input, std::regex("\n"), "\\n");
|
||||
tmp = regex_replace(tmp, std::regex("\b"), "\\b");
|
||||
tmp = regex_replace(tmp, std::regex("\f"), "\\f");
|
||||
tmp = regex_replace(tmp, std::regex("\r"), "\\r");
|
||||
tmp = regex_replace(tmp, std::regex("\t"), "\\t");
|
||||
tmp = regex_replace(tmp, std::regex("\""), "\\\"");
|
||||
tmp = regex_replace(tmp, std::regex("[\\\\]"), "\\\\");
|
||||
return tmp;
|
||||
std::string escape_json_string(std::string input) {
|
||||
// One pass over the input, so each byte produces its escape exactly once.
|
||||
// The previous implementation ran a chain of regex_replace calls with the
|
||||
// backslash pass last, which re-escaped the backslashes the earlier passes
|
||||
// had just introduced: a quote came out as \\" , an escaped backslash
|
||||
// followed by a bare quote, which terminates the JSON string it is sitting
|
||||
// in. It also left control characters other than \b \f \n \r \t unescaped,
|
||||
// which RFC 8259 does not allow.
|
||||
std::string output;
|
||||
output.reserve(input.size());
|
||||
|
||||
for (unsigned char c : input) {
|
||||
switch (c) {
|
||||
case '"': output += "\\\""; break;
|
||||
case '\\': output += "\\\\"; break;
|
||||
case '\b': output += "\\b"; break;
|
||||
case '\f': output += "\\f"; break;
|
||||
case '\n': output += "\\n"; break;
|
||||
case '\r': output += "\\r"; break;
|
||||
case '\t': output += "\\t"; break;
|
||||
default:
|
||||
if (c < 0x20) {
|
||||
// Control characters with no short form have to go out as \u00XX.
|
||||
char buffer[7];
|
||||
snprintf(buffer, sizeof(buffer), "\\u%04x", c);
|
||||
output += buffer;
|
||||
} else {
|
||||
// Bytes >= 0x80 are passed through so UTF-8 stays intact.
|
||||
output += static_cast<char>(c);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
@@ -207,6 +207,66 @@ TEST_CASE("UriEncode") {
|
||||
// Round-trip test: encode then decode should return original
|
||||
std::string original = "hello world!";
|
||||
REQUIRE(UriDecode(UriEncode(original)) == original);
|
||||
|
||||
// Non-ASCII bytes must each be escaped as a single %XX pair. A signed char
|
||||
// sign-extends here and yields "%FF" (truncated from "FFFFFFD0") instead.
|
||||
REQUIRE(UriEncode("\xD0\xB2") == "%D0%B2");
|
||||
REQUIRE(UriEncode("\xFF") == "%FF");
|
||||
REQUIRE(UriEncode("\x80") == "%80");
|
||||
|
||||
// A Cyrillic name (U+0432 U+0445 U+043E U+0434) - the kind of name the
|
||||
// utf8mb4 switch made storable. Written as bytes so this file stays ASCII.
|
||||
REQUIRE(UriEncode("\xD0\xB2\xD1\x85\xD0\xBE\xD0\xB4") == "%D0%B2%D1%85%D0%BE%D0%B4");
|
||||
|
||||
// Mixed ASCII and UTF-8, and a multi-byte round trip.
|
||||
REQUIRE(UriEncode("Cam \xC3\xA9") == "Cam%20%C3%A9");
|
||||
REQUIRE(UriDecode(UriEncode("\xD0\xB2\xD1\x85")) == "\xD0\xB2\xD1\x85");
|
||||
}
|
||||
|
||||
TEST_CASE("escape_json_string") {
|
||||
// Nothing to do.
|
||||
REQUIRE(escape_json_string("") == "");
|
||||
REQUIRE(escape_json_string("Front Door") == "Front Door");
|
||||
|
||||
// A quote must come out as one backslash then a quote. The previous
|
||||
// implementation escaped quotes before backslashes, so it emitted \\" here:
|
||||
// an escaped backslash followed by a bare quote, which closed the JSON
|
||||
// string it was embedded in.
|
||||
REQUIRE(escape_json_string("\"") == "\\\"");
|
||||
REQUIRE(escape_json_string("Front \"Door\"") == "Front \\\"Door\\\"");
|
||||
|
||||
// A backslash doubles, and that doubling must not itself be re-doubled.
|
||||
REQUIRE(escape_json_string("\\") == "\\\\");
|
||||
REQUIRE(escape_json_string("back\\slash") == "back\\\\slash");
|
||||
|
||||
// A backslash adjacent to a quote is where ordering errors surface.
|
||||
REQUIRE(escape_json_string("\\\"") == "\\\\\\\"");
|
||||
|
||||
// Named control character escapes.
|
||||
REQUIRE(escape_json_string("\n") == "\\n");
|
||||
REQUIRE(escape_json_string("\r") == "\\r");
|
||||
REQUIRE(escape_json_string("\t") == "\\t");
|
||||
REQUIRE(escape_json_string("\b") == "\\b");
|
||||
REQUIRE(escape_json_string("\f") == "\\f");
|
||||
REQUIRE(escape_json_string("line\nbreak") == "line\\nbreak");
|
||||
|
||||
// Control characters without a short form must still be escaped.
|
||||
REQUIRE(escape_json_string(std::string(1, '\a')) == "\\u0007");
|
||||
REQUIRE(escape_json_string(std::string(1, '\x01')) == "\\u0001");
|
||||
REQUIRE(escape_json_string(std::string(1, '\x1f')) == "\\u001f");
|
||||
|
||||
// An embedded NUL is a control character like any other and must survive.
|
||||
REQUIRE(escape_json_string(std::string("a\0b", 3)) == "a\\u0000b");
|
||||
|
||||
// 0x7f is not a JSON control character, and UTF-8 passes through intact.
|
||||
REQUIRE(escape_json_string(std::string(1, '\x7f')) == std::string(1, '\x7f'));
|
||||
REQUIRE(escape_json_string("\xD0\xB2\xD1\x85") == "\xD0\xB2\xD1\x85");
|
||||
|
||||
// Escaping twice is not the same as escaping once. This is the double
|
||||
// escape the go2rtc credential path was hitting.
|
||||
const std::string once = escape_json_string("pa\\ss");
|
||||
REQUIRE(once == "pa\\\\ss");
|
||||
REQUIRE(escape_json_string(once) == "pa\\\\\\\\ss");
|
||||
}
|
||||
|
||||
TEST_CASE("QueryString") {
|
||||
|
||||
Reference in new issue
Block a user