| /* |
| Licensed to the Apache Software Foundation (ASF) under one |
| or more contributor license agreements. See the NOTICE file |
| distributed with this work for additional information |
| regarding copyright ownership. The ASF licenses this file |
| to you under the Apache License, Version 2.0 (the |
| "License"); you may not use this file except in compliance |
| with the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| */ |
| |
| /* |
| * These are misc unit tests for header rewrite |
| */ |
| |
| #include <cstdio> |
| #include <cstdarg> |
| #include <cstdlib> |
| #include <iostream> |
| #include <ostream> |
| #include <sys/stat.h> |
| #include <string> |
| #include <utility> |
| |
| #include "parser.h" |
| #include "cidr.h" |
| |
| #if TS_USE_HRW_MAXMINDDB |
| #include <maxminddb.h> |
| #endif |
| |
| namespace header_rewrite_ns |
| { |
| const char PLUGIN_NAME[] = "TEST_header_rewrite"; |
| const char PLUGIN_NAME_DBG[] = "TEST_dbg_header_rewrite"; |
| } // namespace header_rewrite_ns |
| |
| void |
| TSError(const char *fmt, ...) noexcept |
| { |
| va_list args; |
| |
| va_start(args, fmt); |
| vfprintf(stderr, fmt, args); |
| va_end(args); |
| fprintf(stderr, "\n"); |
| } |
| |
| class ParserTest : public Parser |
| { |
| public: |
| ParserTest(const std::string &line) : res(true) |
| { |
| parse_succeeded = Parser::parse_line(line); |
| std::cout << "Finished parser test: " << line << std::endl; |
| } |
| |
| std::vector<std::string> |
| getTokens() const |
| { |
| return _tokens; |
| } |
| |
| template <typename T, typename U> |
| void |
| do_parser_check(T x, U y, int line = 0) |
| { |
| if (x != y) { |
| std::cerr << "CHECK FAILED on line " << line << ": " << x << " != " << y << std::endl; |
| res = false; |
| } |
| } |
| |
| bool res; |
| bool parse_succeeded = false; |
| }; |
| |
| class SimpleTokenizerTest : public HRWSimpleTokenizer |
| { |
| public: |
| SimpleTokenizerTest(const std::string &line) : HRWSimpleTokenizer(line), res(true) |
| { |
| std::cout << "Finished tokenizer test: " << line << std::endl; |
| } |
| |
| template <typename T, typename U> |
| void |
| do_parser_check(T x, U y, int line = 0) |
| { |
| if (x != y) { |
| std::cerr << "CHECK FAILED on line " << line << ": |" << x << "| != |" << y << "|" << std::endl; |
| res = false; |
| } |
| } |
| |
| bool res; |
| }; |
| |
| #define CHECK_EQ(x, y) \ |
| do { \ |
| p.do_parser_check((x), (y), __LINE__); \ |
| } while (false) |
| |
| #define END_TEST(s) \ |
| do { \ |
| if (!p.res) { \ |
| ++errors; \ |
| } \ |
| } while (false) |
| |
| int |
| test_parsing() |
| { |
| int errors = 0; |
| |
| { |
| ParserTest p("cond %{READ_REQUEST_HDR_HOOK}"); |
| |
| CHECK_EQ(p.getTokens().size(), 2U); |
| CHECK_EQ(p.getTokens()[0], "cond"); |
| CHECK_EQ(p.getTokens()[1], "%{READ_REQUEST_HDR_HOOK}"); |
| |
| END_TEST(); |
| } |
| |
| { |
| ParserTest p("cond %{POST_REMAP_HOOK}"); |
| TSHttpHookID hook = TS_HTTP_LAST_HOOK; |
| |
| CHECK_EQ(p.getTokens().size(), 2U); |
| CHECK_EQ(p.getTokens()[0], "cond"); |
| CHECK_EQ(p.getTokens()[1], "%{POST_REMAP_HOOK}"); |
| CHECK_EQ(p.cond_is_hook(hook), true); |
| CHECK_EQ(hook, TS_HTTP_POST_REMAP_HOOK); |
| |
| END_TEST(); |
| } |
| |
| { |
| ParserTest p("cond %{CLIENT-HEADER:Host} =a"); |
| |
| CHECK_EQ(p.getTokens().size(), 4UL); |
| CHECK_EQ(p.getTokens()[0], "cond"); |
| CHECK_EQ(p.getTokens()[1], "%{CLIENT-HEADER:Host}"); |
| CHECK_EQ(p.getTokens()[2], "="); |
| CHECK_EQ(p.getTokens()[3], "a"); |
| |
| END_TEST(); |
| } |
| |
| { |
| ParserTest p(" # COMMENT!"); |
| |
| CHECK_EQ(p.getTokens().size(), 0UL); |
| CHECK_EQ(p.empty(), true); |
| |
| END_TEST(); |
| } |
| |
| { |
| ParserTest p("# COMMENT"); |
| |
| CHECK_EQ(p.getTokens().size(), 0UL); |
| CHECK_EQ(p.empty(), true); |
| |
| END_TEST(); |
| } |
| |
| { |
| ParserTest p("cond %{Client-HEADER:Foo} =b"); |
| |
| CHECK_EQ(p.getTokens().size(), 4UL); |
| CHECK_EQ(p.getTokens()[0], "cond"); |
| CHECK_EQ(p.getTokens()[1], "%{Client-HEADER:Foo}"); |
| CHECK_EQ(p.getTokens()[2], "="); |
| CHECK_EQ(p.getTokens()[3], "b"); |
| |
| END_TEST(); |
| } |
| |
| { |
| ParserTest p("cond %{Client-HEADER:Blah} = x"); |
| |
| CHECK_EQ(p.getTokens().size(), 4UL); |
| CHECK_EQ(p.getTokens()[0], "cond"); |
| CHECK_EQ(p.getTokens()[1], "%{Client-HEADER:Blah}"); |
| CHECK_EQ(p.getTokens()[2], "="); |
| CHECK_EQ(p.getTokens()[3], "x"); |
| |
| END_TEST(); |
| } |
| |
| { |
| ParserTest p(R"(cond %{CLIENT-HEADER:non_existent_header} = "shouldnt_ exist _anyway" [AND])"); |
| |
| CHECK_EQ(p.getTokens().size(), 5UL); |
| CHECK_EQ(p.getTokens()[0], "cond"); |
| CHECK_EQ(p.getTokens()[1], "%{CLIENT-HEADER:non_existent_header}"); |
| CHECK_EQ(p.getTokens()[2], "="); |
| CHECK_EQ(p.getTokens()[3], "shouldnt_ exist _anyway"); |
| CHECK_EQ(p.getTokens()[4], "[AND]"); |
| |
| END_TEST(); |
| } |
| |
| { |
| ParserTest p(R"(cond %{CLIENT-HEADER:non_existent_header} = "shouldnt_ = _anyway" [AND])"); |
| |
| CHECK_EQ(p.getTokens().size(), 5UL); |
| CHECK_EQ(p.getTokens()[0], "cond"); |
| CHECK_EQ(p.getTokens()[1], "%{CLIENT-HEADER:non_existent_header}"); |
| CHECK_EQ(p.getTokens()[2], "="); |
| CHECK_EQ(p.getTokens()[3], "shouldnt_ = _anyway"); |
| CHECK_EQ(p.getTokens()[4], "[AND]"); |
| |
| END_TEST(); |
| } |
| |
| { |
| ParserTest p(R"(cond %{CLIENT-HEADER:non_existent_header} ="=" [AND])"); |
| |
| CHECK_EQ(p.getTokens().size(), 5UL); |
| CHECK_EQ(p.getTokens()[0], "cond"); |
| CHECK_EQ(p.getTokens()[1], "%{CLIENT-HEADER:non_existent_header}"); |
| CHECK_EQ(p.getTokens()[2], "="); |
| CHECK_EQ(p.getTokens()[3], "="); |
| CHECK_EQ(p.getTokens()[4], "[AND]"); |
| |
| END_TEST(); |
| } |
| |
| { |
| ParserTest p(R"(cond %{CLIENT-HEADER:non_existent_header} ="" [AND])"); |
| |
| CHECK_EQ(p.getTokens().size(), 5UL); |
| CHECK_EQ(p.getTokens()[0], "cond"); |
| CHECK_EQ(p.getTokens()[1], "%{CLIENT-HEADER:non_existent_header}"); |
| CHECK_EQ(p.getTokens()[2], "="); |
| CHECK_EQ(p.getTokens()[3], ""); |
| CHECK_EQ(p.getTokens()[4], "[AND]"); |
| |
| END_TEST(); |
| } |
| |
| { |
| ParserTest p(R"(cond %{CLIENT-URL:PATH} /\/foo\/bar/ [OR])"); |
| |
| CHECK_EQ(p.getTokens().size(), 4UL); |
| CHECK_EQ(p.getTokens()[0], "cond"); |
| CHECK_EQ(p.getTokens()[1], "%{CLIENT-URL:PATH}"); |
| CHECK_EQ(p.getTokens()[2], R"(/\/foo\/bar/)"); |
| CHECK_EQ(p.getTokens()[3], "[OR]"); |
| |
| END_TEST(); |
| } |
| |
| { |
| ParserTest p(R"(cond %{INBOUND:REMOTE-ADDR} {192.168.201.0/24,10.0.0.0/8})"); |
| |
| CHECK_EQ(p.getTokens().size(), 3UL); |
| CHECK_EQ(p.getTokens()[0], "cond"); |
| CHECK_EQ(p.getTokens()[1], "%{INBOUND:REMOTE-ADDR}"); |
| CHECK_EQ(p.getTokens()[2], "{192.168.201.0/24,10.0.0.0/8}"); |
| |
| END_TEST(); |
| } |
| |
| { |
| ParserTest p(R"(cond %{INBOUND:REMOTE-ADDR} { 192.168.201.0/24,10.0.0.0/8 })"); |
| |
| CHECK_EQ(p.getTokens().size(), 3UL); |
| CHECK_EQ(p.getTokens()[0], "cond"); |
| CHECK_EQ(p.getTokens()[1], "%{INBOUND:REMOTE-ADDR}"); |
| CHECK_EQ(p.getTokens()[2], "{ 192.168.201.0/24,10.0.0.0/8 }"); |
| |
| END_TEST(); |
| } |
| |
| { |
| ParserTest p("add-header X-HeaderRewriteApplied true"); |
| |
| CHECK_EQ(p.getTokens().size(), 3UL); |
| CHECK_EQ(p.getTokens()[0], "add-header"); |
| CHECK_EQ(p.getTokens()[1], "X-HeaderRewriteApplied"); |
| CHECK_EQ(p.getTokens()[2], "true"); |
| |
| END_TEST(); |
| } |
| |
| /* backslash-escape */ |
| { |
| ParserTest p(R"(add-header foo \ \=\<\>\"\#\\)"); |
| |
| CHECK_EQ(p.getTokens().size(), 3UL); |
| CHECK_EQ(p.getTokens()[0], "add-header"); |
| CHECK_EQ(p.getTokens()[1], "foo"); |
| CHECK_EQ(p.getTokens()[2], R"( =<>"#\)"); |
| |
| END_TEST(); |
| } |
| |
| { |
| ParserTest p(R"(add-header foo \<bar\>)"); |
| |
| CHECK_EQ(p.getTokens().size(), 3UL); |
| CHECK_EQ(p.getTokens()[0], "add-header"); |
| CHECK_EQ(p.getTokens()[1], "foo"); |
| CHECK_EQ(p.getTokens()[2], "<bar>"); |
| |
| END_TEST(); |
| } |
| |
| { |
| ParserTest p(R"(add-header foo \bar\)"); |
| |
| CHECK_EQ(p.getTokens().size(), 3UL); |
| CHECK_EQ(p.getTokens()[0], "add-header"); |
| CHECK_EQ(p.getTokens()[1], "foo"); |
| CHECK_EQ(p.getTokens()[2], "bar"); |
| |
| END_TEST(); |
| } |
| |
| { |
| ParserTest p(R"(add-header foo "bar")"); |
| |
| CHECK_EQ(p.getTokens().size(), 3UL); |
| CHECK_EQ(p.getTokens()[0], "add-header"); |
| CHECK_EQ(p.getTokens()[1], "foo"); |
| CHECK_EQ(p.getTokens()[2], "bar"); |
| |
| END_TEST(); |
| } |
| |
| { |
| ParserTest p(R"(add-header foo "\"bar\"")"); |
| |
| CHECK_EQ(p.getTokens().size(), 3UL); |
| CHECK_EQ(p.getTokens()[0], "add-header"); |
| CHECK_EQ(p.getTokens()[1], "foo"); |
| CHECK_EQ(p.getTokens()[2], R"("bar")"); |
| |
| END_TEST(); |
| } |
| |
| { |
| ParserTest p(R"(add-header foo "\"\\\"bar\\\"\"")"); |
| |
| CHECK_EQ(p.getTokens().size(), 3UL); |
| CHECK_EQ(p.getTokens()[0], "add-header"); |
| CHECK_EQ(p.getTokens()[1], "foo"); |
| CHECK_EQ(p.getTokens()[2], R"("\"bar\"")"); |
| |
| END_TEST(); |
| } |
| |
| { |
| ParserTest p(R"(add-header Public-Key-Pins "max-age=3000; pin-sha256=\"d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=\"")"); |
| |
| CHECK_EQ(p.getTokens().size(), 3UL); |
| CHECK_EQ(p.getTokens()[0], "add-header"); |
| CHECK_EQ(p.getTokens()[1], "Public-Key-Pins"); |
| CHECK_EQ(p.getTokens()[2], R"(max-age=3000; pin-sha256="d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=")"); |
| |
| END_TEST(); |
| } |
| |
| { |
| ParserTest p(R"(add-header Public-Key-Pins max-age\=3000;\ pin-sha256\=\"d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM\=\")"); |
| |
| CHECK_EQ(p.getTokens().size(), 3UL); |
| CHECK_EQ(p.getTokens()[0], "add-header"); |
| CHECK_EQ(p.getTokens()[1], "Public-Key-Pins"); |
| CHECK_EQ(p.getTokens()[2], R"(max-age=3000; pin-sha256="d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=")"); |
| |
| END_TEST(); |
| } |
| |
| { |
| ParserTest p(R"(add-header X-Url "http://trafficserver.apache.org/")"); |
| |
| CHECK_EQ(p.getTokens().size(), 3UL); |
| CHECK_EQ(p.getTokens()[0], "add-header"); |
| CHECK_EQ(p.getTokens()[1], "X-Url"); |
| CHECK_EQ(p.getTokens()[2], "http://trafficserver.apache.org/"); |
| |
| END_TEST(); |
| } |
| |
| { |
| ParserTest p(R"(add-header X-Url http://trafficserver.apache.org/)"); |
| |
| CHECK_EQ(p.getTokens().size(), 3UL); |
| CHECK_EQ(p.getTokens()[0], "add-header"); |
| CHECK_EQ(p.getTokens()[1], "X-Url"); |
| CHECK_EQ(p.getTokens()[2], "http://trafficserver.apache.org/"); |
| |
| END_TEST(); |
| } |
| |
| { |
| ParserTest p(R"(set-header Alt-Svc "quic=\":443\"; v=\"35\"" [L])"); |
| |
| CHECK_EQ(p.getTokens().size(), 4UL); |
| CHECK_EQ(p.getTokens()[0], "set-header"); |
| CHECK_EQ(p.getTokens()[1], "Alt-Svc"); |
| CHECK_EQ(p.getTokens()[2], R"(quic=":443"; v="35")"); |
| CHECK_EQ(p.get_value(), R"(quic=":443"; v="35")"); |
| |
| END_TEST(); |
| } |
| |
| /* |
| * test some failure scenarios |
| */ |
| |
| { /* unterminated quote */ |
| ParserTest p(R"(cond %{CLIENT-HEADER:non_existent_header} =" [AND])"); |
| |
| CHECK_EQ(p.getTokens().size(), 0UL); |
| |
| END_TEST(); |
| } |
| |
| { /* quote in a token */ |
| ParserTest p(R"(cond %{CLIENT-HEADER:non_existent_header} =a"b [AND])"); |
| |
| CHECK_EQ(p.getTokens().size(), 0UL); |
| |
| END_TEST(); |
| } |
| |
| { /* modifiers with no condition or operator to attach them to */ |
| ParserTest p("[L]"); |
| |
| CHECK_EQ(p.parse_succeeded, false); |
| |
| END_TEST(); |
| } |
| |
| { /* same, but long enough that the token is heap allocated rather than SSO */ |
| ParserTest p("[AND,NOCASE,NOT,L,QSA,I,EXT,PRE]"); |
| |
| CHECK_EQ(p.parse_succeeded, false); |
| |
| END_TEST(); |
| } |
| |
| for (const auto *line : {"cond", "cond [L]", "cond [AND,NOCASE,NOT,L,QSA,I,EXT,PRE]"}) { |
| ParserTest p(line); |
| |
| CHECK_EQ(p.parse_succeeded, false); |
| |
| END_TEST(); |
| } |
| |
| return errors; |
| } |
| |
| int |
| test_processing() |
| { |
| int errors = 0; |
| /* |
| * These tests are designed to verify that the processing of the parsed input is correct. |
| */ |
| { |
| ParserTest p(R"(cond %{CLIENT-HEADER:non_existent_header} ="=" [AND])"); |
| |
| CHECK_EQ(p.getTokens().size(), 5UL); |
| CHECK_EQ(p.get_op(), "CLIENT-HEADER:non_existent_header"); |
| CHECK_EQ(p.get_arg(), "=="); |
| CHECK_EQ(p.is_cond(), true); |
| |
| END_TEST(); |
| } |
| |
| { |
| ParserTest p(R"(cond %{CLIENT-HEADER:non_existent_header} = "shouldnt_ = _anyway" [AND])"); |
| |
| CHECK_EQ(p.getTokens().size(), 5UL); |
| CHECK_EQ(p.get_op(), "CLIENT-HEADER:non_existent_header"); |
| CHECK_EQ(p.get_arg(), "=shouldnt_ = _anyway"); |
| CHECK_EQ(p.is_cond(), true); |
| |
| END_TEST(); |
| } |
| |
| { |
| ParserTest p(R"(cond %{CLIENT-URL:PATH} /\.html|\.txt/)"); |
| |
| CHECK_EQ(p.getTokens().size(), 3UL); |
| CHECK_EQ(p.get_op(), "CLIENT-URL:PATH"); |
| CHECK_EQ(p.get_arg(), R"(/\.html|\.txt/)"); |
| CHECK_EQ(p.is_cond(), true); |
| |
| END_TEST(); |
| } |
| |
| { |
| ParserTest p(R"(cond %{CLIENT-URL:PATH} /\/foo\/bar/)"); |
| |
| CHECK_EQ(p.getTokens().size(), 3UL); |
| CHECK_EQ(p.get_op(), "CLIENT-URL:PATH"); |
| CHECK_EQ(p.get_arg(), R"(/\/foo\/bar/)"); |
| CHECK_EQ(p.is_cond(), true); |
| |
| END_TEST(); |
| } |
| |
| { |
| ParserTest p("add-header X-HeaderRewriteApplied true"); |
| |
| CHECK_EQ(p.getTokens().size(), 3UL); |
| CHECK_EQ(p.get_op(), "add-header"); |
| CHECK_EQ(p.get_arg(), "X-HeaderRewriteApplied"); |
| CHECK_EQ(p.get_value(), "true"); |
| CHECK_EQ(p.is_cond(), false); |
| |
| END_TEST(); |
| } |
| |
| return errors; |
| } |
| |
| int |
| test_tokenizer() |
| { |
| int errors = 0; |
| |
| { |
| SimpleTokenizerTest p("a simple test"); |
| CHECK_EQ(p.get_tokens().size(), 1UL); |
| CHECK_EQ(p.get_tokens()[0], "a simple test"); |
| |
| END_TEST(); |
| } |
| |
| { |
| SimpleTokenizerTest p(R"(quic=":443"; v="35")"); |
| CHECK_EQ(p.get_tokens().size(), 1UL); |
| CHECK_EQ(p.get_tokens()[0], R"(quic=":443"; v="35")"); |
| |
| END_TEST(); |
| } |
| |
| { |
| SimpleTokenizerTest p(R"(let's party like it's %{NOW:YEAR})"); |
| CHECK_EQ(p.get_tokens().size(), 2UL); |
| CHECK_EQ(p.get_tokens()[0], "let's party like it's "); |
| CHECK_EQ(p.get_tokens()[1], "%{NOW:YEAR}"); |
| |
| END_TEST(); |
| } |
| { |
| SimpleTokenizerTest p("A racoon's favorite tag is %{METHOD} in %{NOW:YEAR}!"); |
| CHECK_EQ(p.get_tokens().size(), 5UL); |
| CHECK_EQ(p.get_tokens()[0], "A racoon's favorite tag is "); |
| CHECK_EQ(p.get_tokens()[1], "%{METHOD}"); |
| CHECK_EQ(p.get_tokens()[2], " in "); |
| CHECK_EQ(p.get_tokens()[3], "%{NOW:YEAR}"); |
| CHECK_EQ(p.get_tokens()[4], "!"); |
| |
| END_TEST(); |
| } |
| |
| { |
| SimpleTokenizerTest p(R"(Hello from %{IP:SERVER}:%{INBOUND:LOCAL-PORT})"); |
| CHECK_EQ(p.get_tokens().size(), 4UL); |
| CHECK_EQ(p.get_tokens()[0], "Hello from "); |
| CHECK_EQ(p.get_tokens()[1], "%{IP:SERVER}"); |
| CHECK_EQ(p.get_tokens()[2], ":"); |
| CHECK_EQ(p.get_tokens()[3], "%{INBOUND:LOCAL-PORT}"); |
| |
| END_TEST(); |
| } |
| |
| { |
| SimpleTokenizerTest p("{a}%{PATH}"); |
| CHECK_EQ(p.get_tokens().size(), 2UL); |
| CHECK_EQ(p.get_tokens()[0], "{a}"); |
| CHECK_EQ(p.get_tokens()[1], "%{PATH}"); |
| |
| END_TEST(); |
| } |
| |
| { |
| SimpleTokenizerTest p("<a>%{PATH}"); |
| CHECK_EQ(p.get_tokens().size(), 2UL); |
| CHECK_EQ(p.get_tokens()[0], "<a>"); |
| CHECK_EQ(p.get_tokens()[1], "%{PATH}"); |
| |
| END_TEST(); |
| } |
| |
| return errors; |
| } |
| |
| #if TS_USE_HRW_MAXMINDDB |
| |
| static bool |
| file_exists(const char *path) |
| { |
| struct stat st; |
| return stat(path, &st) == 0; |
| } |
| |
| static int |
| open_test_mmdb(MMDB_s &mmdb, const char *path) |
| { |
| int status = MMDB_open(path, MMDB_MODE_MMAP, &mmdb); |
| if (MMDB_SUCCESS != status) { |
| std::cerr << "Cannot open " << path << ": " << MMDB_strerror(status) << std::endl; |
| return 1; |
| } |
| return 0; |
| } |
| |
| static std::string |
| lookup_country(MMDB_s &mmdb, const char *ip) |
| { |
| int gai_error, mmdb_error; |
| MMDB_lookup_result_s result = MMDB_lookup_string(&mmdb, ip, &gai_error, &mmdb_error); |
| |
| if (gai_error != 0 || MMDB_SUCCESS != mmdb_error || !result.found_entry) { |
| return "(lookup_failed)"; |
| } |
| |
| MMDB_entry_data_s entry_data; |
| |
| // Try nested path first (GeoLite2 style) |
| int status = MMDB_get_value(&result.entry, &entry_data, "country", "iso_code", NULL); |
| if (MMDB_SUCCESS == status && entry_data.has_data && entry_data.type == MMDB_DATA_TYPE_UTF8_STRING) { |
| return std::string(entry_data.utf8_string, entry_data.data_size); |
| } |
| |
| // Try flat path (vendor style) |
| status = MMDB_get_value(&result.entry, &entry_data, "country_code", NULL); |
| if (MMDB_SUCCESS == status && entry_data.has_data && entry_data.type == MMDB_DATA_TYPE_UTF8_STRING) { |
| return std::string(entry_data.utf8_string, entry_data.data_size); |
| } |
| |
| return "(not_found)"; |
| } |
| |
| static uint32_t |
| lookup_asn(MMDB_s &mmdb, const char *ip) |
| { |
| int gai_error, mmdb_error; |
| MMDB_lookup_result_s result = MMDB_lookup_string(&mmdb, ip, &gai_error, &mmdb_error); |
| |
| if (gai_error != 0 || MMDB_SUCCESS != mmdb_error || !result.found_entry) { |
| return 0; |
| } |
| |
| MMDB_entry_data_s entry_data; |
| int status = MMDB_get_value(&result.entry, &entry_data, "autonomous_system_number", NULL); |
| if (MMDB_SUCCESS == status && entry_data.has_data && entry_data.type == MMDB_DATA_TYPE_UINT32) { |
| return entry_data.uint32; |
| } |
| return 0; |
| } |
| |
| // Test that we can read country codes from a flat-schema MMDB (vendor databases |
| // where country_code is a top-level string field). |
| int |
| test_flat_schema() |
| { |
| const char *path = getenv("MMDB_TEST_FLAT"); |
| if (path == nullptr || !file_exists(path)) { |
| std::cout << "SKIP: flat-schema test mmdb not found (set MMDB_TEST_FLAT)" << std::endl; |
| return 0; |
| } |
| |
| std::cout << "Testing flat-schema MMDB: " << path << std::endl; |
| int errors = 0; |
| MMDB_s mmdb; |
| if (open_test_mmdb(mmdb, path) != 0) { |
| return 1; |
| } |
| |
| std::string country = lookup_country(mmdb, "8.8.8.8"); |
| if (country != "US") { |
| std::cerr << "FAIL: flat schema 8.8.8.8 expected 'US', got '" << country << "'" << std::endl; |
| ++errors; |
| } else { |
| std::cout << " PASS: flat 8.8.8.8 country = " << country << std::endl; |
| } |
| |
| country = lookup_country(mmdb, "1.2.3.4"); |
| if (country != "KR") { |
| std::cerr << "FAIL: flat schema 1.2.3.4 expected 'KR', got '" << country << "'" << std::endl; |
| ++errors; |
| } else { |
| std::cout << " PASS: flat 1.2.3.4 country = " << country << std::endl; |
| } |
| |
| uint32_t asn = lookup_asn(mmdb, "8.8.8.8"); |
| if (asn != 15169) { |
| std::cerr << "FAIL: flat schema 8.8.8.8 expected ASN 15169, got " << asn << std::endl; |
| ++errors; |
| } else { |
| std::cout << " PASS: flat 8.8.8.8 ASN = " << asn << std::endl; |
| } |
| |
| // Loopback should not be found |
| country = lookup_country(mmdb, "127.0.0.1"); |
| if (country != "(lookup_failed)") { |
| std::cerr << "FAIL: flat schema 127.0.0.1 expected no entry, got '" << country << "'" << std::endl; |
| ++errors; |
| } else { |
| std::cout << " PASS: flat 127.0.0.1 correctly not found" << std::endl; |
| } |
| |
| MMDB_close(&mmdb); |
| return errors; |
| } |
| |
| // Test that we can read country codes from a nested-schema MMDB (GeoLite2/GeoIP2/DBIP |
| // where country is country -> iso_code). |
| int |
| test_nested_schema() |
| { |
| const char *path = getenv("MMDB_TEST_NESTED"); |
| if (path == nullptr || !file_exists(path)) { |
| std::cout << "SKIP: nested-schema test mmdb not found (set MMDB_TEST_NESTED)" << std::endl; |
| return 0; |
| } |
| |
| std::cout << "Testing nested-schema MMDB: " << path << std::endl; |
| int errors = 0; |
| MMDB_s mmdb; |
| if (open_test_mmdb(mmdb, path) != 0) { |
| return 1; |
| } |
| |
| std::string country = lookup_country(mmdb, "8.8.8.8"); |
| if (country != "US") { |
| std::cerr << "FAIL: nested schema 8.8.8.8 expected 'US', got '" << country << "'" << std::endl; |
| ++errors; |
| } else { |
| std::cout << " PASS: nested 8.8.8.8 country = " << country << std::endl; |
| } |
| |
| country = lookup_country(mmdb, "1.2.3.4"); |
| if (country != "KR") { |
| std::cerr << "FAIL: nested schema 1.2.3.4 expected 'KR', got '" << country << "'" << std::endl; |
| ++errors; |
| } else { |
| std::cout << " PASS: nested 1.2.3.4 country = " << country << std::endl; |
| } |
| |
| uint32_t asn = lookup_asn(mmdb, "8.8.8.8"); |
| if (asn != 15169) { |
| std::cerr << "FAIL: nested schema 8.8.8.8 expected ASN 15169, got " << asn << std::endl; |
| ++errors; |
| } else { |
| std::cout << " PASS: nested 8.8.8.8 ASN = " << asn << std::endl; |
| } |
| |
| country = lookup_country(mmdb, "127.0.0.1"); |
| if (country != "(lookup_failed)") { |
| std::cerr << "FAIL: nested schema 127.0.0.1 expected no entry, got '" << country << "'" << std::endl; |
| ++errors; |
| } else { |
| std::cout << " PASS: nested 127.0.0.1 correctly not found" << std::endl; |
| } |
| |
| MMDB_close(&mmdb); |
| return errors; |
| } |
| |
| int |
| test_maxmind_geo() |
| { |
| int errors = 0; |
| errors += test_flat_schema(); |
| errors += test_nested_schema(); |
| return errors; |
| } |
| #endif |
| |
| static std::string |
| cidr_mask_v6_str(const char *addr_str, int prefix) |
| { |
| in6_addr addr{}; |
| |
| if (inet_pton(AF_INET6, addr_str, &addr) != 1) { |
| return "<invalid address>"; |
| } |
| |
| int zero_bytes = 0; |
| unsigned char mask = 0; |
| |
| cidr_v6_params(prefix, zero_bytes, mask); |
| cidr_apply_v6(addr, zero_bytes, mask); |
| |
| char out[INET6_ADDRSTRLEN]{}; |
| |
| if (inet_ntop(AF_INET6, &addr, out, sizeof(out)) == nullptr) { |
| return "<inet_ntop failed>"; |
| } |
| |
| return std::string(out); |
| } |
| |
| int |
| test_cidr() |
| { |
| int errors = 0; |
| |
| auto check_qualifier = [&errors](std::string_view qualifier, CidrQualifierError expected_error, int expected_v4, |
| int expected_v6) { |
| int v4 = 24; |
| int v6 = 48; |
| |
| auto const error = cidr_parse_qualifier(qualifier, v4, v6); |
| if (error != expected_error || v4 != expected_v4 || v6 != expected_v6) { |
| std::cerr << "FAIL: CIDR qualifier '" << qualifier << "' parsed as error=" << static_cast<int>(error) << ", v4=" << v4 |
| << ", v6=" << v6 << "; expected error=" << static_cast<int>(expected_error) << ", v4=" << expected_v4 |
| << ", v6=" << expected_v6 << std::endl; |
| ++errors; |
| } |
| }; |
| |
| check_qualifier("16", CidrQualifierError::NONE, 16, 48); |
| check_qualifier("18,42", CidrQualifierError::NONE, 18, 42); |
| check_qualifier("18/42", CidrQualifierError::NONE, 18, 42); |
| check_qualifier("18:42", CidrQualifierError::NONE, 18, 42); |
| check_qualifier(",8", CidrQualifierError::NONE, 24, 8); |
| check_qualifier("24,", CidrQualifierError::NONE, 24, 48); |
| check_qualifier("0,0", CidrQualifierError::NONE, 0, 0); |
| check_qualifier("abc", CidrQualifierError::IPV4, 24, 48); |
| check_qualifier("33", CidrQualifierError::IPV4, 24, 48); |
| check_qualifier("24,abc", CidrQualifierError::IPV6, 24, 48); |
| check_qualifier("24,129", CidrQualifierError::IPV6, 24, 48); |
| check_qualifier("24,8,7", CidrQualifierError::IPV6, 24, 48); |
| |
| // IPv4 masks, in network byte order. /0 must be 0 (and must not shift by 32). |
| // Out-of-range prefixes clamp to [0, 32]. |
| const std::pair<int, in_addr_t> v4cases[] = { |
| {-1, 0 }, // clamps to /0 |
| {0, 0 }, |
| {24, htonl(0xFFFFFF00u)}, |
| {32, htonl(0xFFFFFFFFu)}, |
| {33, htonl(0xFFFFFFFFu)}, // clamps to /32 |
| }; |
| |
| for (auto const &[prefix, expect] : v4cases) { |
| in_addr_t const got = cidr_v4_mask(prefix); |
| |
| if (got != expect) { |
| std::cerr << "FAIL: cidr_v4_mask(/" << prefix << ") = " << std::hex << got << ", expected " << expect << std::dec |
| << std::endl; |
| ++errors; |
| } else { |
| std::cout << " PASS: cidr_v4_mask(/" << prefix << ")" << std::endl; |
| } |
| } |
| |
| // IPv6 masks. The source has bits set in every byte so non-byte-aligned |
| // prefixes (the actual regression) produce distinct results. Out-of-range |
| // prefixes clamp to [0, 128]. |
| const char *src = "2001:db8:abcd:ef13:3456:789a:bcde:f012"; |
| const std::pair<int, const char *> v6cases[] = { |
| {129, "2001:db8:abcd:ef13:3456:789a:bcde:f012"}, // clamps to /128 |
| {128, "2001:db8:abcd:ef13:3456:789a:bcde:f012"}, |
| {64, "2001:db8:abcd:ef13::" }, |
| {63, "2001:db8:abcd:ef12::" }, |
| {60, "2001:db8:abcd:ef10::" }, |
| {52, "2001:db8:abcd:e000::" }, |
| {48, "2001:db8:abcd::" }, |
| {0, "::" }, |
| {-1, "::" }, // clamps to /0 |
| }; |
| |
| for (auto const &[prefix, expect] : v6cases) { |
| std::string const got = cidr_mask_v6_str(src, prefix); |
| |
| if (got != expect) { |
| std::cerr << "FAIL: " << src << " /" << prefix << " = " << got << ", expected " << expect << std::endl; |
| ++errors; |
| } else { |
| std::cout << " PASS: " << src << " /" << prefix << " = " << got << std::endl; |
| } |
| } |
| |
| return errors; |
| } |
| |
| int |
| main() |
| { |
| if (test_parsing() || test_processing() || test_tokenizer() || test_cidr()) { |
| return 1; |
| } |
| |
| #if TS_USE_HRW_MAXMINDDB |
| if (test_maxmind_geo()) { |
| return 1; |
| } |
| #endif |
| |
| return 0; |
| } |