Skip to content

Commit 967fc70

Browse files
authored
fix: bypass context propagation if headers are not initialized (#213)
When processing HTTP/0.9 requests, the HTTP headers data structure is not initialized because this version of HTTP does not support headers. This leads to a crash when the code attempts to add context propagation information to an uninitialized list of headers. We have implemented a check to bypass context propagation if the headers are not initialized. Resolves #212 Changes: - move headers logic into `common` folder. - add integration test with HTTP/0.9.
1 parent f2ed89a commit 967fc70

File tree

8 files changed

+172
-91
lines changed

8 files changed

+172
-91
lines changed

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ target_sources(ngx_http_datadog_objs
138138
PRIVATE
139139
src/common/variable.cpp
140140
src/common/directives.cpp
141+
src/common/headers.cpp
141142
src/array_util.cpp
142143
src/datadog_conf.cpp
143144
src/datadog_conf_handler.cpp

src/common/headers.cpp

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
#include "headers.h"
2+
3+
#include "string_util.h"
4+
5+
namespace datadog::common {
6+
7+
ngx_table_elt_t *search_header(ngx_list_t &headers, std::string_view key) {
8+
ngx_list_part_t *part = &headers.part;
9+
auto *h = static_cast<ngx_table_elt_t *>(part->elts);
10+
11+
for (std::size_t i = 0;; i++) {
12+
if (i >= part->nelts) {
13+
if (part->next == nullptr) {
14+
break;
15+
}
16+
17+
part = part->next;
18+
h = static_cast<ngx_table_elt_t *>(part->elts);
19+
i = 0;
20+
}
21+
22+
if (key.size() != h[i].key.len ||
23+
ngx_strncasecmp((u_char *)key.data(), h[i].key.data, key.size()) != 0) {
24+
continue;
25+
}
26+
27+
return &h[i];
28+
}
29+
30+
return nullptr;
31+
}
32+
33+
bool add_header(ngx_pool_t &pool, ngx_list_t &headers, std::string_view key,
34+
std::string_view value) {
35+
if (headers.last == nullptr) {
36+
// Certainly a bad request (4xx). No need to add HTTP headers.
37+
return false;
38+
}
39+
40+
ngx_table_elt_t *h = static_cast<ngx_table_elt_t *>(ngx_list_push(&headers));
41+
if (h == nullptr) {
42+
return false;
43+
}
44+
45+
const auto key_size = key.size();
46+
47+
// This trick tells ngx_http_header_module to reflect the header value
48+
// in the actual response. Otherwise the header will be ignored and client
49+
// will never see it. To date the value must be just non zero.
50+
// Source:
51+
// <https://web.archive.org/web/20240409072840/https://www.nginx.com/resources/wiki/start/topics/examples/headers_management/>
52+
h->hash = 1;
53+
54+
// HTTP proxy module expects the header to has a lowercased key value
55+
// Instead of allocating twice the same key, `h->key` and `h->lowcase_key`
56+
// use the same data.
57+
h->key.len = key_size;
58+
h->key.data = (u_char *)ngx_pnalloc(&pool, sizeof(char) * key_size);
59+
for (std::size_t i = 0; i < key_size; ++i) {
60+
h->key.data[i] = nginx::to_lower(key[i]);
61+
}
62+
h->lowcase_key = h->key.data;
63+
64+
h->value = nginx::to_ngx_str(&pool, value);
65+
return true;
66+
}
67+
68+
} // namespace datadog::common

src/common/headers.h

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#pragma once
2+
3+
extern "C" {
4+
#include <ngx_core.h>
5+
#include <ngx_hash.h>
6+
}
7+
8+
#include <string_view>
9+
10+
namespace datadog::common {
11+
12+
/// Searches through an NGINX header list to find a header with a matching key.
13+
///
14+
/// @param headers
15+
/// A reference to an NGINX-style list (`ngx_list_t`) containing
16+
/// `ngx_table_elt_t` elements, typically representing HTTP headers.
17+
///
18+
/// @param key
19+
/// A string view representing the name of the header to search for.
20+
/// The comparison is typically case-insensitive.
21+
///
22+
/// @return
23+
/// A pointer to the matching `ngx_table_elt_t` header element if found,
24+
/// or `nullptr` if no header with the given key exists in the list.
25+
////
26+
ngx_table_elt_t *search_header(ngx_list_t &headers, std::string_view key);
27+
28+
/// Adds a new HTTP header to an NGINX-style header list.
29+
///
30+
/// @param pool
31+
/// A reference to the NGINX memory pool (`ngx_pool_t`) used for allocating
32+
/// memory for the new header and its key/value strings. This pool must
33+
/// remain valid for the lifetime of the header.
34+
///
35+
/// @param headers
36+
/// A reference to an `ngx_list_t` representing the list of HTTP headers
37+
/// (`ngx_table_elt_t` elements) to which the new header will be appended.
38+
///
39+
/// @param key
40+
/// A string view representing the header name (e.g., "Content-Type").
41+
/// This will be copied into the NGINX pool memory before insertion.
42+
///
43+
/// @param value
44+
/// A string view representing the header value (e.g., "application/json").
45+
/// This will also be copied into the NGINX pool memory.
46+
///
47+
/// @return
48+
/// `true` if the header was successfully added to the list;
49+
/// `false` if memory allocation failed or the list could not be updated.
50+
bool add_header(ngx_pool_t &pool, ngx_list_t &headers, std::string_view key,
51+
std::string_view value);
52+
53+
} // namespace datadog::common

src/ngx_header_writer.h

Lines changed: 4 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
#include <datadog/dict_writer.h>
44

5+
#include "common/headers.h"
56
#include "string_util.h"
67

78
extern "C" {
@@ -23,66 +24,14 @@ class NgxHeaderWriter : public datadog::tracing::DictWriter {
2324
: request_(request), pool_(request_->pool) {}
2425

2526
void set(std::string_view key, std::string_view value) override {
26-
const auto key_size = key.size();
27-
28-
ngx_table_elt_t *h = search_header(key);
27+
ngx_table_elt_t *h =
28+
common::search_header(request_->headers_in.headers, key);
2929
if (h != nullptr) {
3030
h->value = to_ngx_str(pool_, value);
3131
} else {
32-
h = static_cast<ngx_table_elt_t *>(
33-
ngx_list_push(&request_->headers_in.headers));
34-
if (h == nullptr) {
35-
return;
36-
}
37-
38-
// This trick tells ngx_http_header_module to reflect the header value
39-
// in the actual response. Otherwise the header will be ignored and client
40-
// will never see it. To date the value must be just non zero.
41-
// Source:
42-
// <https://web.archive.org/web/20240409072840/https://www.nginx.com/resources/wiki/start/topics/examples/headers_management/>
43-
h->hash = 1;
44-
45-
// HTTP proxy module expects the header to has a lowercased key value
46-
// Instead of allocating twice the same key, `h->key` and `h->lowcase_key`
47-
// use the same data.
48-
h->key.len = key_size;
49-
h->key.data = (u_char *)ngx_pnalloc(pool_, sizeof(char) * key_size);
50-
for (std::size_t i = 0; i < key_size; ++i) {
51-
h->key.data[i] = to_lower(key[i]);
52-
}
53-
h->lowcase_key = h->key.data;
54-
55-
h->value = to_ngx_str(pool_, value);
32+
common::add_header(*pool_, request_->headers_in.headers, key, value);
5633
}
5734
}
58-
59-
private:
60-
ngx_table_elt_t *search_header(std::string_view key) {
61-
ngx_list_part_t *part = &request_->headers_in.headers.part;
62-
auto *h = static_cast<ngx_table_elt_t *>(part->elts);
63-
64-
for (std::size_t i = 0;; i++) {
65-
if (i >= part->nelts) {
66-
if (part->next == nullptr) {
67-
break;
68-
}
69-
70-
part = part->next;
71-
h = static_cast<ngx_table_elt_t *>(part->elts);
72-
i = 0;
73-
}
74-
75-
if (key.size() != h[i].key.len ||
76-
ngx_strncasecmp((u_char *)key.data(), h[i].key.data, key.size()) !=
77-
0) {
78-
continue;
79-
}
80-
81-
return &h[i];
82-
}
83-
84-
return nullptr;
85-
}
8635
};
8736

8837
} // namespace nginx

src/rum/injection.cpp

Lines changed: 5 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ extern "C" {
88

99
#include <cassert>
1010

11+
#include "common/headers.h"
1112
#include "datadog_conf.h"
1213
#include "ngx_http_datadog_module.h"
1314
#include "string_util.h"
@@ -18,32 +19,6 @@ namespace nginx {
1819
namespace rum {
1920
namespace {
2021

21-
ngx_table_elt_t *search_header(ngx_list_t &headers, std::string_view key) {
22-
ngx_list_part_t *part = &headers.part;
23-
auto *h = static_cast<ngx_table_elt_t *>(part->elts);
24-
25-
for (std::size_t i = 0;; i++) {
26-
if (i >= part->nelts) {
27-
if (part->next == nullptr) {
28-
break;
29-
}
30-
31-
part = part->next;
32-
h = static_cast<ngx_table_elt_t *>(part->elts);
33-
i = 0;
34-
}
35-
36-
if (key.size() != h[i].key.len ||
37-
ngx_strcasecmp((u_char *)key.data(), h[i].key.data) != 0) {
38-
continue;
39-
}
40-
41-
return &h[i];
42-
}
43-
44-
return nullptr;
45-
}
46-
4722
bool is_html_content(ngx_str_t *content_type) {
4823
assert(content_type != nullptr);
4924
std::string_view content_type_sv = to_string_view(*content_type);
@@ -62,20 +37,14 @@ InjectionHandler::~InjectionHandler() {
6237
}
6338

6439
ngx_int_t InjectionHandler::on_rewrite_handler(ngx_http_request_t *r) {
65-
ngx_table_elt_t *h =
66-
static_cast<ngx_table_elt_t *>(ngx_list_push(&r->headers_in.headers));
67-
if (h == nullptr) {
40+
if (!common::add_header(r->pool, r->headers_in.headers,
41+
"x-datadog-rum-injection-pending", "1")) {
6842
ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
6943
"RUM SDK injection failed: unable to add "
7044
"x-datadog-rum-injection-pending HTTP header");
7145
return NGX_ERROR;
7246
}
7347

74-
ngx_str_set(&h->key, "x-datadog-rum-injection-pending");
75-
ngx_str_set(&h->value, "1");
76-
h->lowcase_key = h->key.data;
77-
h->hash = 1;
78-
7948
return NGX_DECLINED;
8049
}
8150

@@ -88,8 +57,8 @@ ngx_int_t InjectionHandler::on_header_filter(
8857
return next_header_filter(r);
8958
}
9059

91-
if (auto injected_header =
92-
search_header(r->headers_in.headers, "x-datadog-rum-injected");
60+
if (auto injected_header = common::search_header(r->headers_in.headers,
61+
"x-datadog-rum-injected");
9362
injected_header != nullptr) {
9463
if (nginx::to_string_view(injected_header->value) == "1") {
9564
ngx_log_error(NGX_LOG_INFO, r->connection->log, 0,

test/cases/auto_propagation/conf/http_auto.conf

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ http {
1313

1414
proxy_set_header server-block-header not-hidden-by-autoinjection;
1515

16+
location / {
17+
return 200 "Hello, World!";
18+
}
19+
1620
location /http {
1721
proxy_pass http://http:8080;
1822
}

test/cases/auto_propagation/test_http.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,19 @@ def test_without_module(self):
6969
return self.run_test("./conf/http_without_module.conf",
7070
should_propagate=False)
7171

72+
def test_illformated_request(self):
73+
# From: <https://github.com/DataDog/nginx-datadog/issues/212>
74+
conf_path = Path(__file__).parent / "./conf/http_auto.conf"
75+
conf_text = conf_path.read_text()
76+
status, log_lines = self.orch.nginx_replace_config(
77+
conf_text, conf_path.name)
78+
self.assertEqual(status, 0, log_lines)
79+
80+
status, response = self.orch.send_nginx_raw_http_request(
81+
request_line="GET /\r\n")
82+
self.assertEqual(0, status)
83+
self.assertEqual("Hello, World!", response)
84+
7285
def run_test(self, conf_relative_path, should_propagate):
7386
conf_path = Path(__file__).parent / conf_relative_path
7487
conf_text = conf_path.read_text()

test/cases/orchestration.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,30 @@ def send_nginx_websocket_request(
600600
)
601601
return result.returncode, result.stdout
602602

603+
def send_nginx_raw_http_request(self, port=80, request_line=""):
604+
"""Send the request line to nginx, and return the resulting HTTP
605+
status code and response body as a tuple `(status, body)`.
606+
"""
607+
command = docker_compose_command(
608+
"exec",
609+
"-T",
610+
"--",
611+
"client",
612+
"nc",
613+
"nginx",
614+
str(port),
615+
)
616+
617+
result = subprocess.run(
618+
command,
619+
input=request_line,
620+
stdout=subprocess.PIPE,
621+
env=child_env(),
622+
encoding="utf8",
623+
check=True,
624+
)
625+
return result.returncode, result.stdout
626+
603627
def send_nginx_http_request(
604628
self,
605629
path,

0 commit comments

Comments
 (0)