diff --git a/agent_controller/agent_controller.c b/agent_controller/agent_controller.c index 49252424..3fc58989 100644 --- a/agent_controller/agent_controller.c +++ b/agent_controller/agent_controller.c @@ -911,6 +911,37 @@ agent_controller_parse_scan_agent_config (cJSON *root) return d; } +/** + * @brief Extract the filename from a Content-Disposition header value. + * + * @param[in] content_disposition The Content-Disposition header value to parse. + * + * @return A newly allocated string containing the filename, or NULL if not + * found. The caller is responsible for freeing the returned string + * with g_free(). + */ +static gchar * +content_disposition_filename (const gchar *content_disposition) +{ + const gchar *start; + const gchar *end; + + if (!content_disposition) + return NULL; + + start = strstr (content_disposition, "filename=\""); + if (!start) + return NULL; + + start += strlen ("filename=\""); + end = strchr (start, '"'); + + if (!end || end == start) + return NULL; + + return g_strndup (start, end - start); +} + /** * @brief Creates a new Agent Controller connector. */ @@ -1221,6 +1252,33 @@ agent_controller_installer_instruction_free ( g_free (instr); } +/** + * @brief Allocate/zero a new support bundle struct. + * + * @return Newly allocated support bundle struct + */ +agent_controller_support_bundle_t +agent_controller_support_bundle_new (void) +{ + return g_malloc0 (sizeof (struct agent_controller_support_bundle)); +} + +/** + * @brief Free a support bundle struct. + * + * @param[in] bundle to be freed + */ +void +agent_controller_support_bundle_free (agent_controller_support_bundle_t bundle) +{ + if (!bundle) + return; + + g_free (bundle->data); + g_free (bundle->filename); + g_free (bundle); +} + /** * @brief Allocates and initializes a new scan agent config structure. * @@ -1979,3 +2037,103 @@ agent_controller_get_installer_instruction (agent_controller_connector_t conn, gvm_http_response_free (response); return instr; } + +/** + * @brief Downloads a support bundle for a specific agent. + * + * @param[in] conn Active connector to the Agent Controller. + * @param[in] agent_id ID of the agent. + * @param[in] days Number of days of logs to include. A value of 0 uses the + * Agent Controller's configured default. + * + * @return Newly allocated support bundle on success, NULL on failure. + * Free with agent_controller_support_bundle_free(). + */ +agent_controller_support_bundle_t +agent_controller_download_support_bundle (agent_controller_connector_t conn, + const gchar *agent_id, int days) +{ + gvm_http_headers_t *headers; + gvm_http_response_t *response; + agent_controller_support_bundle_t bundle; + gchar *escaped_agent_id; + gchar *path; + + if (!conn || !agent_id || *agent_id == '\0' || days < 0) + return NULL; + + escaped_agent_id = g_uri_escape_string (agent_id, NULL, TRUE); + if (!escaped_agent_id) + return NULL; + + if (days > 0) + path = + g_strdup_printf ("/agent-control/v2/api/agents/%s/support-bundle?days=%d", + escaped_agent_id, days); + else + path = g_strdup_printf ("/agent-control/v2/api/agents/%s/support-bundle", + escaped_agent_id); + + g_free (escaped_agent_id); + + headers = init_custom_header (conn->apikey, FALSE); + if (!headers) + { + g_free (path); + return NULL; + } + + if (!add_custom_header (headers, "Accept", "application/octet-stream")) + { + gvm_http_headers_free (headers); + g_free (path); + return NULL; + } + + response = + agent_controller_send_request_with_headers (conn, GET, path, NULL, headers); + + gvm_http_headers_free (headers); + g_free (path); + + if (!response) + return NULL; + + if (response->http_status != 200) + { + g_warning ("%s: Received HTTP status %ld", __func__, + response->http_status); + gvm_http_response_free (response); + return NULL; + } + + if (!response->data || response->size == 0) + { + g_warning ("%s: Received an empty support bundle", __func__); + gvm_http_response_free (response); + return NULL; + } + + bundle = agent_controller_support_bundle_new (); + if (!bundle) + { + gvm_http_response_free (response); + return NULL; + } + + /* + * Transfer ownership of the binary buffer from the generic HTTP response + * to the Agent Controller support-bundle object. + */ + bundle->data = (guint8 *) response->data; + bundle->size = response->size; + bundle->filename = + content_disposition_filename (response->content_disposition); + + response->data = NULL; + response->size = 0; + + gvm_http_response_free (response); + + return bundle; +} diff --git a/agent_controller/agent_controller.h b/agent_controller/agent_controller.h index fdf1bf5e..c3b9b4c5 100644 --- a/agent_controller/agent_controller.h +++ b/agent_controller/agent_controller.h @@ -237,6 +237,20 @@ struct agent_controller_installer_instruction typedef struct agent_controller_installer_instruction *agent_controller_installer_instruction_t; +/** + * @brief Struct representing a support bundle for agents. + */ +struct agent_controller_support_bundle +{ + guint8 *data; ///> The data of the support bundle file (e.g., ZIP archive) + gsize size; ///< Size of the data in bytes + gchar *filename; ///< Suggested filename for the support bundle (e.g., + ///< "support_bundle.zip") +}; + +typedef struct agent_controller_support_bundle + *agent_controller_support_bundle_t; + agent_controller_connector_t agent_controller_connector_new (void); @@ -286,6 +300,12 @@ void agent_controller_installer_instruction_free ( agent_controller_installer_instruction_t instr); +agent_controller_support_bundle_t +agent_controller_support_bundle_new (void); + +void +agent_controller_support_bundle_free (agent_controller_support_bundle_t bundle); + agent_controller_agent_list_t agent_controller_get_agents (agent_controller_connector_t conn); @@ -341,4 +361,8 @@ agent_controller_installer_instruction_t agent_controller_get_installer_instruction (agent_controller_connector_t conn, instructions_lang_type_t lang_type); +agent_controller_support_bundle_t +agent_controller_download_support_bundle (agent_controller_connector_t conn, + const gchar *agent_id, int days); + #endif /* not _GVM_AGENT_CONTROLLER_AGENT_CONTROLLER_H */ diff --git a/agent_controller/agent_controller_tests.c b/agent_controller/agent_controller_tests.c index 44113cf5..07d01ede 100644 --- a/agent_controller/agent_controller_tests.c +++ b/agent_controller/agent_controller_tests.c @@ -12,6 +12,10 @@ static gchar *last_sent_url = NULL; static gchar *last_sent_payload = NULL; static long mock_http_status = 200; static gchar *mock_response_data = NULL; +static guint8 *mock_binary_response_data = NULL; +static gsize mock_binary_response_size = 0; +static gchar *mock_content_disposition = NULL; +static gboolean mock_empty_response = FALSE; Describe (agent_controller); @@ -26,6 +30,11 @@ BeforeEach (agent_controller) g_clear_pointer (&last_sent_url, g_free); g_clear_pointer (&last_sent_payload, g_free); g_clear_pointer (&mock_response_data, g_free); + g_clear_pointer (&mock_binary_response_data, g_free); + g_clear_pointer (&mock_content_disposition, g_free); + + mock_binary_response_size = 0; + mock_empty_response = FALSE; mock_http_status = 200; } @@ -33,6 +42,12 @@ AfterEach (agent_controller) { g_clear_pointer (&last_sent_url, g_free); g_clear_pointer (&last_sent_payload, g_free); + g_clear_pointer (&mock_response_data, g_free); + g_clear_pointer (&mock_binary_response_data, g_free); + g_clear_pointer (&mock_content_disposition, g_free); + + mock_binary_response_size = 0; + mock_empty_response = FALSE; if (called_headers) { @@ -70,16 +85,39 @@ gvm_http_request_unix (const gchar *url, gvm_http_method_t method, (void) sock_path; last_sent_url = g_strdup (url); - last_sent_payload = g_strdup (payload); + last_sent_payload = payload ? g_strdup (payload) : NULL; - if (!mock_response_data && mock_http_status != 200) + if (!mock_response_data && !mock_binary_response_data + && mock_http_status != 200) return NULL; gvm_http_response_t *response = g_malloc0 (sizeof (gvm_http_response_t)); + response->http_status = mock_http_status; - response->data = - mock_response_data ? g_strdup (mock_response_data) : g_strdup ("{}"); - response->size = strlen (response->data); + response->content_disposition = g_strdup (mock_content_disposition); + + if (mock_empty_response) + { + response->data = NULL; + response->size = 0; + } + else if (mock_binary_response_data) + { + response->data = g_malloc (mock_binary_response_size + 1); + + memcpy (response->data, mock_binary_response_data, + mock_binary_response_size); + + response->data[mock_binary_response_size] = '\0'; + response->size = mock_binary_response_size; + } + else + { + response->data = + mock_response_data ? g_strdup (mock_response_data) : g_strdup ("{}"); + + response->size = strlen (response->data); + } return response; } @@ -194,6 +232,19 @@ make_update_list_with_ids (const char *const *ids, int n) return list; } +static void +set_mock_binary_response (const guint8 *data, gsize size) +{ + g_clear_pointer (&mock_binary_response_data, g_free); + mock_binary_response_size = 0; + + if (!data || size == 0) + return; + + mock_binary_response_data = g_memdup2 (data, size); + mock_binary_response_size = size; +} + Ensure (agent_controller, connector_new_returns_valid_connector) { agent_controller_connector_t conn = agent_controller_connector_new (); @@ -1403,14 +1454,14 @@ Ensure (agent_controller, agent_update_list_free_handles_null) Ensure (agent_controller, get_agents_returns_list_on_successful_response) { - mock_response_data = "[{" - "\"agentid\": \"agent1\"," - "\"hostname\": \"host-a\"," - "\"authorized\": true," - "\"min_interval\": 5," - "\"heartbeat_interval\": 10," - "\"connection_status\": \"online\"" - "}]"; + mock_response_data = g_strdup ("[{" + "\"agentid\": \"agent1\"," + "\"hostname\": \"host-a\"," + "\"authorized\": true," + "\"min_interval\": 5," + "\"heartbeat_interval\": 10," + "\"connection_status\": \"online\"" + "}]"); mock_http_status = 200; agent_controller_connector_t conn = make_conn (); @@ -1429,17 +1480,17 @@ Ensure (agent_controller, get_agents_returns_list_on_successful_response) Ensure (agent_controller, get_agents_returns_list_on_successful_response_with_extended_fields) { - mock_response_data = "[{" - "\"agentid\":\"agent1\"," - "\"hostname\":\"host-a\"," - "\"authorized\":true," - "\"connection_status\":\"active\"," - "\"updater_version\":\"1.2.3\"," - "\"agent_version\":\"0.9.0\"," - "\"operating_system\":\"Linux\"," - "\"architecture\":\"amd64\"," - "\"update_to_latest\":true" - "}]"; + mock_response_data = g_strdup ("[{" + "\"agentid\":\"agent1\"," + "\"hostname\":\"host-a\"," + "\"authorized\":true," + "\"connection_status\":\"active\"," + "\"updater_version\":\"1.2.3\"," + "\"agent_version\":\"0.9.0\"," + "\"operating_system\":\"Linux\"," + "\"architecture\":\"amd64\"," + "\"update_to_latest\":true" + "}]"); mock_http_status = 200; agent_controller_connector_t conn = make_conn (); @@ -1471,7 +1522,7 @@ Ensure (agent_controller, Ensure (agent_controller, get_agents_returns_null_on_non_200_status) { mock_http_status = 403; - mock_response_data = "[{\"agentid\": \"a\"}]"; + mock_response_data = g_strdup ("[{\"agentid\": \"a\"}]"); agent_controller_connector_t conn = make_conn (); @@ -1484,7 +1535,7 @@ Ensure (agent_controller, get_agents_returns_null_on_non_200_status) Ensure (agent_controller, get_agents_returns_null_on_invalid_json) { mock_http_status = 200; - mock_response_data = "not-a-json-array"; + mock_response_data = g_strdup ("not-a-json-array"); agent_controller_connector_t conn = make_conn (); @@ -3481,6 +3532,310 @@ Ensure (agent_controller, agent_controller_connector_free (conn); } +Ensure (agent_controller, content_disposition_filename_extracts_filename) +{ + const gchar *header = + "attachment; filename=\"support-bundle-GAT-29::2d61a736-" + "20260616T062558Z.tar.gz.enc\""; + + gchar *filename = content_disposition_filename (header); + + assert_that (filename, is_not_null); + assert_that (filename, is_equal_to_string ("support-bundle-GAT-29::2d61a736-" + "20260616T062558Z.tar.gz.enc")); + + g_free (filename); +} + +Ensure (agent_controller, content_disposition_filename_returns_null_for_null) +{ + gchar *filename = content_disposition_filename (NULL); + + assert_that (filename, is_null); +} + +Ensure (agent_controller, + content_disposition_filename_returns_null_without_filename) +{ + gchar *filename = content_disposition_filename ("attachment"); + + assert_that (filename, is_null); +} + +Ensure (agent_controller, + content_disposition_filename_returns_null_for_missing_closing_quote) +{ + gchar *filename = content_disposition_filename ( + "attachment; filename=\"support-bundle.tar.gz.enc"); + + assert_that (filename, is_null); +} + +Ensure (agent_controller, + content_disposition_filename_returns_null_for_empty_filename) +{ + gchar *filename = content_disposition_filename ("attachment; filename=\"\""); + + assert_that (filename, is_null); +} + +Ensure (agent_controller, support_bundle_new_returns_zero_initialized_bundle) +{ + agent_controller_support_bundle_t bundle = + agent_controller_support_bundle_new (); + + assert_that (bundle, is_not_null); + assert_that (bundle->data, is_null); + assert_that (bundle->filename, is_null); + assert_that (bundle->size, is_equal_to ((gsize) 0)); + + agent_controller_support_bundle_free (bundle); +} + +Ensure (agent_controller, support_bundle_free_handles_null) +{ + agent_controller_support_bundle_free (NULL); + + assert_that (true, is_true); +} + +Ensure (agent_controller, support_bundle_free_handles_populated_bundle) +{ + static const guint8 data[] = {0x53, 0x61, 0x6c, 0x74, 0x00, 0xff}; + + agent_controller_support_bundle_t bundle = + agent_controller_support_bundle_new (); + + assert_that (bundle, is_not_null); + + bundle->data = g_memdup2 (data, sizeof (data)); + bundle->size = sizeof (data); + bundle->filename = g_strdup ("support-bundle.tar.gz.enc"); + + agent_controller_support_bundle_free (bundle); + + assert_that (true, is_true); +} + +Ensure (agent_controller, download_support_bundle_returns_data_and_filename) +{ + static const guint8 encrypted_data[] = {0x53, 0x61, 0x6c, 0x74, 0x00, + 0xff, 0x10, 0x20, 0x00, 0x42}; + + const gchar *expected_filename = "support-bundle-GAT-29::2d61a736-" + "20260616T062558Z.tar.gz.enc"; + + mock_http_status = 200; + + set_mock_binary_response (encrypted_data, sizeof (encrypted_data)); + + mock_content_disposition = + g_strdup_printf ("attachment; filename=\"%s\"", expected_filename); + + agent_controller_connector_t conn = make_conn (); + + agent_controller_support_bundle_t bundle = + agent_controller_download_support_bundle (conn, "GAT-29::2d61a736", 0); + + assert_that (bundle, is_not_null); + assert_that (bundle->data, is_not_null); + assert_that (bundle->size, is_equal_to ((gsize) sizeof (encrypted_data))); + assert_that (bundle->filename, is_equal_to_string (expected_filename)); + + assert_that (memcmp (bundle->data, encrypted_data, sizeof (encrypted_data)), + is_equal_to (0)); + + assert_that ( + last_sent_url, + is_equal_to_string ("http://localhost:8081/agent-control/v2/api/agents/" + "GAT-29%3A%3A2d61a736/support-bundle")); + + assert_that (last_sent_payload, is_null); + + assert_that (called_headers, is_not_null); + assert_that ((int) called_headers->len, is_equal_to (2)); + + assert_that ((const gchar *) g_ptr_array_index (called_headers, 0), + is_equal_to_string ("X-API-KEY: token")); + + assert_that ((const gchar *) g_ptr_array_index (called_headers, 1), + is_equal_to_string ("Accept: application/octet-stream")); + + agent_controller_support_bundle_free (bundle); + agent_controller_connector_free (conn); +} + +Ensure (agent_controller, download_support_bundle_adds_days_parameter) +{ + static const guint8 encrypted_data[] = {0x01, 0x02, 0x03}; + + mock_http_status = 200; + set_mock_binary_response (encrypted_data, sizeof (encrypted_data)); + + mock_content_disposition = g_strdup ("attachment; filename=\"bundle.enc\""); + + agent_controller_connector_t conn = make_conn (); + + agent_controller_support_bundle_t bundle = + agent_controller_download_support_bundle (conn, "agent-123", 7); + + assert_that (bundle, is_not_null); + + assert_that ( + last_sent_url, + is_equal_to_string ("http://localhost:8081/agent-control/v2/api/agents/" + "agent-123/support-bundle?days=7")); + + agent_controller_support_bundle_free (bundle); + agent_controller_connector_free (conn); +} + +Ensure (agent_controller, + download_support_bundle_allows_missing_content_disposition) +{ + static const guint8 encrypted_data[] = {0x01, 0x02}; + + mock_http_status = 200; + set_mock_binary_response (encrypted_data, sizeof (encrypted_data)); + + agent_controller_connector_t conn = make_conn (); + + agent_controller_support_bundle_t bundle = + agent_controller_download_support_bundle (conn, "agent-123", 0); + + assert_that (bundle, is_not_null); + assert_that (bundle->filename, is_null); + assert_that (bundle->size, is_equal_to ((gsize) sizeof (encrypted_data))); + + agent_controller_support_bundle_free (bundle); + agent_controller_connector_free (conn); +} + +Ensure (agent_controller, + download_support_bundle_allows_invalid_content_disposition) +{ + static const guint8 encrypted_data[] = {0x01, 0x02}; + + mock_http_status = 200; + set_mock_binary_response (encrypted_data, sizeof (encrypted_data)); + + mock_content_disposition = g_strdup ("attachment"); + + agent_controller_connector_t conn = make_conn (); + + agent_controller_support_bundle_t bundle = + agent_controller_download_support_bundle (conn, "agent-123", 0); + + assert_that (bundle, is_not_null); + assert_that (bundle->filename, is_null); + + agent_controller_support_bundle_free (bundle); + agent_controller_connector_free (conn); +} + +Ensure (agent_controller, + download_support_bundle_returns_null_for_null_connection) +{ + agent_controller_support_bundle_t bundle = + agent_controller_download_support_bundle (NULL, "agent-123", 0); + + assert_that (bundle, is_null); + assert_that (last_sent_url, is_null); +} + +Ensure (agent_controller, + download_support_bundle_returns_null_for_null_agent_id) +{ + agent_controller_connector_t conn = make_conn (); + + agent_controller_support_bundle_t bundle = + agent_controller_download_support_bundle (conn, NULL, 0); + + assert_that (bundle, is_null); + assert_that (last_sent_url, is_null); + + agent_controller_connector_free (conn); +} + +Ensure (agent_controller, + download_support_bundle_returns_null_for_empty_agent_id) +{ + agent_controller_connector_t conn = make_conn (); + + agent_controller_support_bundle_t bundle = + agent_controller_download_support_bundle (conn, "", 0); + + assert_that (bundle, is_null); + assert_that (last_sent_url, is_null); + + agent_controller_connector_free (conn); +} + +Ensure (agent_controller, + download_support_bundle_returns_null_for_negative_days) +{ + agent_controller_connector_t conn = make_conn (); + + agent_controller_support_bundle_t bundle = + agent_controller_download_support_bundle (conn, "agent-123", -1); + + assert_that (bundle, is_null); + assert_that (last_sent_url, is_null); + + agent_controller_connector_free (conn); +} + +Ensure (agent_controller, download_support_bundle_returns_null_for_http_error) +{ + mock_http_status = 404; + mock_response_data = g_strdup ("Agent not found"); + + agent_controller_connector_t conn = make_conn (); + + agent_controller_support_bundle_t bundle = + agent_controller_download_support_bundle (conn, "missing-agent", 0); + + assert_that (bundle, is_null); + + assert_that (last_sent_url, contains_string ("/agent-control/v2/api/agents/" + "missing-agent/support-bundle")); + + agent_controller_connector_free (conn); +} + +Ensure (agent_controller, + download_support_bundle_returns_null_when_request_fails) +{ + mock_http_status = 500; + + agent_controller_connector_t conn = make_conn (); + + agent_controller_support_bundle_t bundle = + agent_controller_download_support_bundle (conn, "agent-123", 0); + + assert_that (bundle, is_null); + + agent_controller_connector_free (conn); +} + +Ensure (agent_controller, + download_support_bundle_returns_null_for_empty_response) +{ + mock_http_status = 200; + mock_empty_response = TRUE; + + mock_content_disposition = g_strdup ("attachment; filename=\"bundle.enc\""); + + agent_controller_connector_t conn = make_conn (); + + agent_controller_support_bundle_t bundle = + agent_controller_download_support_bundle (conn, "agent-123", 0); + + assert_that (bundle, is_null); + + agent_controller_connector_free (conn); +} + int main (int argc, char **argv) { @@ -3858,6 +4213,58 @@ main (int argc, char **argv) suite, agent_controller, get_installer_instruction_returns_null_when_request_fails); + add_test_with_context (suite, agent_controller, + content_disposition_filename_extracts_filename); + add_test_with_context (suite, agent_controller, + content_disposition_filename_returns_null_for_null); + add_test_with_context ( + suite, agent_controller, + content_disposition_filename_returns_null_without_filename); + add_test_with_context ( + suite, agent_controller, + content_disposition_filename_returns_null_for_missing_closing_quote); + add_test_with_context ( + suite, agent_controller, + content_disposition_filename_returns_null_for_empty_filename); + + add_test_with_context (suite, agent_controller, + support_bundle_new_returns_zero_initialized_bundle); + add_test_with_context (suite, agent_controller, + support_bundle_free_handles_null); + add_test_with_context (suite, agent_controller, + support_bundle_free_handles_populated_bundle); + + add_test_with_context (suite, agent_controller, + download_support_bundle_returns_data_and_filename); + add_test_with_context (suite, agent_controller, + download_support_bundle_adds_days_parameter); + add_test_with_context ( + suite, agent_controller, + download_support_bundle_allows_missing_content_disposition); + add_test_with_context ( + suite, agent_controller, + download_support_bundle_allows_invalid_content_disposition); + add_test_with_context ( + suite, agent_controller, + download_support_bundle_returns_null_for_null_connection); + add_test_with_context ( + suite, agent_controller, + download_support_bundle_returns_null_for_null_agent_id); + add_test_with_context ( + suite, agent_controller, + download_support_bundle_returns_null_for_empty_agent_id); + add_test_with_context ( + suite, agent_controller, + download_support_bundle_returns_null_for_negative_days); + add_test_with_context (suite, agent_controller, + download_support_bundle_returns_null_for_http_error); + add_test_with_context ( + suite, agent_controller, + download_support_bundle_returns_null_when_request_fails); + add_test_with_context ( + suite, agent_controller, + download_support_bundle_returns_null_for_empty_response); + if (argc > 1) ret = run_single_test (suite, argv[1], create_text_reporter ()); else diff --git a/http/httputils.c b/http/httputils.c index c98c3589..3b4ad64e 100644 --- a/http/httputils.c +++ b/http/httputils.c @@ -239,6 +239,43 @@ gvm_http_new_internal (const gchar *url, gvm_http_method_t method, return gvm_http_t_new (curl); } +/** + * @brief Capture selected HTTP response headers. + * + * @param buffer Header data received from libcurl. + * @param size Size of one data element. + * @param nitems Number of data elements. + * @param userdata HTTP response object to update. + * + * @return Number of bytes processed. + */ +static size_t +store_response_header (char *buffer, size_t size, size_t nitems, void *userdata) +{ + static const gchar header_name[] = "Content-Disposition:"; + gvm_http_response_t *response = userdata; + size_t length = size * nitems; + gchar *line; + gchar *value; + + if (!response || length == 0) + return length; + + line = g_strndup (buffer, length); + + if (g_ascii_strncasecmp (line, header_name, sizeof (header_name) - 1) == 0) + { + value = g_strstrip (line + sizeof (header_name) - 1); + + g_free (response->content_disposition); + response->content_disposition = g_strdup (value); + } + + g_free (line); + + return length; +} + /** * @brief Internal helper to send a synchronous HTTP request and capture * the response. @@ -287,6 +324,7 @@ gvm_http_request_internal (const gchar *url, gvm_http_method_t method, http_response->http_status = -1; http_response->data = g_strdup ("{\"error\": \"Failed to initialize curl request\"}"); + http_response->size = strlen (http_response->data); if (internal_stream_allocated) { @@ -299,6 +337,11 @@ gvm_http_request_internal (const gchar *url, gvm_http_method_t method, http_response->http = http; + /* Capture selected response headers. */ + curl_easy_setopt (http->handler, CURLOPT_HEADERFUNCTION, + store_response_header); + curl_easy_setopt (http->handler, CURLOPT_HEADERDATA, http_response); + CURLcode result = curl_easy_perform (http->handler); if (result == CURLE_OK) { @@ -318,7 +361,12 @@ gvm_http_request_internal (const gchar *url, gvm_http_method_t method, if (response && response->data) { g_free (http_response->data); - http_response->data = g_strdup (response->data); + + http_response->data = g_malloc (response->length + 1); + memcpy (http_response->data, response->data, response->length); + http_response->data[response->length] = '\0'; + + http_response->size = response->length; } else if (http_response->data == NULL) { @@ -544,6 +592,7 @@ gvm_http_response_free (gvm_http_response_t *response) gvm_http_free (response->http); g_free (response->data); + g_free (response->content_disposition); g_free (response); } diff --git a/http/httputils.h b/http/httputils.h index b136c802..48012448 100644 --- a/http/httputils.h +++ b/http/httputils.h @@ -107,6 +107,9 @@ typedef struct glong http_status; ///< HTTP status code returned by the server. + gchar *content_disposition; ///< The value of the Content-Disposition header, + ///< if present. + gvm_http_t *http; ///< The HTTP request (easy handle wrapper). } gvm_http_response_t; diff --git a/http/httputils_tests.c b/http/httputils_tests.c index bc802834..0c535f80 100644 --- a/http/httputils_tests.c +++ b/http/httputils_tests.c @@ -168,6 +168,57 @@ Ensure (gvm_http, response_stream_reset_frees_and_resets_data) gvm_http_response_stream_free (stream); } +Ensure (gvm_http, store_response_header_captures_content_disposition) +{ + gvm_http_response_t response = {0}; + + gchar header[] = "Content-Disposition: attachment; " + "filename=\"support-bundle-agent-123.tar.gz.enc\"\r\n"; + + size_t processed = + store_response_header (header, 1, strlen (header), &response); + + assert_that (processed, is_equal_to (strlen (header))); + assert_that (response.content_disposition, is_not_null); + assert_that ( + response.content_disposition, + is_equal_to_string ( + "attachment; filename=\"support-bundle-agent-123.tar.gz.enc\"")); + + g_free (response.content_disposition); +} + +Ensure (gvm_http, store_response_header_matches_case_insensitively) +{ + gvm_http_response_t response = {0}; + + gchar header[] = + "content-disposition: attachment; filename=\"bundle.enc\"\r\n"; + + store_response_header (header, 1, strlen (header), &response); + + assert_that (response.content_disposition, is_not_null); + assert_that (response.content_disposition, + is_equal_to_string ("attachment; filename=\"bundle.enc\"")); + + g_free (response.content_disposition); +} + +Ensure (gvm_http, store_response_header_trims_value) +{ + gvm_http_response_t response = {0}; + + gchar header[] = + "Content-Disposition: attachment; filename=\"bundle.enc\" \r\n"; + + store_response_header (header, 1, strlen (header), &response); + + assert_that (response.content_disposition, + is_equal_to_string ("attachment; filename=\"bundle.enc\"")); + + g_free (response.content_disposition); +} + int main (int argc, char **argv) { @@ -199,6 +250,11 @@ main (int argc, char **argv) add_test_with_context (suite, gvm_http, http_free_frees_allocated_struct); add_test_with_context (suite, gvm_http, response_stream_reset_frees_and_resets_data); + add_test_with_context (suite, gvm_http, + store_response_header_captures_content_disposition); + add_test_with_context (suite, gvm_http, + store_response_header_matches_case_insensitively); + add_test_with_context (suite, gvm_http, store_response_header_trims_value); if (argc > 1) ret = run_single_test (suite, argv[1], create_text_reporter ());