mirror of
https://github.com/ZoneMinder/zoneminder.git
synced 2026-10-02 07:25:02 -04:00
With ZM_AUTH_HASH_IPS on, the client address bound into the auth hash was taken from the left-most X-Forwarded-For value whenever the header was present, both when PHP generated the hash (getRemoteAddr()) and when PHP or zms validated it (getAuthUser(), zmLoadAuthUser()). The header is client controlled, so anyone holding a leaked hash could replay it from anywhere by sending the address it was bound to. Add ZM_AUTH_TRUSTED_PROXIES, a list of exact reverse proxy addresses. X-Forwarded-For is now used only when REMOTE_ADDR is one of them, and is read from the right, skipping hops that are themselves listed proxies, so values a client prepends are never chosen. With the option empty, the default, the header is ignored and REMOTE_ADDR is used. PHP (web/includes/Network.php getRemoteAddr()) and C++ (ClientAddress() in zm_utils, used by zmLoadAuthUser()) implement the same rule so generation and validation continue to agree. Every PHP caller already routes through getRemoteAddr(), so session.php and auth.php need no change. Reverse proxy users who enable ZM_AUTH_HASH_IPS must list their proxy in the new option; until they do, hashes bind to the proxy address, which still validates but no longer distinguishes clients. This is the behaviour change that the #4921 work avoided by trusting the header. The option is added through ConfigData only, like other recent options; zmupdate.pl --freshen inserts it, zms falls back to the compiled-in default and PHP treats an undefined constant as empty, so no schema migration is needed. Tests: ClientAddress Catch2 case; tests/php/test_remote_addr.php updated for the trusted-proxy rule. refs GHSA-72rf-54rm-798c Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit acea5889dec0826f595cb736147a5fdc9c94a2a9)
495 lines
17 KiB
C++
495 lines
17 KiB
C++
/*
|
|
* 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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "zm_catch2.h"
|
|
|
|
#include "zm_utils.h"
|
|
#include <sstream>
|
|
|
|
TEST_CASE("Trim") {
|
|
REQUIRE(Trim("", "") == "");
|
|
REQUIRE(Trim("test", "") == "test");
|
|
REQUIRE(Trim(" ", "") == " ");
|
|
|
|
REQUIRE(Trim("\"test", "\"") == "test");
|
|
REQUIRE(Trim("test\"", "\"") == "test");
|
|
REQUIRE(Trim("\"test\"", "\"") == "test");
|
|
|
|
REQUIRE(Trim("te\"st", "\"") == "te\"st");
|
|
REQUIRE(Trim("\"te\"st\"", "\"") == "te\"st");
|
|
}
|
|
|
|
TEST_CASE("TrimSpaces") {
|
|
REQUIRE(TrimSpaces(" ") == "");
|
|
|
|
REQUIRE(TrimSpaces("test") == "test");
|
|
REQUIRE(TrimSpaces(" test ") == "test");
|
|
REQUIRE(TrimSpaces(" test ") == "test");
|
|
REQUIRE(TrimSpaces(" test ") == "test");
|
|
REQUIRE(TrimSpaces(" test") == "test");
|
|
REQUIRE(TrimSpaces("\ttest") == "test");
|
|
REQUIRE(TrimSpaces("test\t") == "test");
|
|
REQUIRE(TrimSpaces("\ttest\t") == "test");
|
|
REQUIRE(TrimSpaces(" test\t") == "test");
|
|
REQUIRE(TrimSpaces("\ttest ") == "test");
|
|
REQUIRE(TrimSpaces("\t test \t") == "test");
|
|
|
|
REQUIRE(TrimSpaces("\t te st \t") == "te st");
|
|
}
|
|
|
|
TEST_CASE("ReplaceAll") {
|
|
REQUIRE(ReplaceAll("", "", "") == "");
|
|
|
|
REQUIRE(ReplaceAll("a", "", "b") == "a");
|
|
REQUIRE(ReplaceAll("a", "a", "b") == "b");
|
|
REQUIRE(ReplaceAll("a", "b", "c") == "a");
|
|
|
|
REQUIRE(ReplaceAll("aa", "a", "b") == "bb");
|
|
REQUIRE(ReplaceAll("aba", "a", "c") == "cbc");
|
|
|
|
REQUIRE(ReplaceAll("aTOKENa", "TOKEN", "VAL") == "aVALa");
|
|
REQUIRE(ReplaceAll("aTOKENaTOKEN", "TOKEN", "VAL") == "aVALaVAL");
|
|
}
|
|
|
|
TEST_CASE("StartsWith") {
|
|
REQUIRE(StartsWith("", "") == true);
|
|
|
|
REQUIRE(StartsWith("test", "test") == true);
|
|
REQUIRE(StartsWith("test=abc", "test") == true);
|
|
REQUIRE(StartsWith(" test=abc", "test") == false);
|
|
}
|
|
|
|
TEST_CASE("Split (char delimiter)") {
|
|
std::vector<std::string> items = Split("", ' ');
|
|
REQUIRE(items == std::vector<std::string>{""});
|
|
|
|
items = Split("abc def ghi", ' ');
|
|
REQUIRE(items == std::vector<std::string>{"abc", "def", "ghi"});
|
|
|
|
items = Split("abc,def,,ghi", ',');
|
|
REQUIRE(items == std::vector<std::string>{"abc", "def", "", "ghi"});
|
|
}
|
|
|
|
TEST_CASE("Split (string delimiter)") {
|
|
std::vector<std::string> items;
|
|
|
|
items = Split("", "");
|
|
REQUIRE(items == std::vector<std::string>{""});
|
|
|
|
items = Split("", " ");
|
|
REQUIRE(items == std::vector<std::string>{""});
|
|
|
|
items = Split("", " \t");
|
|
REQUIRE(items == std::vector<std::string>{""});
|
|
|
|
items = Split("", " \t");
|
|
REQUIRE(items == std::vector<std::string>{""});
|
|
|
|
items = Split(" ", " ");
|
|
REQUIRE(items.size() == 0);
|
|
|
|
items = Split(" ", " ");
|
|
REQUIRE(items.size() == 0);
|
|
|
|
items = Split(" ", " \t");
|
|
REQUIRE(items.size() == 0);
|
|
|
|
items = Split("a b", "");
|
|
REQUIRE(items == std::vector<std::string>{"a b"});
|
|
|
|
items = Split("a b", " ");
|
|
REQUIRE(items == std::vector<std::string>{"a", "b"});
|
|
|
|
items = Split("a \tb", " \t");
|
|
REQUIRE(items == std::vector<std::string>{"a", "b"});
|
|
|
|
items = Split(" a \tb ", " \t");
|
|
REQUIRE(items == std::vector<std::string>{"a", "b"});
|
|
|
|
items = Split(" a=b ", "=");
|
|
REQUIRE(items == std::vector<std::string>{" a", "b "});
|
|
|
|
items = Split(" a=b ", " =");
|
|
REQUIRE(items == std::vector<std::string>{"a", "b"});
|
|
|
|
items = Split("a b c", " ", 2);
|
|
REQUIRE(items == std::vector<std::string>{"a", "b c"});
|
|
|
|
// LibvlcCamera splits the monitor Options field this way. The field is a
|
|
// textarea, so an option may be on its own line rather than after a comma,
|
|
// and runs of separators (a blank line, or crlf) must not become entries.
|
|
items = Split("--rate=1\n--no-audio", kOptionSeparators);
|
|
REQUIRE(items == std::vector<std::string>{"--rate=1", "--no-audio"});
|
|
|
|
items = Split("--rate=1,--no-audio", kOptionSeparators);
|
|
REQUIRE(items == std::vector<std::string>{"--rate=1", "--no-audio"});
|
|
|
|
items = Split("--rate=1\r\n\r\n--no-audio\n", kOptionSeparators);
|
|
REQUIRE(items == std::vector<std::string>{"--rate=1", "--no-audio"});
|
|
}
|
|
|
|
TEST_CASE("Join") {
|
|
REQUIRE(Join({}, "") == "");
|
|
REQUIRE(Join({}, " ") == "");
|
|
REQUIRE(Join({""}, "") == "");
|
|
REQUIRE(Join({"a"}, "") == "a");
|
|
REQUIRE(Join({"a"}, ",") == "a");
|
|
REQUIRE(Join({"a", "b"}, ",") == "a,b");
|
|
REQUIRE(Join({"a", "b"}, "") == "ab");
|
|
}
|
|
|
|
TEST_CASE("ByteArrayToHexString") {
|
|
std::vector<uint8> bytes;
|
|
|
|
REQUIRE(ByteArrayToHexString(bytes) == "");
|
|
|
|
bytes = {0x00};
|
|
REQUIRE(ByteArrayToHexString(bytes) == "00");
|
|
|
|
bytes = {0x00, 0x01, 0x02, 0xff};
|
|
REQUIRE(ByteArrayToHexString(bytes) == "000102ff");
|
|
}
|
|
|
|
TEST_CASE("Base64Encode") {
|
|
REQUIRE(Base64Encode("") == "");
|
|
REQUIRE(Base64Encode("f") == "Zg==");
|
|
REQUIRE(Base64Encode("fo") == "Zm8=");
|
|
REQUIRE(Base64Encode("foo") == "Zm9v");
|
|
REQUIRE(Base64Encode("foob") == "Zm9vYg==");
|
|
REQUIRE(Base64Encode("fooba") == "Zm9vYmE=");
|
|
REQUIRE(Base64Encode("foobar") == "Zm9vYmFy");
|
|
}
|
|
|
|
TEST_CASE("ZM::clamp") {
|
|
REQUIRE(zm::clamp(1, 0, 2) == 1);
|
|
REQUIRE(zm::clamp(3, 0, 2) == 2);
|
|
REQUIRE(zm::clamp(-1, 0, 2) == 0);
|
|
}
|
|
|
|
TEST_CASE("UriDecode") {
|
|
REQUIRE(UriDecode("abcABC123-_.~%21%28%29%26%3d%20") == "abcABC123-_.~!()&= ");
|
|
REQUIRE(UriDecode("abcABC123-_.~%21%28%29%26%3d+") == "abcABC123-_.~!()&= ");
|
|
}
|
|
|
|
TEST_CASE("UriEncode") {
|
|
// Basic alphanumeric and safe characters should not be encoded
|
|
REQUIRE(UriEncode("abcABC123") == "abcABC123");
|
|
REQUIRE(UriEncode("-_.~") == "-_.~");
|
|
|
|
// Space should be encoded as %20
|
|
REQUIRE(UriEncode(" ") == "%20");
|
|
REQUIRE(UriEncode("hello world") == "hello%20world");
|
|
REQUIRE(UriEncode("a b c") == "a%20b%20c");
|
|
|
|
// Special characters should be percent-encoded
|
|
REQUIRE(UriEncode("!") == "%21");
|
|
REQUIRE(UriEncode("&") == "%26");
|
|
REQUIRE(UriEncode("=") == "%3D");
|
|
REQUIRE(UriEncode("?") == "%3F");
|
|
|
|
// Empty string
|
|
REQUIRE(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") {
|
|
SECTION("no value") {
|
|
std::stringstream str("name1=");
|
|
QueryString qs(str);
|
|
|
|
REQUIRE(qs.size() == 1);
|
|
REQUIRE(qs.has("name1") == true);
|
|
|
|
const QueryParameter *p = qs.get("name1");
|
|
REQUIRE(p != nullptr);
|
|
REQUIRE(p->name() == "name1");
|
|
REQUIRE(p->size() == 0);
|
|
}
|
|
|
|
SECTION("no value and ampersand") {
|
|
std::stringstream str("name1=&");
|
|
QueryString qs(str);
|
|
|
|
REQUIRE(qs.size() == 1);
|
|
REQUIRE(qs.has("name1") == true);
|
|
|
|
const QueryParameter *p = qs.get("name1");
|
|
REQUIRE(p != nullptr);
|
|
REQUIRE(p->name() == "name1");
|
|
REQUIRE(p->size() == 0);
|
|
}
|
|
|
|
SECTION("one parameter, one value") {
|
|
std::stringstream str("name1=value1");
|
|
QueryString qs(str);
|
|
|
|
REQUIRE(qs.size() == 1);
|
|
REQUIRE(qs.has("name1") == true);
|
|
|
|
const QueryParameter *p = qs.get("name1");
|
|
REQUIRE(p != nullptr);
|
|
REQUIRE(p->name() == "name1");
|
|
REQUIRE(p->size() == 1);
|
|
REQUIRE(p->values()[0] == "value1");
|
|
}
|
|
|
|
SECTION("one parameter, multiple values") {
|
|
std::stringstream str("name1=value1&name1=value2");
|
|
QueryString qs(str);
|
|
|
|
REQUIRE(qs.size() == 1);
|
|
REQUIRE(qs.has("name1") == true);
|
|
|
|
const QueryParameter *p = qs.get("name1");
|
|
REQUIRE(p != nullptr);
|
|
REQUIRE(p->name() == "name1");
|
|
REQUIRE(p->size() == 2);
|
|
REQUIRE(p->values()[0] == "value1");
|
|
REQUIRE(p->values()[1] == "value2");
|
|
}
|
|
|
|
SECTION("multiple parameters, multiple values") {
|
|
std::stringstream str("name1=value1&name2=value2");
|
|
QueryString qs(str);
|
|
|
|
REQUIRE(qs.size() == 2);
|
|
REQUIRE(qs.has("name1") == true);
|
|
REQUIRE(qs.has("name2") == true);
|
|
|
|
const QueryParameter *p1 = qs.get("name1");
|
|
REQUIRE(p1 != nullptr);
|
|
REQUIRE(p1->name() == "name1");
|
|
REQUIRE(p1->size() == 1);
|
|
REQUIRE(p1->values()[0] == "value1");
|
|
|
|
const QueryParameter *p2 = qs.get("name2");
|
|
REQUIRE(p2 != nullptr);
|
|
REQUIRE(p2->name() == "name2");
|
|
REQUIRE(p2->size() == 1);
|
|
REQUIRE(p2->values()[0] == "value2");
|
|
}
|
|
}
|
|
|
|
TEST_CASE("mask_authentication") {
|
|
SECTION("no authentication") {
|
|
std::string url("http://192.168.1.1");
|
|
std::string result = mask_authentication(url);
|
|
REQUIRE(url == result);
|
|
}
|
|
SECTION("has username no password has scheme") {
|
|
std::string url("http://username@192.168.1.1");
|
|
std::string result = mask_authentication(url);
|
|
REQUIRE(result == "http://********@192.168.1.1");
|
|
}
|
|
SECTION("has username no password no scheme") {
|
|
std::string url("username@192.168.1.1");
|
|
std::string result = mask_authentication(url);
|
|
REQUIRE(result == "********@192.168.1.1");
|
|
}
|
|
SECTION("has username has password no scheme") {
|
|
std::string url("username:password@192.168.1.1");
|
|
std::string result = mask_authentication(url);
|
|
REQUIRE(result == "********:********@192.168.1.1");
|
|
}
|
|
SECTION("has username has password has scheme") {
|
|
std::string url("http://username:password@192.168.1.1");
|
|
std::string result = mask_authentication(url);
|
|
REQUIRE(result == "http://********:********@192.168.1.1");
|
|
}
|
|
}
|
|
|
|
TEST_CASE("remove_authentication") {
|
|
SECTION("no authentication") {
|
|
std::string url("http://192.168.1.1");
|
|
std::string result = remove_authentication(url);
|
|
REQUIRE(url == result);
|
|
}
|
|
SECTION("has username no password has scheme") {
|
|
std::string url("http://username@192.168.1.1");
|
|
std::string result = remove_authentication(url);
|
|
REQUIRE(result == "http://192.168.1.1");
|
|
}
|
|
SECTION("has username no password no scheme") {
|
|
std::string url("username@192.168.1.1");
|
|
std::string result = remove_authentication(url);
|
|
REQUIRE(result == "192.168.1.1");
|
|
}
|
|
SECTION("has username has password no scheme") {
|
|
std::string url("username:password@192.168.1.1");
|
|
std::string result = remove_authentication(url);
|
|
REQUIRE(result == "192.168.1.1");
|
|
}
|
|
SECTION("has username has password has scheme") {
|
|
std::string url("http://username:password@192.168.1.1");
|
|
std::string result = remove_authentication(url);
|
|
REQUIRE(result == "http://192.168.1.1");
|
|
}
|
|
}
|
|
|
|
TEST_CASE("zm_strncpy") {
|
|
char buf[8];
|
|
|
|
SECTION("fits, null-terminated") {
|
|
zm_strncpy(buf, "abc", sizeof(buf));
|
|
REQUIRE(std::string(buf) == "abc");
|
|
}
|
|
SECTION("oversize truncated to size-1 and terminated") {
|
|
zm_strncpy(buf, "abcdefghijkl", sizeof(buf));
|
|
REQUIRE(std::string(buf) == "abcdefg"); // 7 chars + '\0'
|
|
REQUIRE(buf[7] == '\0');
|
|
}
|
|
SECTION("delimiter length n limits copy") {
|
|
zm_strncpy(buf, "abcdef", sizeof(buf), 3); // src not null-terminated at field end
|
|
REQUIRE(std::string(buf) == "abc");
|
|
}
|
|
SECTION("delimiter length also capped to buffer") {
|
|
zm_strncpy(buf, "abcdefghij", sizeof(buf), 20);
|
|
REQUIRE(std::string(buf) == "abcdefg");
|
|
REQUIRE(buf[7] == '\0');
|
|
}
|
|
SECTION("empty source") {
|
|
zm_strncpy(buf, "", sizeof(buf));
|
|
REQUIRE(buf[0] == '\0');
|
|
}
|
|
}
|
|
|
|
TEST_CASE("TimevalToString") {
|
|
// tv_usec used to go to the format string as %ld. Under glibc's 64 bit time
|
|
// ABI on a 32 bit platform (Debian trixie on armhf) tv_usec is
|
|
// __suseconds64_t while long is still 32 bits, so %ld consumed half the
|
|
// argument and every vararg after it was read from the wrong offset. zmc
|
|
// crashed on armhf because this result is written into a buffer and then
|
|
// used. See #4580 and Debian bug 1120447.
|
|
//
|
|
// amd64 cannot reproduce the mismatch (long is already 64 bits), so what
|
|
// this pins is the rendering: six zero padded digits of microseconds, which
|
|
// is what a wrong conversion would disturb.
|
|
const char *tz = getenv("TZ");
|
|
std::string saved_tz = tz ? tz : "";
|
|
setenv("TZ", "UTC", 1);
|
|
tzset();
|
|
|
|
SECTION("pads microseconds to six digits") {
|
|
REQUIRE(TimevalToString(timeval{0, 0}) == "1970-01-01 00:00:00.000000");
|
|
REQUIRE(TimevalToString(timeval{0, 1}) == "1970-01-01 00:00:00.000001");
|
|
REQUIRE(TimevalToString(timeval{0, 999999}) == "1970-01-01 00:00:00.999999");
|
|
}
|
|
|
|
SECTION("does not truncate or reorder the arguments") {
|
|
// A conversion that mis-reads tv_usec tends to show up either as a wrong
|
|
// number here or as the seconds field being wrong, since the bad read
|
|
// shifts everything after it.
|
|
REQUIRE(TimevalToString(timeval{1700000000, 123456}) == "2023-11-14 22:13:20.123456");
|
|
}
|
|
|
|
if (saved_tz.empty()) {
|
|
unsetenv("TZ");
|
|
} else {
|
|
setenv("TZ", saved_tz.c_str(), 1);
|
|
}
|
|
tzset();
|
|
}
|
|
|
|
TEST_CASE("ClientAddress") {
|
|
SECTION("X-Forwarded-For is ignored unless the peer is a trusted proxy") {
|
|
REQUIRE(ClientAddress("203.0.113.9", "192.168.1.55", "") == "203.0.113.9");
|
|
REQUIRE(ClientAddress("203.0.113.9", "192.168.1.55", "10.0.0.1") == "203.0.113.9");
|
|
REQUIRE(ClientAddress("", "192.168.1.55", "").empty());
|
|
}
|
|
|
|
SECTION("a trusted proxy's X-Forwarded-For is used") {
|
|
REQUIRE(ClientAddress("10.0.0.1", "192.168.1.55", "10.0.0.1") == "192.168.1.55");
|
|
REQUIRE(ClientAddress("10.0.0.1", "", "10.0.0.1") == "10.0.0.1");
|
|
REQUIRE(ClientAddress("10.0.0.1", " , ", "10.0.0.1") == "10.0.0.1");
|
|
}
|
|
|
|
SECTION("the right-most untrusted hop wins, not a spoofed left-most one") {
|
|
REQUIRE(ClientAddress("10.0.0.1", "1.2.3.4, 192.168.1.55", "10.0.0.1") == "192.168.1.55");
|
|
REQUIRE(ClientAddress("10.0.0.1", "1.2.3.4, 192.168.1.55 , 10.0.0.9 ", "10.0.0.1, 10.0.0.9")
|
|
== "192.168.1.55");
|
|
REQUIRE(ClientAddress("10.0.0.1", "1.2.3.4, 192.168.1.55, 10.0.0.9", "10.0.0.1 10.0.0.9")
|
|
== "192.168.1.55");
|
|
}
|
|
|
|
SECTION("when every hop is trusted the left-most is the origin") {
|
|
REQUIRE(ClientAddress("10.0.0.1", "10.0.0.9, 10.0.0.1", "10.0.0.1,10.0.0.9") == "10.0.0.9");
|
|
}
|
|
}
|