diff --git a/appsec/src/extension/commands/request_shutdown.c b/appsec/src/extension/commands/request_shutdown.c
index dc845e665c..64ece7f03e 100644
--- a/appsec/src/extension/commands/request_shutdown.c
+++ b/appsec/src/extension/commands/request_shutdown.c
@@ -6,6 +6,7 @@
#include "request_shutdown.h"
#include "../commands_helpers.h"
+#include "../configuration.h"
#include "../ddappsec.h"
#include "../ddtrace.h"
#include "../entity_body.h"
@@ -62,7 +63,11 @@ static dd_result _request_pack(mpack_writer_t *nonnull w, void *nonnull ctx)
}
}
- mpack_start_map(w, 2 + (Z_TYPE(resp_body) != IS_NULL ? 1 : 0));
+ bool send_raw_body = get_global_DD_APPSEC_RAW_RESPONSE_BODY_ENABLED() &&
+ req_info->entity != NULL && req_info->entity->len > 0;
+
+ mpack_start_map(w, 2 + (Z_TYPE(resp_body) != IS_NULL ? 1 : 0) +
+ (send_raw_body ? 1 : 0));
// 1.1.
{
@@ -94,6 +99,13 @@ static dd_result _request_pack(mpack_writer_t *nonnull w, void *nonnull ctx)
}
}
+ // 1.4.?
+ if (send_raw_body) {
+ dd_mpack_write_lstr(w, "server.response.body.raw");
+ mpack_write_str(w, ZSTR_VAL(req_info->entity),
+ (uint32_t)ZSTR_LEN(req_info->entity));
+ }
+
mpack_finish_map(w);
// 2.
diff --git a/appsec/src/extension/configuration.h b/appsec/src/extension/configuration.h
index 641c073ba4..8e41475d0a 100644
--- a/appsec/src/extension/configuration.h
+++ b/appsec/src/extension/configuration.h
@@ -62,6 +62,7 @@ extern bool runtime_config_first_init;
CONFIG(STRING, DD_AGENT_HOST, "localhost") \
CONFIG(INT, DD_TRACE_AGENT_PORT, "0") \
CONFIG(INT, DD_APPSEC_MAX_BODY_BUFF_SIZE, "524288") \
+ SYSCFG(BOOL, DD_APPSEC_RAW_RESPONSE_BODY_ENABLED, "false") \
CONFIG(STRING, DD_TRACE_AGENT_URL, "") \
CONFIG(BOOL, DD_TRACE_ENABLED, "true") \
CALIAS(CUSTOM(STRING), DD_APPSEC_AUTO_USER_INSTRUMENTATION_MODE, "ident", \
diff --git a/appsec/tests/extension/rshutdown_command_raw_body_disabled.phpt b/appsec/tests/extension/rshutdown_command_raw_body_disabled.phpt
new file mode 100644
index 0000000000..fe1d08753e
--- /dev/null
+++ b/appsec/tests/extension/rshutdown_command_raw_body_disabled.phpt
@@ -0,0 +1,33 @@
+--TEST--
+request_shutdown — server.response.body.raw not sent when feature is disabled (default)
+--INI--
+expose_php=0
+datadog.appsec.enabled=1
+--GET--
+a=b
+--FILE--
+get_commands(); // ignore
+
+var_dump(rshutdown());
+$c = $helper->get_commands();
+var_dump(array_key_exists('server.response.body.raw', $c[0][1][0]));
+?>
+--EXPECT--
+bool(true)
+plain text body
+bool(true)
+bool(false)
diff --git a/appsec/tests/extension/rshutdown_command_raw_body_json.phpt b/appsec/tests/extension/rshutdown_command_raw_body_json.phpt
new file mode 100644
index 0000000000..2cfde38ed5
--- /dev/null
+++ b/appsec/tests/extension/rshutdown_command_raw_body_json.phpt
@@ -0,0 +1,46 @@
+--TEST--
+request_shutdown — server.response.body.raw sent alongside parsed server.response.body for JSON
+--INI--
+expose_php=0
+datadog.appsec.enabled=1
+datadog.appsec.raw_response_body_enabled=1
+--GET--
+a=b
+--FILE--
+get_commands(); // ignore
+
+var_dump(rshutdown());
+$c = $helper->get_commands();
+$data = $c[0][1][0];
+
+// both structured and raw body are present
+var_dump(isset($data['server.response.body']));
+var_dump(isset($data['server.response.body.raw']));
+print_r($data['server.response.body']);
+echo $data['server.response.body.raw'];
+?>
+--EXPECT--
+bool(true)
+{"key":"value"}
+bool(true)
+bool(true)
+bool(true)
+Array
+(
+ [key] => value
+)
+{"key":"value"}
diff --git a/appsec/tests/extension/rshutdown_command_raw_body_plain_text.phpt b/appsec/tests/extension/rshutdown_command_raw_body_plain_text.phpt
new file mode 100644
index 0000000000..cb7ccfa14e
--- /dev/null
+++ b/appsec/tests/extension/rshutdown_command_raw_body_plain_text.phpt
@@ -0,0 +1,42 @@
+--TEST--
+request_shutdown — server.response.body.raw sent for non-JSON/XML content types
+--INI--
+expose_php=0
+datadog.appsec.enabled=1
+datadog.appsec.raw_response_body_enabled=1
+--GET--
+a=b
+--FILE--
+get_commands(); // ignore
+
+var_dump(rshutdown());
+$c = $helper->get_commands();
+$data = $c[0][1][0];
+
+// server.response.body is absent for non-JSON/XML
+var_dump(isset($data['server.response.body']));
+// server.response.body.raw is present with the raw text
+var_dump(isset($data['server.response.body.raw']));
+echo $data['server.response.body.raw'];
+?>
+--EXPECT--
+bool(true)
+plain text body
+bool(true)
+bool(false)
+bool(true)
+plain text body
diff --git a/appsec/tests/extension/rshutdown_command_raw_body_xml.phpt b/appsec/tests/extension/rshutdown_command_raw_body_xml.phpt
new file mode 100644
index 0000000000..6b4a315108
--- /dev/null
+++ b/appsec/tests/extension/rshutdown_command_raw_body_xml.phpt
@@ -0,0 +1,61 @@
+--TEST--
+request_shutdown — server.response.body.raw sent alongside parsed server.response.body for XML
+--INI--
+expose_php=0
+datadog.appsec.enabled=1
+datadog.appsec.raw_response_body_enabled=1
+--GET--
+a=b
+--FILE--
+
+baz
+XML;
+echo "$xml\n";
+$helper->get_commands(); // ignore
+
+var_dump(rshutdown());
+$c = $helper->get_commands();
+$data = $c[0][1][0];
+
+// both structured and raw body are present
+var_dump(isset($data['server.response.body']));
+var_dump(isset($data['server.response.body.raw']));
+print_r($data['server.response.body']);
+echo $data['server.response.body.raw'];
+?>
+--EXPECT--
+bool(true)
+
+baz
+bool(true)
+bool(true)
+bool(true)
+Array
+(
+ [foo] => Array
+ (
+ [0] => Array
+ (
+ [@attr] => bar
+ )
+
+ [1] => baz
+ )
+
+)
+
+baz