From 294f1165fa47620bf81da9fe94f9304895907661 Mon Sep 17 00:00:00 2001 From: Thibault Saunier Date: Wed, 11 Dec 2024 17:28:21 -0300 Subject: [PATCH] validate: Implement a 'http-request' action type Which is useable with our own HTTP server Part-of: --- .../gst/validate/gst-validate-http-actions.c | 271 ++++++++++++++++++ .../gst/validate/gst-validate-scenario.c | 76 +++++ .../validate/gst/validate/meson.build | 1 + 3 files changed, 348 insertions(+) create mode 100644 subprojects/gst-devtools/validate/gst/validate/gst-validate-http-actions.c diff --git a/subprojects/gst-devtools/validate/gst/validate/gst-validate-http-actions.c b/subprojects/gst-devtools/validate/gst/validate/gst-validate-http-actions.c new file mode 100644 index 0000000000..848938fe5f --- /dev/null +++ b/subprojects/gst-devtools/validate/gst/validate/gst-validate-http-actions.c @@ -0,0 +1,271 @@ +/* GStreamer + * + * Copyright (C) 2024 Igalia S.L + * Author: Thibault Saunier + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ +#include +#include +#include +#include +#include +#include + +gboolean run_http_request (const GstStructure * args, GError ** error); + +typedef struct +{ + const gchar *method; + const gchar *host; + gint port; + const gchar *path; + const gchar *content_type; + const gchar *body; + gsize body_length; +} HttpRequestParams; + +typedef struct +{ + gchar *body; + gsize body_length; + gint status_code; +} HttpResponse; + +static void +http_response_clear (HttpResponse * response) +{ + if (response) { + g_clear_pointer (&response->body, g_free); + response->body_length = 0; + response->status_code = 0; + } +} + +static gboolean +parse_uri (const gchar * uri, gchar ** host, gint * port, gchar ** path, + GError ** error) +{ + GUri *guri; + gboolean ret = FALSE; + + guri = g_uri_parse (uri, G_URI_FLAGS_NONE, error); + if (!guri) + return FALSE; + + *host = g_strdup (g_uri_get_host (guri)); + if (!*host) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, + "Invalid URI: missing host"); + goto cleanup; + } + + *port = g_uri_get_port (guri); + if (*port == -1) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, + "Invalid URI: missing port"); + goto cleanup; + } + + *path = g_strdup (g_uri_get_path (guri)); + if (!*path) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, + "Invalid URI: missing path"); + goto cleanup; + } + + ret = TRUE; + +cleanup: + if (!ret) { + g_clear_pointer (host, g_free); + g_clear_pointer (path, g_free); + } + g_uri_unref (guri); + return ret; +} + +static gboolean +send_http_request (const HttpRequestParams * params, HttpResponse * response, + GError ** error) +{ + GSocketClient *client = NULL; + GSocketConnection *connection = NULL; + GOutputStream *output_stream; + GInputStream *input_stream; + GString *request = NULL; + gchar *host_port = NULL; + gboolean success = FALSE; + GString *response_str = NULL; + gchar buffer[4096]; + gssize bytes_read; + + // Construct request without leading newlines + request = g_string_new (NULL); + g_string_append_printf (request, + "%s %s HTTP/1.1\r\n" + "Host: %s:%d\r\n" + "Content-Type: %s\r\n", + params->method, + params->path, params->host, params->port, params->content_type); + + if (params->body) { + g_string_append_printf (request, + "Content-Length: %zu\r\n\r\n%s\r\n", params->body_length, params->body); + } else { + g_string_append (request, "\r\n"); + } + + // Create client and establish connection + client = g_socket_client_new (); + host_port = g_strdup_printf ("%s:%d", params->host, params->port); + connection = g_socket_client_connect_to_host (client, + host_port, params->port, NULL, error); + + if (!connection) { + goto cleanup; + } + output_stream = g_io_stream_get_output_stream (G_IO_STREAM (connection)); + if (!g_output_stream_write_all (output_stream, + request->str, request->len, NULL, NULL, error)) { + goto cleanup; + } + // Read response + response_str = g_string_new (NULL); + input_stream = g_io_stream_get_input_stream (G_IO_STREAM (connection)); + while ((bytes_read = g_input_stream_read (input_stream, + buffer, sizeof (buffer), NULL, error)) > 0) { + g_string_append_len (response_str, buffer, bytes_read); + } + + if (*error) { + goto cleanup; + } + // Parse HTTP response + gchar **lines = g_strsplit (response_str->str, "\r\n", -1); + if (lines && lines[0]) { + gchar **status_parts = g_strsplit (lines[0], " ", 3); + if (status_parts && status_parts[1]) { + response->status_code = atoi (status_parts[1]); + } + g_strfreev (status_parts); + + gint i; + for (i = 0; lines[i] != NULL; i++) { + if (strlen (lines[i]) == 0 && lines[i + 1] != NULL) { + response->body = g_strdup (lines[i + 1]); + response->body_length = strlen (response->body); + break; + } + } + } + g_strfreev (lines); + + // Check if the status code indicates success (2xx) + if (response->status_code < 200 || response->status_code >= 300) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "HTTP request failed with status %d: %s", + response->status_code, + response->body ? response->body : "No error message"); + goto cleanup; + } + + success = TRUE; + +cleanup: + g_clear_pointer (&host_port, g_free); + if (request) + g_string_free (request, TRUE); + if (response_str) + g_string_free (response_str, TRUE); + g_clear_object (&connection); + g_clear_object (&client); + + return success; +} + +gboolean +run_http_request (const GstStructure * args, GError ** error) +{ + const gchar *uri; + const gchar *method; + const gchar *body; + const gchar *headers; + gchar *host = NULL; + gchar *path = NULL; + gint port; + HttpRequestParams params = { 0 }; + HttpResponse response = { 0 }; + gboolean ret = FALSE; + + g_return_val_if_fail (args != NULL, FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + // Get required parameters + uri = gst_structure_get_string (args, "uri"); + if (!uri) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, + "Missing 'uri' parameter"); + return FALSE; + } + + method = gst_structure_get_string (args, "method"); + if (!method) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, + "Missing 'method' parameter"); + return FALSE; + } + // Parse URI + if (!parse_uri (uri, &host, &port, &path, error)) + return FALSE; + + // Get optional parameters + body = gst_structure_get_string (args, "body"); + if (!gst_structure_has_field (args, "headers")) + headers = "application/json"; + else + headers = gst_structure_get_string (args, "headers"); + + // Prepare request parameters + params.method = method; + params.host = host; + params.port = port; + params.path = path; + params.content_type = headers; + params.body = body; + params.body_length = body ? strlen (body) : 0; + + // Send request + ret = send_http_request (¶ms, &response, error); + + const gchar *expected_response = + gst_structure_get_string (args, "expected-response"); + if (expected_response) { + if (g_strcmp0 (response.body, expected_response) != 0) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Expected response '%s' but got '%s'", + expected_response, + response.body ? response.body : "No error message"); + ret = FALSE; + } + } + // Cleanup + g_free (host); + g_free (path); + http_response_clear (&response); + + return ret; +} diff --git a/subprojects/gst-devtools/validate/gst/validate/gst-validate-scenario.c b/subprojects/gst-devtools/validate/gst/validate/gst-validate-scenario.c index 38345e27ee..0d1b07a8ff 100644 --- a/subprojects/gst-devtools/validate/gst/validate/gst-validate-scenario.c +++ b/subprojects/gst-devtools/validate/gst/validate/gst-validate-scenario.c @@ -66,6 +66,8 @@ #include #include +extern gboolean run_http_request (const GstStructure * args, GError ** error); + #define GST_VALIDATE_SCENARIO_DIRECTORY "scenarios" #define DEFAULT_SEEK_TOLERANCE (1 * GST_MSECOND) /* tolerance seek interval @@ -7523,6 +7525,25 @@ done: return res; } +static GstValidateExecuteActionReturn +_execute_http_request (GstValidateScenario * scenario, + GstValidateAction * action) +{ + GError *error = NULL; + GstValidateExecuteActionReturn ret = GST_VALIDATE_EXECUTE_ACTION_OK; + + if (!run_http_request (action->structure, &error)) { + GST_VALIDATE_REPORT_ACTION (scenario, action, + SCENARIO_ACTION_EXECUTION_ERROR, + "Failed to execute HTTP request: %s", + error ? error->message : "Unknown error"); + g_clear_error (&error); + ret = GST_VALIDATE_EXECUTE_ACTION_ERROR_REPORTED; + } + + return ret; +} + static GstValidateExecuteActionReturn _execute_start_http_server (GstValidateScenario * scenario, GstValidateAction * action) @@ -9150,6 +9171,61 @@ register_action_types (void) " using the `run-on-sub-pipeline` action type.", GST_VALIDATE_ACTION_TYPE_NONE); + REGISTER_ACTION_TYPE("http-request", _execute_http_request, + ((GstValidateActionParameter[]) { + { + .name = "uri", + .description = "The URI to send the request to", + .mandatory = TRUE, + .types = "string", + NULL + }, + { + .name = "method", + .description = "The HTTP method to use (GET, POST, etc)", + .mandatory = TRUE, + .types = "string", + NULL + }, + { + .name = "body", + .description = "The request body (for POST/PUT requests)", + .mandatory = FALSE, + .types = "string", + NULL + }, + { + .name = "headers", + .description = "The request headers as Content-Type", + .mandatory = FALSE, + .types = "string", + .def = "application/json", + NULL + }, + { + .name = "expected-response", + .description = "The exact expected response as a string", + .mandatory = FALSE, + .types = "string", + NULL + }, + {NULL} + }), + "Send an HTTP request to a server.\n" + "\n" + "NOTE: This is not expected to be usebale on any server but the\n" + "one started with the `start-http-server` action.\n" + "\n" + "Example:\n" + "``` jproperties\n" + "http-request,\n" + " uri=\"http://127.0.0.1:$(http_server_port)/test\",\n" + " method=POST,\n" + " body=\"test data\",\n" + " headers=\"text/plain\"\n" + "```\n", + GST_VALIDATE_ACTION_TYPE_NONE); + REGISTER_ACTION_TYPE("start-http-server", _execute_start_http_server, ((GstValidateActionParameter[]) { { diff --git a/subprojects/gst-devtools/validate/gst/validate/meson.build b/subprojects/gst-devtools/validate/gst/validate/meson.build index 2829f4cc0c..0639831030 100644 --- a/subprojects/gst-devtools/validate/gst/validate/meson.build +++ b/subprojects/gst-devtools/validate/gst/validate/meson.build @@ -1,6 +1,7 @@ runner_file = files('gst-validate-runner.c') gstvalidate_sources = files( 'gst-validate-reporter.c', + 'gst-validate-http-actions.c', 'gst-validate-mockdecryptor.c', 'gst-validate-monitor.c', 'gst-validate-element-monitor.c',