diff --git a/CMakeLists.txt b/CMakeLists.txt index 34ea7f9..5de6383 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -100,6 +100,7 @@ add_subdirectory( ut ) add_subdirectory( tools/matching-helper ) add_subdirectory( tools/arashpartow-helper ) add_subdirectory( tools/h2client ) +add_subdirectory( tools/udp-server ) ########### # Install # diff --git a/Dockerfile b/Dockerfile index 0a0a295..00df49e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,6 +22,7 @@ COPY --from=builder /code/build/${build_type}/bin/h2agent /opt/ COPY --from=builder /code/build/${build_type}/bin/h2client /opt/ COPY --from=builder /code/build/${build_type}/bin/matching-helper /opt/ COPY --from=builder /code/build/${build_type}/bin/arashpartow-helper /opt/ +COPY --from=builder /code/build/${build_type}/bin/udp-server /opt/ # We add curl & jq for helpers.src # Ubuntu has bash already installed, but vim is missing diff --git a/Dockerfile.training b/Dockerfile.training index 4306484..55b231e 100644 --- a/Dockerfile.training +++ b/Dockerfile.training @@ -34,6 +34,7 @@ RUN ln -s /opt/h2agent RUN ln -s /opt/h2client RUN ln -s /opt/matching-helper RUN ln -s /opt/arashpartow-helper +RUN ln -s /opt/udp-server ENTRYPOINT ["sleep", "infinity"] CMD [] diff --git a/README.md b/README.md index c80e897..123d54d 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,8 @@ The option `--auto` builds the builder image (`--builder-image`) , then t $> docker run --rm -it --network=host --entrypoint "/opt/matching-helper" ghcr.io/testillano/h2agent:latest --help -or- $> docker run --rm -it --network=host --entrypoint "/opt/arashpartow-helper" ghcr.io/testillano/h2agent:latest --help + -or- + $> docker run --rm -it --entrypoint "/opt/udp-server" ghcr.io/testillano/h2agent:latest --help ``` * Run within `kubernetes` deployment: corresponding `helm charts` are normally packaged into releases. This is described in ["how it is delivered"](#How-it-is-delivered) section, but in summary, you could do the following: @@ -803,7 +805,7 @@ Expression: 404 == 404 Result: 1 ``` -## Execution of h2client helper utility +## Execution of h2client utility This utility could be useful to test simple HTTP/2 requests. @@ -873,6 +875,49 @@ Uri: http://localhost:8000/book/8472098362 Response headers: [date: Sun, 27 Nov 2022 18:58:32 GMT] ``` +## Execution of udp-server utility + +This utility could be useful to test UDP messages sent by `h2agent` (`udpSocket.*` target). +You can also use netcat in bash, to generate messages easily: + +```bash +echo -n "" | nc -u -q0 -w1 -U /tmp/my_unix_socket +``` + +### Command line + +You may take a look to `udp-server` command line by just typing the build path, for example for `Release` target using native executable: + +```bash +$> ./build/Release/bin/udp-server --help +Usage: udp-server [options] + +Options: + +--path + UDP unix socket path + +--print-each + Print messages each specific amount (must be positive). Defaults to 1. + +[-h|--help] + This help. + +Examples: + udp-server --path "/tmp/my_unix_socket" +``` + +Execution example: + +```bash +$> ./build/Release/bin/udp-server --path "/tmp/my_unix_socket" + +Path: /tmp/my_unix_socket +Print each: 1 message(s) + +Waiting for messages ([sequence] ) ... +``` + ## Execution with TLS support `H2agent` server mock supports `SSL/TLS`. You may use helpers located under `tools/ssl` to create server key and certificate files: @@ -1706,7 +1751,7 @@ Defines the response behavior for an incoming request matching some basic condit }, "target": { "type": "string", - "pattern": "^response\\.body\\.(string$|hexstring$)|^response\\.body\\.json\\.(object$|object\\..+|jsonstring$|jsonstring\\..+|string$|string\\..+|integer$|integer\\..+|unsigned$|unsigned\\..+|float$|float\\..+|boolean$|boolean\\..+)|^response\\.(header\\..+|statusCode|delayMs)$|^(var|globalVar|serverEvent)\\..+|^outState(\\.(POST|GET|PUT|DELETE|HEAD)(\\..+)?)?$|^txtFile\\..+|^binFile\\..+|^break$" + "pattern": "^response\\.body\\.(string$|hexstring$)|^response\\.body\\.json\\.(object$|object\\..+|jsonstring$|jsonstring\\..+|string$|string\\..+|integer$|integer\\..+|unsigned$|unsigned\\..+|float$|float\\..+|boolean$|boolean\\..+)|^response\\.(header\\..+|statusCode|delayMs)$|^(var|globalVar|serverEvent)\\..+|^outState(\\.(POST|GET|PUT|DELETE|HEAD)(\\..+)?)?$|^txtFile\\..+|^binFile\\..+|^udpSocket\\..+|^break$" } }, "additionalProperties" : { @@ -2027,8 +2072,12 @@ The **target** of information is classified after parsing the following possible - txtFile.`` *[string]*: dumps source (as string) over text file with the path provided. The path can be relative (to the execution directory) or absolute, and **admits variables substitution**. Note that paths to missing directories will fail to open (the process does not create tree hierarchy). It is considered long term file (file is closed 1 second after last write, by default) when a constant path is configured, because this is normally used for specific log files. On the other hand, when any substitution may took place in the path provided (it has variables in the form `@{varname}`) it is considered as a dynamic name, so understood as short term file (file is opened, written and closed without delay, by default). **Note:** you can force short term type inserting a variable, for example with empty value: `txtFile./path/to/short-term-file.txt@{empty}`. Delays in microseconds are configurable on process startup. Check [command line](#Command-line) for `--long-term-files-close-delay-usecs` and `--short-term-files-close-delay-usecs` options. + This target can also be used to write named pipes (previously created: `mkfifo /tmp/mypipe && chmod 0666 /tmp/mypipe`), with the following restriction: writes must close the file descriptor everytime, so long/short term delays for close operations must be zero depending on which of them applies: variable paths zeroes the delay by default, but constant ones shall be zeroed too by command-line (`--long-term-files-close-delay-usecs 0`). Just like with regular UNIX pipes (`|`), when the writer closes, the pipe is torn down, so fast operations writting named pipes could provoke data looses (some writes missed). In that case, it is more recommended to use UDP unix sockets target (`udpSocket./tmp/myunixsocket`). + - binFile.`` *[string]*: same as `txtFile` but writting binary data. +- udpSocket.`[.]` *[string]*: sends source (as string) towards the UDP unix socket with the path provided, with an optional delay in milliseconds (if path contains dots, an unexpected delay can be configured and same could happen for the path, so in that case, you must force delay specification by mean adding `.0` or any valid value). The path can be relative (to the execution directory) or absolute, and **admits variables substitution**. UDP is a transport layer protocol in the TCP/IP suite, which provides a simple, connectionless, and unreliable communication service. It is a lightweight protocol that does not guarantee the delivery or order of data packets. Instead, it allows applications to send individual datagrams (data packets) to other hosts over the network without establishing a connection first. UDP is often used where low latency is crucial. In `h2agent` is useful to signal external applications to do associated tasks sharing specific data for the transactions processed. Use `./tools/udp-server` program to play with it. + - serverEvent.``: this target is always used in conjunction with `eraser` source acting as an alternative purge method to the purge `outState`. The main difference is that states-driven purge method acts over processed events key (`method` and `uri` for the provision in which the purge state is planned), so not all the test scenarios may be covered with that constraint if they need to remove events registered for different transactions. In this case, event addressing is defined by request *method* (`requestMethod`), *URI* (`requestUri`), and events *number* (`eventNumber`): events number *path* (`eventPath`) is not accepted, as this operation just remove specific events or whole history, like REST API for server-data deletion: - *requestMethod*: any supported method (*POST*, *GET*, *PUT*, *DELETE*, *HEAD*). Mandatory. @@ -3048,7 +3097,7 @@ Client provisions are a fundamental part of the client mode configuration. Unlik }, "target": { "type": "string", - "pattern": "^request\\.body\\.(string$|hexstring$)|^request\\.body\\.json\\.(object$|object\\..+|jsonstring$|jsonstring\\..+|string$|string\\..+|integer$|integer\\..+|unsigned$|unsigned\\..+|float$|float\\..+|boolean$|boolean\\..+)|^request\\.(header\\..+|delayMs|timeoutMs)$|^(var|globalVar|clientEvent)\\..+|^outState$|^txtFile\\..+|^binFile\\..+" + "pattern": "^request\\.body\\.(string$|hexstring$)|^request\\.body\\.json\\.(object$|object\\..+|jsonstring$|jsonstring\\..+|string$|string\\..+|integer$|integer\\..+|unsigned$|unsigned\\..+|float$|float\\..+|boolean$|boolean\\..+)|^request\\.(header\\..+|delayMs|timeoutMs)$|^(var|globalVar|clientEvent)\\..+|^outState$|^txtFile\\..+|^binFile\\..+|^udpSocket\\..+" } }, "additionalProperties" : { @@ -3071,7 +3120,7 @@ Client provisions are a fundamental part of the client mode configuration. Unlik }, "target": { "type": "string", - "pattern": "^(var|globalVar|clientEvent)\\..+|^outState$|^txtFile\\..+|^binFile\\..+|^break$" + "pattern": "^(var|globalVar|clientEvent)\\..+|^outState$|^txtFile\\..+|^binFile\\..+|^udpSocket\\..+|^break$" } }, "additionalProperties" : { @@ -3843,6 +3892,12 @@ FileSystem_observed_operations_total{operation="write"} 100000 FileSystem_observed_operations_total{operation="close"} 1 FileSystem_observed_operations_total{operation="empty"} 0 FileSystem_observed_operations_total{operation="open"} 1 +# TYPE UDPSocket_observed_operations_total counter +UDPSocket_observed_operations_total{operation="open",success="false"} 0 +UDPSocket_observed_operations_total{operation="delayedWrite"} 0 +UDPSocket_observed_operations_total{operation="instantWrite"} 0 +UDPSocket_observed_operations_total{operation="open"} 0 +UDPSocket_observed_operations_total{operation="write"} 0 # HELP AdminHttp2Server_responses_delay_seconds_gauge Http2 message responses delay gauge (seconds) in AdminHttp2Server # TYPE AdminHttp2Server_responses_delay_seconds_gauge gauge AdminHttp2Server_responses_delay_seconds_gauge 7.2e-05 @@ -3907,6 +3962,7 @@ So, metrics implemented could be divided in two categories, **counters** and **g - DELETE requests - HEAD requests - requests +- File system and Unix sockets operations - Error-condition requests (POST/GET/PUT/DELETE/HEAD/other) #### Gauges and histograms diff --git a/ct/src/conftest.py b/ct/src/conftest.py index 53cd281..a7bad70 100644 --- a/ct/src/conftest.py +++ b/ct/src/conftest.py @@ -38,6 +38,7 @@ ADMIN_CONFIGURATION_URI = ADMIN_URI_PREFIX + 'configuration' ADMIN_GLOBAL_VARIABLE_URI = ADMIN_URI_PREFIX + 'global-variable' ADMIN_FILES_URI = ADMIN_URI_PREFIX + 'files' +ADMIN_SOCKETS_URI = ADMIN_URI_PREFIX + 'udp-sockets' ADMIN_LOGGING_URI = ADMIN_URI_PREFIX + 'logging' ADMIN_SERVER_CONFIGURATION_URI = ADMIN_URI_PREFIX + 'server/configuration' ADMIN_SERVER_MATCHING_URI = ADMIN_URI_PREFIX + 'server-matching' @@ -638,6 +639,36 @@ def send(content, responseBodyRef = VALID_CLIENT_PROVISION__RESPONSE_BODY, respo } ''' +SOCKET_MANAGER_PROVISION=''' +{ + "requestMethod": "GET", + "requestUri":"/app/v1/foo/bar", + "responseCode": 200, + "transform": [ + { + "source": "value./tmp/my_unix_socket1", + "target": "var.file1" + }, + { + "source": "value./tmp/my_unix_socket2", + "target": "var.file2" + }, + { + "source": "value.0", + "target": "var.delayms" + }, + { + "source": "value.hello", + "target": "udpSocket.@{file1}.@{delayms}" + }, + { + "source": "value.world", + "target": "udpSocket.@{file2}.@{delayms}" + } + ] +} +''' + GLOBAL_VARIABLE_PROVISION_TEMPLATE_GVARCREATED_GVARREMOVED_GVARANSWERED=''' {{ "requestMethod":"POST", diff --git a/ct/src/files_operation/fo_test.py b/ct/src/files_operation/fo_test.py index 4871350..83918c0 100644 --- a/ct/src/files_operation/fo_test.py +++ b/ct/src/files_operation/fo_test.py @@ -1,6 +1,6 @@ import pytest import json -import time +#import time from conftest import ADMIN_FILES_URI, string2dict, FILE_MANAGER_PROVISION @@ -26,7 +26,7 @@ def test_001_i_want_to_get_process_files(h2ac_admin, admin_server_provision, h2a # # Wait 2 seconds (long-term file closes in 1 second by default) # time.sleep(2) # - # Check file + # Check files json response = h2ac_admin.get(ADMIN_FILES_URI) responseBodyRef = [{ "bytes":5, "path": "/tmp/example.txt", "state": "closed" }] h2ac_admin.assert_response__status_body_headers(response, 200, responseBodyRef) diff --git a/ct/src/sockets_operation/so2_test.py b/ct/src/sockets_operation/so2_test.py new file mode 100644 index 0000000..680718e --- /dev/null +++ b/ct/src/sockets_operation/so2_test.py @@ -0,0 +1,26 @@ +import pytest +import json +from conftest import ADMIN_SOCKETS_URI, string2dict, SOCKET_MANAGER_PROVISION + + +@pytest.mark.admin +def test_001_i_want_to_get_process_sockets(h2ac_admin, admin_server_provision, h2ac_traffic): + + # Provision + admin_server_provision(string2dict(SOCKET_MANAGER_PROVISION)) + + # Check file before traffic: skipped because the test could re-run + #response = h2ac_admin.get(ADMIN_SOCKETS_URI) + #assert response["status"] == 204 + + # Send GET + response = h2ac_traffic.get("/app/v1/foo/bar") + h2ac_admin.assert_response__status_body_headers(response, 200, "") + + # Check sockets json + response = h2ac_admin.get(ADMIN_SOCKETS_URI) + responseBodyRef = [{ "socket":0, "path": "/tmp/my_unix_socket2" }, { "socket":0, "path": "/tmp/my_unix_socket1" }] + responseBodyRef[0]["socket"] = response["body"][0]["socket"] + responseBodyRef[1]["socket"] = response["body"][1]["socket"] + h2ac_admin.assert_response__status_body_headers(response, 200, responseBodyRef) + diff --git a/src/http2/MyAdminHttp2Server.cpp b/src/http2/MyAdminHttp2Server.cpp index ee1eda1..9de86c5 100644 --- a/src/http2/MyAdminHttp2Server.cpp +++ b/src/http2/MyAdminHttp2Server.cpp @@ -53,6 +53,7 @@ SOFTWARE. #include #include #include +#include #include @@ -570,6 +571,10 @@ void MyAdminHttp2Server::receiveGET(const std::string &uri, const std::string &p responseBody = getFileManager()->asJsonString(); statusCode = ((responseBody == "[]") ? ert::http2comm::ResponseCode::NO_CONTENT:ert::http2comm::ResponseCode::OK); // 204 or 200 } + else if (pathSuffix == "udp-sockets") { + responseBody = getSocketManager()->asJsonString(); + statusCode = ((responseBody == "[]") ? ert::http2comm::ResponseCode::NO_CONTENT:ert::http2comm::ResponseCode::OK); // 204 or 200 + } else if (pathSuffix == "logging") { responseBody = ert::tracing::Logger::levelAsString(ert::tracing::Logger::getLevel()); headers.emplace("content-type", nghttp2::asio_http2::header_value{"text/html"}); diff --git a/src/http2/MyAdminHttp2Server.hpp b/src/http2/MyAdminHttp2Server.hpp index 8103416..619ad20 100644 --- a/src/http2/MyAdminHttp2Server.hpp +++ b/src/http2/MyAdminHttp2Server.hpp @@ -52,6 +52,7 @@ namespace model class Configuration; class GlobalVariable; class FileManager; +class SocketManager; class AdminData; class MockServerData; class MockClientData; @@ -131,6 +132,13 @@ class MyAdminHttp2Server: public ert::http2comm::Http2Server return common_resources_.FileManagerPtr; } + void setSocketManager(model::SocketManager *p) { + common_resources_.SocketManagerPtr = p; + } + model::SocketManager *getSocketManager() const { + return common_resources_.SocketManagerPtr; + } + void setMetricsData(ert::metrics::Metrics *metrics, const ert::metrics::bucket_boundaries_t &responseDelaySecondsHistogramBucketBoundaries, const ert::metrics::bucket_boundaries_t &messageSizeBytesHistogramBucketBoundaries) { common_resources_.MetricsPtr = metrics; common_resources_.ResponseDelaySecondsHistogramBucketBoundaries = responseDelaySecondsHistogramBucketBoundaries; diff --git a/src/http2/MyTrafficHttp2Client.cpp b/src/http2/MyTrafficHttp2Client.cpp index 59ab743..f7d9322 100644 --- a/src/http2/MyTrafficHttp2Client.cpp +++ b/src/http2/MyTrafficHttp2Client.cpp @@ -51,6 +51,7 @@ SOFTWARE. #include #include #include +#include #include namespace h2agent diff --git a/src/http2/MyTrafficHttp2Client.hpp b/src/http2/MyTrafficHttp2Client.hpp index 636ea82..be85055 100644 --- a/src/http2/MyTrafficHttp2Client.hpp +++ b/src/http2/MyTrafficHttp2Client.hpp @@ -57,6 +57,7 @@ namespace model class Configuration; class GlobalVariable; class FileManager; +class SocketManager; class AdminData; } diff --git a/src/http2/MyTrafficHttp2Server.cpp b/src/http2/MyTrafficHttp2Server.cpp index a564558..f9a12c9 100644 --- a/src/http2/MyTrafficHttp2Server.cpp +++ b/src/http2/MyTrafficHttp2Server.cpp @@ -50,6 +50,7 @@ SOFTWARE. #include #include #include +#include #include namespace h2agent diff --git a/src/http2/MyTrafficHttp2Server.hpp b/src/http2/MyTrafficHttp2Server.hpp index d291289..8cc46ba 100644 --- a/src/http2/MyTrafficHttp2Server.hpp +++ b/src/http2/MyTrafficHttp2Server.hpp @@ -59,6 +59,7 @@ class MockClientData; class Configuration; class GlobalVariable; class FileManager; +class SocketManager; class AdminData; } diff --git a/src/jsonSchema/AdminSchemas.hpp b/src/jsonSchema/AdminSchemas.hpp index a197bbd..5a4b369 100644 --- a/src/jsonSchema/AdminSchemas.hpp +++ b/src/jsonSchema/AdminSchemas.hpp @@ -235,7 +235,7 @@ const nlohmann::json server_provision = R"( }, "target": { "type": "string", - "pattern": "^response\\.body\\.(string$|hexstring$)|^response\\.body\\.json\\.(object$|object\\..+|jsonstring$|jsonstring\\..+|string$|string\\..+|integer$|integer\\..+|unsigned$|unsigned\\..+|float$|float\\..+|boolean$|boolean\\..+)|^response\\.(header\\..+|statusCode|delayMs)$|^(var|globalVar|serverEvent)\\..+|^outState(\\.(POST|GET|PUT|DELETE|HEAD)(\\..+)?)?$|^txtFile\\..+|^binFile\\..+|^break$" + "pattern": "^response\\.body\\.(string$|hexstring$)|^response\\.body\\.json\\.(object$|object\\..+|jsonstring$|jsonstring\\..+|string$|string\\..+|integer$|integer\\..+|unsigned$|unsigned\\..+|float$|float\\..+|boolean$|boolean\\..+)|^response\\.(header\\..+|statusCode|delayMs)$|^(var|globalVar|serverEvent)\\..+|^outState(\\.(POST|GET|PUT|DELETE|HEAD)(\\..+)?)?$|^txtFile\\..+|^binFile\\..+|^udpSocket\\..+|^break$" } }, "additionalProperties" : { @@ -426,7 +426,7 @@ const nlohmann::json client_provision = R"( }, "target": { "type": "string", - "pattern": "^request\\.body\\.(string$|hexstring$)|^request\\.body\\.json\\.(object$|object\\..+|jsonstring$|jsonstring\\..+|string$|string\\..+|integer$|integer\\..+|unsigned$|unsigned\\..+|float$|float\\..+|boolean$|boolean\\..+)|^request\\.(header\\..+|delayMs|timeoutMs)$|^(var|globalVar|clientEvent)\\..+|^outState$|^txtFile\\..+|^binFile\\..+" + "pattern": "^request\\.body\\.(string$|hexstring$)|^request\\.body\\.json\\.(object$|object\\..+|jsonstring$|jsonstring\\..+|string$|string\\..+|integer$|integer\\..+|unsigned$|unsigned\\..+|float$|float\\..+|boolean$|boolean\\..+)|^request\\.(header\\..+|delayMs|timeoutMs)$|^(var|globalVar|clientEvent)\\..+|^outState$|^txtFile\\..+|^binFile\\..+|^udpSocket\\..+" } }, "additionalProperties" : { @@ -449,7 +449,7 @@ const nlohmann::json client_provision = R"( }, "target": { "type": "string", - "pattern": "^(var|globalVar|clientEvent)\\..+|^outState$|^txtFile\\..+|^binFile\\..+|^break$" + "pattern": "^(var|globalVar|clientEvent)\\..+|^outState$|^txtFile\\..+|^binFile\\..+|^udpSocket\\..+|^break$" } }, "additionalProperties" : { diff --git a/src/main.cpp b/src/main.cpp index 73de06c..a8f178a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -52,6 +52,7 @@ SOFTWARE. #include #include #include +#include #include #include #include @@ -81,6 +82,7 @@ boost::asio::io_service *myTimersIoService = nullptr; h2agent::model::Configuration* myConfiguration = nullptr; h2agent::model::GlobalVariable* myGlobalVariable = nullptr; h2agent::model::FileManager* myFileManager = nullptr; +h2agent::model::SocketManager* mySocketManager = nullptr; h2agent::model::MockServerData* myMockServerData = nullptr; h2agent::model::MockClientData* myMockClientData = nullptr; ert::metrics::Metrics *myMetrics = nullptr; @@ -190,6 +192,9 @@ void stopAgent() delete(myFileManager); myFileManager = nullptr; + delete(mySocketManager); + mySocketManager = nullptr; + delete(myGlobalVariable); myGlobalVariable = nullptr; @@ -461,6 +466,7 @@ int main(int argc, char* argv[]) myConfiguration = new h2agent::model::Configuration(); myGlobalVariable = new h2agent::model::GlobalVariable(); myFileManager = new h2agent::model::FileManager(myTimersIoService); + mySocketManager = new h2agent::model::SocketManager(myTimersIoService); // Parse command-line /////////////////////////////////////////////////////////////////////////////////////// bool ipv6 = false; // ipv4 by default @@ -810,6 +816,9 @@ int main(int argc, char* argv[]) // FileManager/SafeFile metrics myFileManager->enableMetrics(myMetrics); + // SocketManager/SafeSocket metrics + mySocketManager->enableMetrics(myMetrics); + // Admin server myAdminHttp2Server = new h2agent::http2::MyAdminHttp2Server(ADMIN_SERVER_WORKER_THREADS); myAdminHttp2Server->enableMetrics(myMetrics); @@ -818,6 +827,7 @@ int main(int argc, char* argv[]) myAdminHttp2Server->setConfiguration(myConfiguration); myAdminHttp2Server->setGlobalVariable(myGlobalVariable); myAdminHttp2Server->setFileManager(myFileManager); + myAdminHttp2Server->setSocketManager(mySocketManager); myAdminHttp2Server->setMetricsData(myMetrics, responseDelaySecondsHistogramBucketBoundaries, messageSizeBytesHistogramBucketBoundaries); // for client connection class // Timers thread: diff --git a/src/model/AdminClientProvision.cpp b/src/model/AdminClientProvision.cpp index 0e40082..88d0644 100644 --- a/src/model/AdminClientProvision.cpp +++ b/src/model/AdminClientProvision.cpp @@ -55,6 +55,7 @@ SOFTWARE. #include #include #include +#include #include @@ -161,7 +162,7 @@ void AdminClientProvision::transform( std::string &requestMethod, } } - // TARGETS: ResponseBodyString, ResponseBodyHexString, ResponseBodyJson_String, ResponseBodyJson_Integer, ResponseBodyJson_Unsigned, ResponseBodyJson_Float, ResponseBodyJson_Boolean, ResponseBodyJson_Object, ResponseBodyJson_JsonString, ResponseHeader, ResponseStatusCode, ResponseDelayMs, TVar, TGVar, OutState, TTxtFile, TBinFile, ServerEventToPurge, Break + // TARGETS: ResponseBodyString, ResponseBodyHexString, ResponseBodyJson_String, ResponseBodyJson_Integer, ResponseBodyJson_Unsigned, ResponseBodyJson_Float, ResponseBodyJson_Boolean, ResponseBodyJson_Object, ResponseBodyJson_JsonString, ResponseHeader, ResponseStatusCode, ResponseDelayMs, TVar, TGVar, OutState, TTxtFile, TBinFile, UDPSocket, ServerEventToPurge, Break if (!processTargets(transformation, sourceVault, variables, matches, eraser, hasFilter, responseStatusCode, responseBodyJson, responseBody, responseHeaders, responseDelayMs, outState, outStateMethod, outStateUri, breakCondition)) { LOGDEBUG(ert::tracing::Logger::debug("Transformation item skipped on target", ERT_FILE_LOCATION)); continue; diff --git a/src/model/AdminClientProvision.hpp b/src/model/AdminClientProvision.hpp index 7fd3bb0..6bf84ea 100644 --- a/src/model/AdminClientProvision.hpp +++ b/src/model/AdminClientProvision.hpp @@ -61,6 +61,7 @@ class MockServerData; class Configuration; class GlobalVariable; class FileManager; +class SocketManager; class AdminClientProvision @@ -93,6 +94,7 @@ class AdminClientProvision model::Configuration *configuration_{}; // just in case it is used model::GlobalVariable *global_variable_{}; // just in case it is used model::FileManager *file_manager_{}; // just in case it is used + model::SocketManager *socket_manager_{}; // just in case it is used void loadTransformation(std::vector> &transformationsVector, const nlohmann::json &j); @@ -238,6 +240,14 @@ class AdminClientProvision file_manager_ = p; } + /** + * Sets the socket manager reference, + * just in case it is used in event target + */ + void setSocketManager(model::SocketManager *p) { + socket_manager_ = p; + } + // getters: /** diff --git a/src/model/AdminClientProvisionData.cpp b/src/model/AdminClientProvisionData.cpp index 75e4b8e..541c12f 100644 --- a/src/model/AdminClientProvisionData.cpp +++ b/src/model/AdminClientProvisionData.cpp @@ -91,6 +91,7 @@ AdminClientProvisionData::LoadResult AdminClientProvisionData::loadSingle(const provision->setConfiguration(cr.ConfigurationPtr); provision->setGlobalVariable(cr.GlobalVariablePtr); provision->setFileManager(cr.FileManagerPtr); + provision->setSocketManager(cr.SocketManagerPtr); provision->setMockClientData(cr.MockClientDataPtr); provision->setMockServerData(cr.MockServerDataPtr); diff --git a/src/model/AdminServerProvision.cpp b/src/model/AdminServerProvision.cpp index a8b9474..0e7150c 100644 --- a/src/model/AdminServerProvision.cpp +++ b/src/model/AdminServerProvision.cpp @@ -54,6 +54,7 @@ SOFTWARE. #include #include #include +#include #include @@ -824,6 +825,25 @@ bool AdminServerProvision::processTargets(std::shared_ptr transf file_manager_->write(target/*path*/, targetS/*data*/, false/*binary*/, (longTerm ? configuration_->getLongTermFilesCloseDelayUsecs():configuration_->getShortTermFilesCloseDelayUsecs())); } } + else if (transformation->getTargetType() == Transformation::TargetType::UDPSocket) { + // extraction + targetS = sourceVault.getString(success); + if (!success) return false; + + // assignments + // Possible delay provided in 'target': . + std::string path = target; + size_t lastDotPos = target.find_last_of("."); + unsigned int delayMs = atoi(target.substr(lastDotPos + 1).c_str()); + path = target.substr(0, lastDotPos); + + LOGDEBUG( + std::string msg = ert::tracing::Logger::asString("UDPSocket '%s' target, delayed %u milliseconds, in transformation item", path.c_str(), delayMs); + ert::tracing::Logger::debug(msg, ERT_FILE_LOCATION); + ); + + socket_manager_->write(path, targetS/*data*/, delayMs * 1000 /* usecs */); + } else if (transformation->getTargetType() == Transformation::TargetType::ServerEventToPurge) { if (!eraser) { LOGDEBUG(ert::tracing::Logger::debug("'ServerEventToPurge' target type only works with 'eraser' source type. This transformation will be ignored.", ERT_FILE_LOCATION)); @@ -1000,7 +1020,7 @@ void AdminServerProvision::transform( const std::string &requestUri, } } - // TARGETS: ResponseBodyString, ResponseBodyHexString, ResponseBodyJson_String, ResponseBodyJson_Integer, ResponseBodyJson_Unsigned, ResponseBodyJson_Float, ResponseBodyJson_Boolean, ResponseBodyJson_Object, ResponseBodyJson_JsonString, ResponseHeader, ResponseStatusCode, ResponseDelayMs, TVar, TGVar, OutState, TTxtFile, TBinFile, ServerEventToPurge, Break + // TARGETS: ResponseBodyString, ResponseBodyHexString, ResponseBodyJson_String, ResponseBodyJson_Integer, ResponseBodyJson_Unsigned, ResponseBodyJson_Float, ResponseBodyJson_Boolean, ResponseBodyJson_Object, ResponseBodyJson_JsonString, ResponseHeader, ResponseStatusCode, ResponseDelayMs, TVar, TGVar, OutState, TTxtFile, TBinFile, UDPSocket, ServerEventToPurge, Break if (!processTargets(transformation, sourceVault, variables, matches, eraser, hasFilter, responseStatusCode, responseBodyJson, responseBody, responseHeaders, responseDelayMs, outState, outStateMethod, outStateUri, breakCondition)) { LOGDEBUG(ert::tracing::Logger::debug("Transformation item skipped on target", ERT_FILE_LOCATION)); continue; diff --git a/src/model/AdminServerProvision.hpp b/src/model/AdminServerProvision.hpp index 6aff3a3..e79c1eb 100644 --- a/src/model/AdminServerProvision.hpp +++ b/src/model/AdminServerProvision.hpp @@ -64,6 +64,7 @@ class MockClientData; class Configuration; class GlobalVariable; class FileManager; +class SocketManager; class AdminServerProvision @@ -102,6 +103,7 @@ class AdminServerProvision model::Configuration *configuration_{}; // just in case it is used model::GlobalVariable *global_variable_{}; // just in case it is used model::FileManager *file_manager_{}; // just in case it is used + model::SocketManager *socket_manager_{}; // just in case it is used void loadTransformation(const nlohmann::json &j); @@ -239,6 +241,14 @@ class AdminServerProvision file_manager_ = p; } + /** + * Sets the socket manager reference, + * just in case it is used in event target + */ + void setSocketManager(model::SocketManager *p) { + socket_manager_ = p; + } + // getters: /** diff --git a/src/model/AdminServerProvisionData.cpp b/src/model/AdminServerProvisionData.cpp index be57028..76ca53c 100644 --- a/src/model/AdminServerProvisionData.cpp +++ b/src/model/AdminServerProvisionData.cpp @@ -105,6 +105,7 @@ AdminServerProvisionData::LoadResult AdminServerProvisionData::loadSingle(const provision->setConfiguration(cr.ConfigurationPtr); provision->setGlobalVariable(cr.GlobalVariablePtr); provision->setFileManager(cr.FileManagerPtr); + provision->setSocketManager(cr.SocketManagerPtr); provision->setMockServerData(cr.MockServerDataPtr); provision->setMockClientData(cr.MockClientDataPtr); diff --git a/src/model/CMakeLists.txt b/src/model/CMakeLists.txt index fe45f0c..43c93e5 100644 --- a/src/model/CMakeLists.txt +++ b/src/model/CMakeLists.txt @@ -24,6 +24,8 @@ add_library (h2agent-model ${CMAKE_CURRENT_LIST_DIR}/Configuration.cpp ${CMAKE_CURRENT_LIST_DIR}/FileManager.cpp ${CMAKE_CURRENT_LIST_DIR}/SafeFile.cpp + ${CMAKE_CURRENT_LIST_DIR}/SocketManager.cpp + ${CMAKE_CURRENT_LIST_DIR}/SafeSocket.cpp ${CMAKE_CURRENT_LIST_DIR}/DataPart.cpp ) diff --git a/src/model/FileManager.cpp b/src/model/FileManager.cpp index 2175cfe..104de83 100644 --- a/src/model/FileManager.cpp +++ b/src/model/FileManager.cpp @@ -184,7 +184,7 @@ nlohmann::json FileManager::getJson() const { std::string FileManager::asJsonString() const { - return ((size() != 0) ? getJson().dump() : "[]"); // server data is shown as an array + return ((size() != 0) ? getJson().dump() : "[]"); } diff --git a/src/model/SafeFile.hpp b/src/model/SafeFile.hpp index 1de6e2d..5ea0a1a 100644 --- a/src/model/SafeFile.hpp +++ b/src/model/SafeFile.hpp @@ -89,7 +89,6 @@ class SafeFile { * operations with the intention to reduce overhead in some scenarios. By default * it is not used (if not provided in constructor), so delay is not performed * regardless the close delay configured. - * about I/O operations. It may be 'nullptr' if no metrics are enabled. * @param mode open mode. By default, text files and append is selected. You * could anyway add other flags, for example for binary dumps: std::ios::binary */ diff --git a/src/model/SafeSocket.cpp b/src/model/SafeSocket.cpp new file mode 100644 index 0000000..051026a --- /dev/null +++ b/src/model/SafeSocket.cpp @@ -0,0 +1,132 @@ +/* + ___________________________________________ +| _ ___ _ | +| | | |__ \ | | | +| | |__ ) |__ _ __ _ ___ _ __ | |_ | +| | '_ \ / // _` |/ _` |/ _ \ '_ \| __| | HTTP/2 AGENT FOR MOCK TESTING +| | | | |/ /| (_| | (_| | __/ | | | |_ | Version 0.0.z +| |_| |_|____\__,_|\__, |\___|_| |_|\__| | https://github.com/testillano/h2agent +| __/ | | +| |___/ | +|___________________________________________| + +Licensed under the MIT License . +SPDX-License-Identifier: MIT +Copyright (c) 2021 Eduardo Ramos + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation sockets (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#include +#include + +#include + +#include +#include +#include + + +namespace h2agent +{ +namespace model +{ + + +SafeSocket::SafeSocket (SocketManager *socketManager, const std::string& path, boost::asio::io_service *timersIoService): + path_(path), + io_service_(timersIoService), + socket_manager_(socketManager) +{ + open(); +} + +void SafeSocket::delayedWrite(unsigned int writeDelayUs, const std::string &data) { + // metrics + socket_manager_->incrementObservedDelayedWriteOperationCounter(); + + //if (!io_service_) return; // protection + auto timer = std::make_shared(*io_service_, boost::posix_time::microseconds(writeDelayUs)); + timer->expires_from_now(boost::posix_time::microseconds(writeDelayUs)); + timer->async_wait([this, timer, data] (const boost::system::error_code& e) { + if( e ) return; // probably, we were cancelled (boost::asio::error::operation_aborted) + write(data); + }); +} + +bool SafeSocket::open() { + + socket_ = socket(AF_UNIX, SOCK_DGRAM, 0); + if (socket_ >= 0) { + // metrics + socket_manager_->incrementObservedOpenOperationCounter(); + + memset(&server_addr_, 0, sizeof(struct sockaddr_un)); + server_addr_.sun_family = AF_UNIX; + strcpy(server_addr_.sun_path, path_.c_str()); + } + else { + LOGWARNING(ert::tracing::Logger::warning(ert::tracing::Logger::asString("Failed to open '%s'", path_.c_str()), ERT_FILE_LOCATION)); + // metrics + socket_manager_->incrementObservedErrorOpenOperationCounter(); + return false; + } + + return true; +} + +nlohmann::json SafeSocket::getJson() const { + nlohmann::json result; + + result["path"] = path_; + result["socket"] = socket_; + + return result; +} + +void SafeSocket::write (const std::string& data, unsigned int writeDelayUs) { + + // trace data & delay + LOGDEBUG( + ert::tracing::Logger::debug(ert::tracing::Logger::asString("Data for write operation is: %s", data.c_str()), ERT_FILE_LOCATION); + if (writeDelayUs != 0) ert::tracing::Logger::debug(ert::tracing::Logger::asString("Delay for write operation is: %lu", writeDelayUs), ERT_FILE_LOCATION); + ); + + // Write socket: + //std::lock_guard lock(mutex_); + + // metrics + socket_manager_->incrementObservedWriteOperationCounter(); + + // Close socket: + if (io_service_ && writeDelayUs != 0) { + delayedWrite(writeDelayUs, data); + } + else { + sendto(socket_, data.c_str(), data.length(), 0, (struct sockaddr*)&server_addr_, sizeof(struct sockaddr_un)); + LOGDEBUG(ert::tracing::Logger::debug(ert::tracing::Logger::asString("Data written into '%s'", path_.c_str()), ERT_FILE_LOCATION)); + + // metrics + socket_manager_->incrementObservedInstantWriteOperationCounter(); + } +} + +} +} + diff --git a/src/model/SafeSocket.hpp b/src/model/SafeSocket.hpp new file mode 100644 index 0000000..11d92d2 --- /dev/null +++ b/src/model/SafeSocket.hpp @@ -0,0 +1,113 @@ +/* + ___________________________________________ +| _ ___ _ | +| | | |__ \ | | | +| | |__ ) |__ _ __ _ ___ _ __ | |_ | +| | '_ \ / // _` |/ _` |/ _ \ '_ \| __| | HTTP/2 AGENT FOR MOCK TESTING +| | | | |/ /| (_| | (_| | __/ | | | |_ | Version 0.0.z +| |_| |_|____\__,_|\__, |\___|_| |_|\__| | https://github.com/testillano/h2agent +| __/ | | +| |___/ | +|___________________________________________| + +Licensed under the MIT License . +SPDX-License-Identifier: MIT +Copyright (c) 2021 Eduardo Ramos + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation sockets (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#pragma once + +#include +#include +#include + +#include + +#include +#include +#include +#include + + +namespace h2agent +{ +namespace model +{ +class SocketManager; + +/** + * This class allows safe writting of udp sockets. + * Write procedure could be planned with a certain delay + */ +class SafeSocket { + + std::string path_; + int socket_; + struct sockaddr_un server_addr_; + + boost::asio::io_service *io_service_{}; + + SocketManager *socket_manager_{}; + + void delayedWrite(unsigned int writeDelayUs, const std::string &data); + +public: + + /** + * Constructor + * + * @param socketManager parent reference to socket manager. + * @param path socket path to write. It could be relative (to execution path) or absolute. + * @param timersIoService asio io service which will be used to delay write operations. + * By default it is not used (if not provided in constructor), so delay is not performed + * regardless the write delay configured. + */ + SafeSocket (SocketManager *socketManager, + const std::string& path, + boost::asio::io_service *timersIoService = nullptr); + + ~SafeSocket() {;} + + /** + * Open the socket for writting + * + * @return Boolean about success operation. + */ + bool open(); + + /** + * Json representation of the class instance + */ + nlohmann::json getJson() const; + + /** + * Write data to the socket. + * Write could be delayed. + * + * @param data data to write + * @param writeDelayUs delay for write operation. By default no delay is configured. + */ + void write(const std::string& data, unsigned int writeDelayUs = 0); +}; + +} +} + diff --git a/src/model/SocketManager.cpp b/src/model/SocketManager.cpp new file mode 100644 index 0000000..81266d9 --- /dev/null +++ b/src/model/SocketManager.cpp @@ -0,0 +1,128 @@ +/* + ___________________________________________ +| _ ___ _ | +| | | |__ \ | | | +| | |__ ) |__ _ __ _ ___ _ __ | |_ | +| | '_ \ / // _` |/ _` |/ _ \ '_ \| __| | HTTP/2 AGENT FOR MOCK TESTING +| | | | |/ /| (_| | (_| | __/ | | | |_ | Version 0.0.z +| |_| |_|____\__,_|\__, |\___|_| |_|\__| | https://github.com/testillano/h2agent +| __/ | | +| |___/ | +|___________________________________________| + +Licensed under the MIT License . +SPDX-License-Identifier: MIT +Copyright (c) 2021 Eduardo Ramos + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#include + +#include + +#include + +namespace h2agent +{ +namespace model +{ + + +void SocketManager::enableMetrics(ert::metrics::Metrics *metrics) { + + metrics_ = metrics; + + if (metrics_) { + ert::metrics::counter_family_ref_t cf = metrics->addCounterFamily("UDPSocket_observed_operations_total", "H2agent udp socket operations"); + observed_open_operation_counter_ = &(cf.Add({{"operation", "open"}})); + observed_write_operation_counter_ = &(cf.Add({{"operation", "write"}})); + observed_delayed_write_operation_counter_ = &(cf.Add({{"operation", "delayedWrite"}})); + observed_instant_write_operation_counter_ = &(cf.Add({{"operation", "instantWrite"}})); + observed_error_open_operation_counter_ = &(cf.Add({{"success", "false"}, {"operation", "open"}})); + } +} + +void SocketManager::incrementObservedOpenOperationCounter() { + if (metrics_) observed_open_operation_counter_->Increment(); +} + +void SocketManager::incrementObservedWriteOperationCounter() { + if (metrics_) observed_write_operation_counter_->Increment(); +} + +void SocketManager::incrementObservedDelayedWriteOperationCounter() { + if (metrics_) observed_delayed_write_operation_counter_->Increment(); +} + +void SocketManager::incrementObservedInstantWriteOperationCounter() { + if (metrics_) observed_instant_write_operation_counter_->Increment(); +} + +void SocketManager::incrementObservedErrorOpenOperationCounter() { + if (metrics_) observed_error_open_operation_counter_->Increment(); +} + +void SocketManager::write(const std::string &path, const std::string &data, unsigned int writeDelayUs) { + + std::shared_ptr safeSocket; + + auto it = get(path); + if (it != end()) { + safeSocket = it->second; + } + else { + safeSocket = std::make_shared(this, path, io_service_); + add(path, safeSocket); + } + + safeSocket->write(data, writeDelayUs); +} + +bool SocketManager::clear() +{ + bool result = (map_.size() > 0); // something deleted + + map_.clear(); // shared_ptr dereferenced too + + return result; +} + +nlohmann::json SocketManager::getJson() const { + + nlohmann::json result; + + read_guard_t guard(rw_mutex_); + + for (auto it = begin(); it != end(); it++) { + result.push_back(it->second->getJson()); + }; + + return result; +} + +std::string SocketManager::asJsonString() const { + + return ((size() != 0) ? getJson().dump() : "[]"); +} + + +} +} + diff --git a/src/model/SocketManager.hpp b/src/model/SocketManager.hpp new file mode 100644 index 0000000..ca5c9de --- /dev/null +++ b/src/model/SocketManager.hpp @@ -0,0 +1,152 @@ +/* + ___________________________________________ +| _ ___ _ | +| | | |__ \ | | | +| | |__ ) |__ _ __ _ ___ _ __ | |_ | +| | '_ \ / // _` |/ _` |/ _ \ '_ \| __| | HTTP/2 AGENT FOR MOCK TESTING +| | | | |/ /| (_| | (_| | __/ | | | |_ | Version 0.0.z +| |_| |_|____\__,_|\__, |\___|_| |_|\__| | https://github.com/testillano/h2agent +| __/ | | +| |___/ | +|___________________________________________| + +Licensed under the MIT License . +SPDX-License-Identifier: MIT +Copyright (c) 2021 Eduardo Ramos + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#pragma once + +#include + +#include + +#include +#include + + +namespace ert +{ +namespace metrics +{ +class Metrics; +} +} + +namespace h2agent +{ +namespace model +{ + +/** + * This class stores a list of safe files. + */ +class SocketManager : public Map> +{ + mutable mutex_t rw_mutex_{}; + boost::asio::io_service *io_service_{}; + + // metrics (will be passed to SafeSocket): + ert::metrics::Metrics *metrics_{}; + + ert::metrics::counter_t *observed_open_operation_counter_{}; + ert::metrics::counter_t *observed_write_operation_counter_{}; + ert::metrics::counter_t *observed_delayed_write_operation_counter_{}; + ert::metrics::counter_t *observed_instant_write_operation_counter_{}; + ert::metrics::counter_t *observed_error_open_operation_counter_{}; + +public: + /** + * Socket manager class + * + * @param timersIoService timers IO service needed to schedule delayed write operations. + * If you never schedule write operations (@see write) it may be 'nullptr'. + * @param metrics underlaying reference for SafeSocket in order to compute prometheus metrics + * about I/O operations. It may be 'nullptr' if no metrics are enabled. + * + * @see SafeSocket + */ + SocketManager(boost::asio::io_service *timersIoService = nullptr, ert::metrics::Metrics *metrics = nullptr) : io_service_(timersIoService), metrics_(metrics) {;} + ~SocketManager() = default; + + /** + * Set metrics reference + * + * @param metrics Optional metrics object to compute counters + */ + void enableMetrics(ert::metrics::Metrics *metrics); + + /** incrementObservedOpenOperationCounter */ + void incrementObservedOpenOperationCounter(); + + /** incrementObservedWriteOperationCounter */ + void incrementObservedWriteOperationCounter(); + + /** incrementObservedDelayedWriteOperationCounter */ + void incrementObservedDelayedWriteOperationCounter(); + + /** incrementObservedInstantWriteOperationCounter */ + void incrementObservedInstantWriteOperationCounter(); + + /** incrementObservedErrorOpenOperationCounter */ + void incrementObservedErrorOpenOperationCounter(); + + /** + * Write socket + * + * @param path path file to write. Can be relative (to execution directory) or absolute. + * @param data data string to write. + * @param writeDelayUs delay for write operation. + * Zero value means that no planned write is scheduled, so the socket is written instantly. + */ + void write(const std::string &path, const std::string &data, unsigned int writeDelayUs); + + /** Clears list + * + * @return Boolean about success of operation (something removed, nothing removed: already empty) + */ + bool clear(); + + /** + * Builds json document for class configuration + * + * @return Json object + */ + nlohmann::json getConfigurationJson() const; + + /** + * Builds json document for class information + * + * @return Json object + */ + nlohmann::json getJson() const; + + /** + * Json string representation for class information (json object) + * + * @return Json string representation ('[]' for empty object). + */ + std::string asJsonString() const; +}; + +} +} + diff --git a/src/model/Transformation.cpp b/src/model/Transformation.cpp index 7ffd2a9..a062f0c 100644 --- a/src/model/Transformation.cpp +++ b/src/model/Transformation.cpp @@ -333,7 +333,7 @@ bool Transformation::load(const nlohmann::json &j) { // return false; //} - // TARGET (enum TargetType { ResponseBodyString = 0, ResponseBodyHexString, ResponseBodyJson_String, ResponseBodyJson_Integer, ResponseBodyJson_Unsigned, ResponseBodyJson_Float, ResponseBodyJson_Boolean, ResponseBodyJson_Object, ResponseBodyJson_JsonString, ResponseHeader, ResponseStatusCode, ResponseDelayMs, TVar, TGVar, OutState, TTxtFile, TBinFile, ServerEventToPurge, Break };) + // TARGET (enum TargetType { ResponseBodyString = 0, ResponseBodyHexString, ResponseBodyJson_String, ResponseBodyJson_Integer, ResponseBodyJson_Unsigned, ResponseBodyJson_Float, ResponseBodyJson_Boolean, ResponseBodyJson_Object, ResponseBodyJson_JsonString, ResponseHeader, ResponseStatusCode, ResponseDelayMs, TVar, TGVar, OutState, TTxtFile, TBinFile, UDPSocket, ServerEventToPurge, Break };) target_ = ""; // empty by default (-), as many cases are only work modes and no parameters(+) are included in their transformation configuration target2_ = ""; // same @@ -364,6 +364,7 @@ bool Transformation::load(const nlohmann::json &j) { // + outState.`[POST|GET|PUT|DELETE|HEAD][.]` *[string (or number as string)]*: next processing state for specific method (virtual server data will be created if needed: this way we could modify the flow for other methods different than the one which is managing the current provision). This target **admits variables substitution** in the `uri` part. // + txtFile.`` *[string]*: dumps source (as string) over text file with the path provided. // + binFile.`` *[string]*: dumps source (as string) over binary file with the path provided. + // + udpSocket.`` *[string]*: sends source (as string) towards the UDP unix socket with the path provided. // + serverEvent.``: this target is always used in conjunction with `eraser`. // - break *[string]*: when non-empty string is transferred, the transformations list is interrupted. Empty string (or undefined source) ignores the action. // @@ -406,6 +407,9 @@ bool Transformation::load(const nlohmann::json &j) { static std::regex requestBodyJson_ObjectNode("^request.body.json.object.(.*)", std::regex::optimize); static std::regex requestBodyJson_JsonStringNode("^request.body.json.jsonstring.(.*)", std::regex::optimize); + // Only target + static std::regex udpSocket("^udpSocket.(.*)", std::regex::optimize); + // no need to try (controlled regex) //try { // SERVER_MODE @@ -558,6 +562,10 @@ bool Transformation::load(const nlohmann::json &j) { target_ = matches.str(1); target_type_ = TargetType::TBinFile; } + else if (std::regex_match(targetSpec, matches, udpSocket)) { // path file + target_ = matches.str(1); + target_type_ = TargetType::UDPSocket; + } else if (std::regex_match(targetSpec, matches, serverEvent)) { // value content target_ = matches.str(1); // i.e. requestMethod=GET&requestUri=/app/v1/foo/bar%3Fid%3D5%26name%3Dtest&eventNumber=3 target_type_ = TargetType::ServerEventToPurge; @@ -650,6 +658,9 @@ std::string Transformation::asString() const { else if (target_type_ == TargetType::TTxtFile || target_type_ == TargetType::TBinFile) { ss << " (path file)"; } + else if (target_type_ == TargetType::UDPSocket) { + ss << " ([.])"; + } if (!target_patterns_.empty()) { ss << " | target variables:"; diff --git a/src/model/Transformation.hpp b/src/model/Transformation.hpp index 7121794..be27f37 100644 --- a/src/model/Transformation.hpp +++ b/src/model/Transformation.hpp @@ -67,10 +67,10 @@ class Transformation } // Target type - enum TargetType { ResponseBodyString = 0, ResponseBodyHexString, ResponseBodyJson_String, ResponseBodyJson_Integer, ResponseBodyJson_Unsigned, ResponseBodyJson_Float, ResponseBodyJson_Boolean, ResponseBodyJson_Object, ResponseBodyJson_JsonString, ResponseHeader, ResponseStatusCode, ResponseDelayMs, TVar, TGVar, OutState, TTxtFile, TBinFile, ServerEventToPurge, Break, RequestBodyString, RequestBodyHexString, RequestBodyJson_String, RequestBodyJson_Integer, RequestBodyJson_Unsigned, RequestBodyJson_Float, RequestBodyJson_Boolean, RequestBodyJson_Object, RequestBodyJson_JsonString }; + enum TargetType { ResponseBodyString = 0, ResponseBodyHexString, ResponseBodyJson_String, ResponseBodyJson_Integer, ResponseBodyJson_Unsigned, ResponseBodyJson_Float, ResponseBodyJson_Boolean, ResponseBodyJson_Object, ResponseBodyJson_JsonString, ResponseHeader, ResponseStatusCode, ResponseDelayMs, TVar, TGVar, OutState, TTxtFile, TBinFile, UDPSocket, ServerEventToPurge, Break, RequestBodyString, RequestBodyHexString, RequestBodyJson_String, RequestBodyJson_Integer, RequestBodyJson_Unsigned, RequestBodyJson_Float, RequestBodyJson_Boolean, RequestBodyJson_Object, RequestBodyJson_JsonString }; const char* TargetTypeAsText(const TargetType & type) const { - static const char* text [] = { "ResponseBodyString", "ResponseBodyHexString", "ResponseBodyJson_String", "ResponseBodyJson_Integer", "ResponseBodyJson_Unsigned", "ResponseBodyJson_Float", "ResponseBodyJson_Boolean", "ResponseBodyJson_Object", "ResponseBodyJson_JsonString", "ResponseHeader", "ResponseStatusCode", "ResponseDelayMs", "TVar", "TGVar", "OutState", "TTxtFile", "TBinFile", "ServerEventToPurge", "Break", "RequestBodyString", "RequestBodyHexString", "RequestBodyJson_String", "RequestBodyJson_Integer", "RequestBodyJson_Unsigned", "RequestBodyJson_Float", "RequestBodyJson_Boolean", "RequestBodyJson_Object", "RequestBodyJson_JsonString" }; + static const char* text [] = { "ResponseBodyString", "ResponseBodyHexString", "ResponseBodyJson_String", "ResponseBodyJson_Integer", "ResponseBodyJson_Unsigned", "ResponseBodyJson_Float", "ResponseBodyJson_Boolean", "ResponseBodyJson_Object", "ResponseBodyJson_JsonString", "ResponseHeader", "ResponseStatusCode", "ResponseDelayMs", "TVar", "TGVar", "OutState", "TTxtFile", "TBinFile", "UDPSocket", "ServerEventToPurge", "Break", "RequestBodyString", "RequestBodyHexString", "RequestBodyJson_String", "RequestBodyJson_Integer", "RequestBodyJson_Unsigned", "RequestBodyJson_Float", "RequestBodyJson_Boolean", "RequestBodyJson_Object", "RequestBodyJson_JsonString" }; return text [type]; } @@ -112,7 +112,7 @@ class Transformation TargetType target_type_{}; std::string target_{}; // ResponseBodyJson_String/Integer/Unsigned/Float/Boolean/Object/JsonString(empty: whole, path: node), - // ResponseHeader, TVar, TGVar, OutState (foreign method part), TTxtFile(path), TBinFile (path) + // ResponseHeader, TVar, TGVar, OutState (foreign method part), TTxtFile(path), TBinFile (path), UDPSocket (path[.delayMs]) std::vector target_tokenized_{}; // ServerEventToPurge std::string target2_{}; // OutState (foreign uri part) diff --git a/src/model/common.hpp b/src/model/common.hpp index be2a77b..959d44b 100644 --- a/src/model/common.hpp +++ b/src/model/common.hpp @@ -53,6 +53,7 @@ namespace model class Configuration; class GlobalVariable; class FileManager; +class SocketManager; class MockServerData; class MockClientData; @@ -60,6 +61,7 @@ typedef struct { Configuration *ConfigurationPtr; GlobalVariable *GlobalVariablePtr; FileManager *FileManagerPtr; + SocketManager *SocketManagerPtr; MockServerData *MockServerDataPtr; MockClientData *MockClientDataPtr; ert::metrics::Metrics *MetricsPtr; diff --git a/tools/README.md b/tools/README.md index 93ed3f0..87d704e 100644 --- a/tools/README.md +++ b/tools/README.md @@ -31,6 +31,10 @@ │   ├── CMakeLists.txt │   ├── Makefile │   └── main.cpp +├── udp-server +│   ├── CMakeLists.txt +│   ├── Makefile +│   └── main.cpp ├── play-grafana.sh ├── schemas.sh ├── ssl @@ -50,6 +54,7 @@ * play-h2agent: examples and a guide through them (`play.sh`). * matching-helper: c++ utility to test regular expressions as a configuration helper. * arashpartow-helper: c++ utility to test Arash-Partow math expressions. +* udp-server: c++ utility to test UDP messages written by `h2agent` by mean `UDPSocket` target. * play-grafana.sh: prometheus server and grafana deployment to provide an `h2agent` metrics front-end. * schemas.sh: shows all the schemas available (also requests schema if configured). * ssl: utilities to create certificates and test the server with tls/ssl enabled. diff --git a/tools/helpers.src b/tools/helpers.src index c81c8ab..04f5fc3 100644 --- a/tools/helpers.src +++ b/tools/helpers.src @@ -90,6 +90,16 @@ files_configuration() { fi } +udp_sockets() { + if [ "$1" = "-h" -o "$1" = "--help" ] + then + echo "Usage: udp_sockets [-h|--help]; Gets the udp sockets processed." + return 0 + else + do_curl ${ADMIN_URL}/udp-sockets + fi +} + configuration() { if [ "$1" = "-h" -o "$1" = "--help" ] then @@ -473,6 +483,7 @@ snapshot() { global_variable_schema && json > ${dir}/global-variable_schema.json files && json > ${dir}/files.json files_configuration && json > ${dir}/files-configuration.json + udp_sockets && json > ${dir}/udp-sockets.json configuration && json > ${dir}/configuration.json server_configuration && json > ${dir}/server-configuration.json @@ -608,6 +619,7 @@ help() { global_variable -h files -h files_configuration -h + udp_sockets -h configuration -h echo echo "=== Traffic server ===" diff --git a/tools/matching-helper/main.cpp b/tools/matching-helper/main.cpp index a600319..9550569 100644 --- a/tools/matching-helper/main.cpp +++ b/tools/matching-helper/main.cpp @@ -99,9 +99,9 @@ int main(int argc, char* argv[]) progname = basename(argv[0]); // Parse command-line /////////////////////////////////////////////////////////////////////////////////////// - std::string regex; - std::string test; - std::string fmt; + std::string regex{}; + std::string test{}; + std::string fmt{}; std::string value; diff --git a/tools/udp-server/CMakeLists.txt b/tools/udp-server/CMakeLists.txt new file mode 100644 index 0000000..3031ed3 --- /dev/null +++ b/tools/udp-server/CMakeLists.txt @@ -0,0 +1,2 @@ +add_executable( udp-server main.cpp ) +target_link_libraries( udp-server PRIVATE ${CMAKE_EXE_LINKER_FLAGS} ) diff --git a/tools/udp-server/main.cpp b/tools/udp-server/main.cpp new file mode 100644 index 0000000..a488eb1 --- /dev/null +++ b/tools/udp-server/main.cpp @@ -0,0 +1,196 @@ +/* + __________________________________________________________ +| | +| _ | +| | | | +| _ _ __| |_ __ __ ___ ___ _ ____ _____ _ __ | +| | | | |/ _` | '_ \ |__| / __|/ _ \ '__\ \ / / _ \ '__| | SERVER UDP UTILITY TO TEST h2agent UDP messages +| | |_| | (_| | |_) | \__ \ __/ | \ V / __/ | | Version 0.0.z +| \__,_|\__,_| .__/ |___/\___|_| \_/ \___|_| | https://github.com/testillano/h2agent (tools/udp-server) +| | | | +| |_| | +|__________________________________________________________| + +Licensed under the MIT License . +SPDX-License-Identifier: MIT +Copyright (c) 2021 Eduardo Ramos + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#include // basename +#include +#include +#include +#include +#include + +// Standard +#include +#include +#include + +#define BUFFER_SIZE 256 + +const char* progname; + +//////////////////////////// +// Command line functions // +//////////////////////////// + +void usage(int rc) +{ + auto& ss = (rc == 0) ? std::cout : std::cerr; + + ss << "Usage: " << progname << " [options]\n\nOptions:\n\n" + + << "--path \n" + << " UDP unix socket path.\n\n" + + << "--print-each \n" + << " Print messages each specific amount (must be positive). Defaults to 1.\n\n" + + << "[-h|--help]\n" + << " This help.\n\n" + + << "Examples: " << '\n' + << " " << progname << " --path \"/tmp/my_unix_socket\"" << '\n' + + << '\n'; + + exit(rc); +} + +bool cmdOptionExists(char** begin, char** end, const std::string& option, + std::string& value) +{ + char** itr = std::find(begin, end, option); + bool exists = (itr != end); + + if (exists && ++itr != end) + { + value = *itr; + } + + return exists; +} + +void sighndl(int signal) +{ + std::cout << "Signal received: " << signal << std::endl; + //close(sockfd); + //unlink(path.c_str()); + exit(EXIT_FAILURE); +} + + +/////////////////// +// MAIN FUNCTION // +/////////////////// + +int main(int argc, char* argv[]) +{ + progname = basename(argv[0]); + + // Parse command-line /////////////////////////////////////////////////////////////////////////////////////// + std::string path{}; + std::string printEach{}; + + std::string value; + + if (cmdOptionExists(argv, argv + argc, "-h", value) + || cmdOptionExists(argv, argv + argc, "--help", value)) + { + usage(EXIT_SUCCESS); + } + + if (cmdOptionExists(argv, argv + argc, "--path", value)) + { + path = value; + } + + if (cmdOptionExists(argv, argv + argc, "--print-each", value)) + { + printEach = value; + } + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////// + std::cout << '\n'; + + if (path.empty()) usage(EXIT_FAILURE); + int i_printEach = (printEach.empty() ? 1:atoi(printEach.c_str())); + if (i_printEach <= 0) usage(EXIT_FAILURE); + + std::cout << "Path: " << path << '\n'; + std::cout << "Print each: " << i_printEach << " message(s)\n"; + + int sockfd = socket(AF_UNIX, SOCK_DGRAM, 0); + if (sockfd < 0) { + perror("Error creating UDP socket !"); + exit(EXIT_FAILURE); + } + + struct sockaddr_un serverAddr; + memset(&serverAddr, 0, sizeof(struct sockaddr_un)); + serverAddr.sun_family = AF_UNIX; + strcpy(serverAddr.sun_path, path.c_str()); + + unlink(path.c_str()); // just in case + + // Capture TERM/INT signals for graceful exit: + signal(SIGTERM, sighndl); + signal(SIGINT, sighndl); + + if (bind(sockfd, (struct sockaddr*)&serverAddr, sizeof(struct sockaddr_un)) < 0) { + perror("Error binding UDP socket !"); + close(sockfd); + exit(EXIT_FAILURE); + } + + char buffer[BUFFER_SIZE]; + ssize_t bytesRead; + struct sockaddr_un clientAddr; + socklen_t clientAddrLen; + + std::cout << std::endl << "Waiting for messages ([sequence] ) ..." << '\n'; + int sequence = 1; + std::string message; + while ((bytesRead = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&clientAddr, &clientAddrLen)) > 0) { + + buffer[bytesRead] = '\0'; // Agregar terminador nulo al final del texto leído + message.assign(buffer); + + if (sequence % i_printEach == 0) { + std::cout << "[" << sequence << "] " << message << std::endl; + } + sequence++; + + // exit condition: + if (message == "EOF") { + std::cout<< std::endl << "Existing (EOF received) !" << '\n'; + break; + } + } + + close(sockfd); + unlink(path.c_str()); + + exit(EXIT_SUCCESS); +} + diff --git a/ut/http2Client/http2Client_test.cpp b/ut/http2Client/http2Client_test.cpp index 0698cdd..5043878 100644 --- a/ut/http2Client/http2Client_test.cpp +++ b/ut/http2Client/http2Client_test.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -90,6 +91,7 @@ class XXXX_http2Server_test : public ::testing::Test h2agent::model::Configuration* configuration_{}; h2agent::model::GlobalVariable* global_variable_{}; h2agent::model::FileManager* file_manager_{}; + h2agent::model::SocketManager* socket_manager_{}; h2agent::model::MockServerData* mock_server_events_data_{}; h2agent::model::MockClientData* mock_client_events_data_{}; ert::metrics::Metrics* metrics_{}; @@ -101,8 +103,10 @@ class XXXX_http2Server_test : public ::testing::Test configuration_ = new h2agent::model::Configuration(); global_variable_ = new h2agent::model::GlobalVariable(); file_manager_ = new h2agent::model::FileManager(timers_io_service_); + socket_manager_ = new h2agent::model::SocketManager(timers_io_service_); metrics_ = new ert::metrics::Metrics(); file_manager_->enableMetrics(metrics_); + socket_manager_->enableMetrics(metrics_); mock_server_events_data_ = new h2agent::model::MockServerData(); mock_client_events_data_ = new h2agent::model::MockClientData(); @@ -112,6 +116,7 @@ class XXXX_http2Server_test : public ::testing::Test admin_http2_server_->setConfiguration(configuration_); admin_http2_server_->setGlobalVariable(global_variable_); admin_http2_server_->setFileManager(file_manager_); + admin_http2_server_->setSocketManager(socket_manager_); admin_http2_server_->setMockServerData(mock_server_events_data_); // stored at administrative class to pass through created server provisions admin_http2_server_->setMockClientData(mock_client_events_data_); // stored at administrative class to pass through created client provisions admin_http2_server_->enableMetrics(metrics_); @@ -147,6 +152,7 @@ class XXXX_http2Server_test : public ::testing::Test delete(configuration_); delete(global_variable_); delete(file_manager_); + delete(socket_manager_); delete(mock_server_events_data_); delete(mock_client_events_data_); admin_http2_server_->stop(); diff --git a/ut/http2Server/http2Server_test.cpp b/ut/http2Server/http2Server_test.cpp index d118696..f723d8a 100644 --- a/ut/http2Server/http2Server_test.cpp +++ b/ut/http2Server/http2Server_test.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -175,6 +176,7 @@ class http2Server_test : public ::testing::Test h2agent::model::Configuration* configuration_{}; h2agent::model::GlobalVariable* global_variable_{}; h2agent::model::FileManager* file_manager_{}; + h2agent::model::SocketManager* socket_manager_{}; h2agent::model::MockServerData* mock_server_events_data_{}; h2agent::model::MockClientData* mock_client_events_data_{}; ert::metrics::Metrics* metrics_{}; @@ -186,8 +188,10 @@ class http2Server_test : public ::testing::Test configuration_ = new h2agent::model::Configuration(); global_variable_ = new h2agent::model::GlobalVariable(); file_manager_ = new h2agent::model::FileManager(timers_io_service_); + socket_manager_ = new h2agent::model::SocketManager(timers_io_service_); metrics_ = new ert::metrics::Metrics(); file_manager_->enableMetrics(metrics_); + socket_manager_->enableMetrics(metrics_); mock_server_events_data_ = new h2agent::model::MockServerData(); mock_client_events_data_ = new h2agent::model::MockClientData(); @@ -197,6 +201,7 @@ class http2Server_test : public ::testing::Test admin_http2_server_->setConfiguration(configuration_); admin_http2_server_->setGlobalVariable(global_variable_); admin_http2_server_->setFileManager(file_manager_); + admin_http2_server_->setSocketManager(socket_manager_); admin_http2_server_->setMockServerData(mock_server_events_data_); // stored at administrative class to pass through created server provisions admin_http2_server_->setMockClientData(mock_client_events_data_); // stored at administrative class to pass through created client provisions admin_http2_server_->enableMetrics(metrics_); @@ -232,6 +237,7 @@ class http2Server_test : public ::testing::Test delete(configuration_); delete(global_variable_); delete(file_manager_); + delete(socket_manager_); delete(mock_server_events_data_); delete(mock_client_events_data_); admin_http2_server_->stop(); diff --git a/ut/model/CMakeLists.txt b/ut/model/CMakeLists.txt index 752512f..3c1e30e 100644 --- a/ut/model/CMakeLists.txt +++ b/ut/model/CMakeLists.txt @@ -6,3 +6,4 @@ add_subdirectory(Transformation) add_subdirectory(Storage) add_subdirectory(Configuration) add_subdirectory(FileSystem) +add_subdirectory(UnixSockets) diff --git a/ut/model/Transformation/transform.cpp b/ut/model/Transformation/transform.cpp index f9fb217..657db17 100644 --- a/ut/model/Transformation/transform.cpp +++ b/ut/model/Transformation/transform.cpp @@ -1,3 +1,7 @@ +#include +#include +#include + #include #include #include @@ -16,6 +20,7 @@ #include #include #include +#include #include #include @@ -227,6 +232,7 @@ class Transform_test : public ::testing::Test common_resources_.ConfigurationPtr = new h2agent::model::Configuration(); common_resources_.GlobalVariablePtr = new h2agent::model::GlobalVariable(); common_resources_.FileManagerPtr = new h2agent::model::FileManager(nullptr); + common_resources_.SocketManagerPtr = new h2agent::model::SocketManager(nullptr); common_resources_.MockServerDataPtr = new h2agent::model::MockServerData(); // Global variables: @@ -256,6 +262,7 @@ class Transform_test : public ::testing::Test delete(common_resources_.ConfigurationPtr); delete(common_resources_.GlobalVariablePtr); delete(common_resources_.FileManagerPtr); + delete(common_resources_.SocketManagerPtr); delete(common_resources_.MockServerDataPtr); } }; @@ -1574,6 +1581,53 @@ TEST_F(Transform_test, TargetBinFile) EXPECT_EQ(value2, value); } +TEST_F(Transform_test, TargetUDPSocket) +{ + // Socket path: + std::string socketPath = "/tmp/h2agent.UT.Transform_test.TargetUDPSocket"; + std::string targetValue = "udpSocket."; + targetValue += socketPath; + targetValue += ".0"; + + // Build test provision: + nlohmann::json item = R"({ "source": "value.hello", "target": "tobereplaced" })"_json; + item["target"] = targetValue; + + server_provision_json_["transform"].push_back(item); + + // Prepare UDP socket for reception: + int sockfd = socket(AF_UNIX, SOCK_DGRAM, 0); + ASSERT_FALSE(sockfd < 0); + + struct sockaddr_un serverAddr; + memset(&serverAddr, 0, sizeof(struct sockaddr_un)); + serverAddr.sun_family = AF_UNIX; + strcpy(serverAddr.sun_path, socketPath.c_str()); + + unlink(socketPath.c_str()); // just in case, it exists + + int bindrc = bind(sockfd, (struct sockaddr*)&serverAddr, sizeof(struct sockaddr_un)); + if (bindrc < 0) close(sockfd); + ASSERT_FALSE(bindrc < 0); + + // Run transformation: + provisionAndTransform(request_body_.dump()); + + // Validations: + EXPECT_EQ(status_code_, 200); + + // Check socket content: + char buffer[32]; + struct sockaddr_un clientAddr; + socklen_t clientAddrLen; + + ssize_t bytesRead = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&clientAddr, &clientAddrLen); + ASSERT_TRUE(bytesRead > 0); + buffer[bytesRead] = '\0'; + std::string msg; + msg.assign(buffer); + EXPECT_EQ(msg, "hello"); +} TEST_F(Transform_test, TargetBreak) { diff --git a/ut/model/UnixSockets/CMakeLists.txt b/ut/model/UnixSockets/CMakeLists.txt new file mode 100644 index 0000000..67fcc97 --- /dev/null +++ b/ut/model/UnixSockets/CMakeLists.txt @@ -0,0 +1,5 @@ +target_sources( unit-test +PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/safeSocket.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/socketManager.cpp +) diff --git a/ut/model/UnixSockets/safeSocket.cpp b/ut/model/UnixSockets/safeSocket.cpp new file mode 100644 index 0000000..f3df2fe --- /dev/null +++ b/ut/model/UnixSockets/safeSocket.cpp @@ -0,0 +1,131 @@ +#include +#include +#include + +#include + +#include + +#include +#include + +// SafeSocket json example template: +nlohmann::json SafeSocketJson = R"( +{ + "path": "/tmp/h2agent.ut.safesocket.txt", + "socket": 0 +} +)"_json; + +// SafeSocket content example: +std::string SafeSocketContent = "hi"; + +class SafeSocket_test : public ::testing::Test +{ +public: + h2agent::model::SocketManager *socket_manager_{}; + boost::asio::io_service *timers_io_service_{}; + std::thread *timers_thread_{}; + + SafeSocket_test() { + socket_manager_ = new h2agent::model::SocketManager(); // no metrics by default + timers_io_service_ = new boost::asio::io_service(); + timers_thread_ = new std::thread([&] { + boost::asio::io_service::work work(*timers_io_service_); + timers_io_service_->run(); + }); + } + + ~SafeSocket_test() { + timers_io_service_->stop(); + delete(timers_io_service_); + delete(timers_thread_); + } +}; + +TEST_F(SafeSocket_test, SafeSocketWithWriteDelayed) +{ + // Socket path: + std::string socketPath = SafeSocketJson["path"]; + + // Create socket to receive: + int sockfd = socket(AF_UNIX, SOCK_DGRAM, 0); + ASSERT_FALSE(sockfd < 0); + + struct sockaddr_un serverAddr; + memset(&serverAddr, 0, sizeof(struct sockaddr_un)); + serverAddr.sun_family = AF_UNIX; + strcpy(serverAddr.sun_path, socketPath.c_str()); + + unlink(socketPath.c_str()); // just in case, it exists + + int bindrc = bind(sockfd, (struct sockaddr*)&serverAddr, sizeof(struct sockaddr_un)); + if (bindrc < 0) close(sockfd); + ASSERT_FALSE(bindrc < 0); + + // Open socket to write: + h2agent::model::SafeSocket socket(socket_manager_, socketPath, timers_io_service_); + socket.write(SafeSocketContent, 5000 /* write delay value */); + + // Written after 5000 usecs (5 ms), we wait 50 ms (10x !) to ensure it is written: + boost::asio::deadline_timer exitTimer(*timers_io_service_, boost::posix_time::milliseconds(50)); + exitTimer.async_wait([&] (const boost::system::error_code& e) { timers_io_service_->stop(); }); + timers_thread_->join(); + + // Check socket content: + char buffer[32]; + struct sockaddr_un clientAddr; + socklen_t clientAddrLen; + + ssize_t bytesRead = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&clientAddr, &clientAddrLen); + ASSERT_TRUE(bytesRead > 0); + buffer[bytesRead] = '\0'; + EXPECT_EQ(buffer, SafeSocketContent); + + // Check json representation: + SafeSocketJson["socket"] = socket.getJson()["socket"]; // unpredictable + EXPECT_EQ(socket.getJson(), SafeSocketJson); +} + +TEST_F(SafeSocket_test, SafeSocketWithInstantWrite) +{ + // Stop timers service: + timers_io_service_->stop(); + timers_thread_->join(); + + // Socket path: + std::string socketPath = SafeSocketJson["path"]; + + // Create socket to receive: + int sockfd = socket(AF_UNIX, SOCK_DGRAM, 0); + ASSERT_FALSE(sockfd < 0); + + struct sockaddr_un serverAddr; + memset(&serverAddr, 0, sizeof(struct sockaddr_un)); + serverAddr.sun_family = AF_UNIX; + strcpy(serverAddr.sun_path, socketPath.c_str()); + + unlink(socketPath.c_str()); // just in case, it exists + + int bindrc = bind(sockfd, (struct sockaddr*)&serverAddr, sizeof(struct sockaddr_un)); + if (bindrc < 0) close(sockfd); + ASSERT_FALSE(bindrc < 0); + + // Open socket to write: + h2agent::model::SafeSocket socket(socket_manager_, socketPath, nullptr /* no timers io service will be used */); + socket.write(SafeSocketContent); + + // Check socket content: + char buffer[32]; + struct sockaddr_un clientAddr; + socklen_t clientAddrLen; + + ssize_t bytesRead = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&clientAddr, &clientAddrLen); + ASSERT_TRUE(bytesRead > 0); + buffer[bytesRead] = '\0'; + EXPECT_EQ(buffer, SafeSocketContent); + + // Check json representation: + SafeSocketJson["socket"] = socket.getJson()["socket"]; // unpredictable + EXPECT_EQ(socket.getJson(), SafeSocketJson); +} diff --git a/ut/model/UnixSockets/socketManager.cpp b/ut/model/UnixSockets/socketManager.cpp new file mode 100644 index 0000000..3a2fbff --- /dev/null +++ b/ut/model/UnixSockets/socketManager.cpp @@ -0,0 +1,108 @@ +#include +#include +#include + +#include + +#include + +#include +#include + +// SocketManager json example template: +nlohmann::json SocketManagerJson = R"( +[ + { + "socket": 0, + "path": "/tmp/h2agent.ut.Beethoven.txt" + }, + { + "socket": 0, + "path": "/tmp/h2agent.ut.Mozart.txt" + } +] +)"_json; + +// Target file content example: +std::string SocketManagerSafeSocketContent1 = "hi"; +std::string SocketManagerSafeSocketContent2 = "bye"; + +class SocketManager_test : public ::testing::Test +{ +public: + + SocketManager_test() { + ; + } + + ~SocketManager_test() { + } +}; + +TEST_F(SocketManager_test, SocketManager) +{ + h2agent::model::SocketManager sm(nullptr); + sm.enableMetrics(nullptr); + + // Socket paths: + std::string path1, path2; + path1 = SocketManagerJson[0]["path"]; + path2 = SocketManagerJson[1]["path"]; + + // Create sockets to receive: + int sockfd1 = socket(AF_UNIX, SOCK_DGRAM, 0); + ASSERT_FALSE(sockfd1 < 0); + int sockfd2 = socket(AF_UNIX, SOCK_DGRAM, 0); + ASSERT_FALSE(sockfd2 < 0); + + struct sockaddr_un serverAddr1, serverAddr2; + memset(&serverAddr1, 0, sizeof(struct sockaddr_un)); + serverAddr1.sun_family = AF_UNIX; + strcpy(serverAddr1.sun_path, path1.c_str()); + memset(&serverAddr2, 0, sizeof(struct sockaddr_un)); + serverAddr2.sun_family = AF_UNIX; + strcpy(serverAddr2.sun_path, path2.c_str()); + + unlink(path1.c_str()); // just in case, it exists + unlink(path2.c_str()); // just in case, it exists + + int bindrc1 = bind(sockfd1, (struct sockaddr*)&serverAddr1, sizeof(struct sockaddr_un)); + if (bindrc1 < 0) close(sockfd1); + ASSERT_FALSE(bindrc1 < 0); + + int bindrc2 = bind(sockfd2, (struct sockaddr*)&serverAddr2, sizeof(struct sockaddr_un)); + if (bindrc2 < 0) close(sockfd2); + ASSERT_FALSE(bindrc2 < 0); + + // write: + sm.write(path2, SocketManagerSafeSocketContent2, 0); + sm.write(path1, SocketManagerSafeSocketContent1, 0); + + // read: + // Check socket content: + char buffer[32]; + struct sockaddr_un clientAddr; + socklen_t clientAddrLen; + + ssize_t bytesRead = recvfrom(sockfd1, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&clientAddr, &clientAddrLen); + ASSERT_TRUE(bytesRead > 0); + buffer[bytesRead] = '\0'; + EXPECT_EQ(buffer, SocketManagerSafeSocketContent1); + + bytesRead = recvfrom(sockfd2, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&clientAddr, &clientAddrLen); + ASSERT_TRUE(bytesRead > 0); + buffer[bytesRead] = '\0'; + EXPECT_EQ(buffer, SocketManagerSafeSocketContent2); + + // Check json representation: + SocketManagerJson[0]["socket"] = sm.getJson()[0]["socket"]; + SocketManagerJson[1]["socket"] = sm.getJson()[1]["socket"]; + EXPECT_EQ(sm.getJson(), SocketManagerJson); + + // clear: + sm.clear(); + + // Check empty: + EXPECT_EQ(sm.asJsonString(), "[]"); +} +