diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1b19217 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +__pycache__ +*.pyc +build +install diff --git a/README.md b/README.md index e673ad6..a1a20cd 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ This AF uses the [Open5GS](https://open5gs.org/) framework to implement the netw ## Specifications -A list of specification related to this repository is available [here](https://github.com/5G-MAG/Standards/wiki/5G-Media-Streaming-Architecture-(5GMS):-Relevant-Specifications). +A list of specification related to this repository is available [here](https://github.com/5G-MAG/Standards/wiki/5G-Media-Streaming-Architecture-(5GMS\):-Relevant-Specifications). ## Install dependencies @@ -77,6 +77,8 @@ specify an alternative configuration file. For example: The source example configuration file can be found in `~/rt-5gms-application-function/src/5gmsaf/msaf.yaml`. +Also see the [Configuring the Application Function](https://github.com/5G-MAG/rt-5gms-application-function/wiki/Configuring-the-Application-Function) wiki page for details on configuration. + ## Testing See the section on [Testing](https://github.com/5G-MAG/rt-5gms-application-function/wiki/Developing-and-Contributing#testing) in the wiki. diff --git a/examples/Certificates.json b/examples/Certificates.json deleted file mode 100644 index c9bc6a4..0000000 --- a/examples/Certificates.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "testcert1": "certificate-1.pem" -} diff --git a/examples/ContentHostingConfiguration_Big-Buck-Bunny_pull-ingest_domain-name_http_and_https.json b/examples/ContentHostingConfiguration_Big-Buck-Bunny_pull-ingest_domain-name_http_and_https.json deleted file mode 100644 index 501a5fd..0000000 --- a/examples/ContentHostingConfiguration_Big-Buck-Bunny_pull-ingest_domain-name_http_and_https.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "Big Buck Bunny", - "entryPointPath": "BigBuckBunny_4s_onDemand_2014_05_09.mpd", - "ingestConfiguration": { - "pull": true, - "protocol": "urn:3gpp:5gms:content-protocol:http-pull-ingest", - "baseURL": "https://ftp.itec.aau.at/datasets/DASHDataset2014/BigBuckBunny/4sec/" - }, - "distributionConfigurations": [ - { - "domainNameAlias": "media.example.com" - }, - { - "domainNameAlias": "media.example.com", - "certificateId": "testcert1" - } - ] -} diff --git a/examples/ContentHostingConfiguration_Big-Buck-Bunny_pull-ingest_http_and_https.json b/examples/ContentHostingConfiguration_Big-Buck-Bunny_pull-ingest_http_and_https.json.tmpl similarity index 89% rename from examples/ContentHostingConfiguration_Big-Buck-Bunny_pull-ingest_http_and_https.json rename to examples/ContentHostingConfiguration_Big-Buck-Bunny_pull-ingest_http_and_https.json.tmpl index 0bf4390..f0e0c42 100644 --- a/examples/ContentHostingConfiguration_Big-Buck-Bunny_pull-ingest_http_and_https.json +++ b/examples/ContentHostingConfiguration_Big-Buck-Bunny_pull-ingest_http_and_https.json.tmpl @@ -9,7 +9,7 @@ "distributionConfigurations": [ {}, { - "certificateId": "testcert1" + "certificateId": "@certificate-id@" } ] } diff --git a/examples/ContentHostingConfiguration_Big-Buck-Bunny_pull-ingest_https.json b/examples/ContentHostingConfiguration_Big-Buck-Bunny_pull-ingest_https.json deleted file mode 100644 index 3752bc8..0000000 --- a/examples/ContentHostingConfiguration_Big-Buck-Bunny_pull-ingest_https.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "Big Buck Bunny", - "entryPointPath": "BigBuckBunny_4s_onDemand_2014_05_09.mpd", - "ingestConfiguration": { - "pull": true, - "protocol": "urn:3gpp:5gms:content-protocol:http-pull-ingest", - "baseURL": "https://ftp.itec.aau.at/datasets/DASHDataset2014/BigBuckBunny/4sec/" - }, - "distributionConfigurations": [ - { - "certificateId": "testcert1" - } - ] -} diff --git a/examples/ContentHostingConfiguration_Big-Buck-Bunny_pull-ingest_domain-name_http.json b/examples/ContentHostingConfiguration_Big-Buck-Bunny_pull-ingest_https.json.tmpl similarity index 88% rename from examples/ContentHostingConfiguration_Big-Buck-Bunny_pull-ingest_domain-name_http.json rename to examples/ContentHostingConfiguration_Big-Buck-Bunny_pull-ingest_https.json.tmpl index 3383f29..13c4e1e 100644 --- a/examples/ContentHostingConfiguration_Big-Buck-Bunny_pull-ingest_domain-name_http.json +++ b/examples/ContentHostingConfiguration_Big-Buck-Bunny_pull-ingest_https.json.tmpl @@ -7,6 +7,6 @@ "baseURL": "https://ftp.itec.aau.at/datasets/DASHDataset2014/BigBuckBunny/4sec/" }, "distributionConfigurations": [ - {"domainNameAlias": "media.example.com"} + { "certificateId": "@certificate-id@" } ] } diff --git a/examples/README.md b/examples/README.md index 27873cb..041b7a2 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,21 +1,6 @@ # 5G-MAG Reference Tools: 5GMS Application Function Example Configurations -This directory contains examples of configuration files for the 5GMS Application Function. - -## `Certificates.json` - -This file contains a mapping from certificate ID to certificate filename. The -certificate IDs in this file are used to find the matching certificate file -(containing a public certificate, private key and any intermediate CA -certificates) when referenced from a ContentHostingConfiguration file. - -The `subprojects/rt-common-shared/5gms/scripts/make_self_signed_certs.py` script can be used, passing a 5GMS Application Function YAML configuration file as a parameter, to create suitable self-signed certificate files for testing purposes. - -For example: -```bash -cd ~/rt-5gms-application-function -subprojects/rt-common-shared/5gms/scripts/make_self_signed_certs.py --af-conf=examples/Test_https_canonical-msaf.yaml -``` +This directory contains examples of configurations for the 5GMS Application Function. ## `ContentHostingConfiguration_Big-Buck-Bunny_pull-ingest.json` @@ -25,38 +10,11 @@ It contains a ContentHostingConfiguration, based on 3GPP TS 26.512 Release 17.3. The distribution side of the configurations tells the rt-5gms-application-function to configure a 5GMS Application Server to reverse proxy the media on its localhost (127.0.0.1) loopback interface. The distribution configuration object is left empty as this is a simple distribution where all fields are generated by the 5GMS Application Function. Note that there needs to be at least one entry in `distributionConfigurations`, even if it is an empty object. -## `ContentHostingConfiguration_Big-Buck-Bunny_pull-ingest_http_and_https.json` - -This file is an alternative to `ContentHostingConfiguration_Big-Buck-Bunny_pull-ingest.json` (see above) and can be used along with the `Certificates.json` file to configure a rt-5gms-application-function to provision a 5GMS Application Server which will provide both HTTP and HTTPS distribution points. - -## `ContentHostingConfiguration_Big-Buck-Bunny_pull-ingest_https.json` - -This file is an alternative to `ContentHostingConfiguration_Big-Buck-Bunny_pull-ingest.json` (see above) and can be used along with the `Certificates.json` file to run a rt-5gms-application-function to provision a 5GMS Application Server which will provide an HTTPS distribution point only. - -## `ContentHostingConfiguration_Big-Buck-Bunny_pull-ingest_domain-name_http.json` - -This file is the same as `ContentHostingConfiguration_Big-Buck-Bunny_pull-ingest.json` except that the distribution configuration also has "media.example.com" as a domain name alias. This file is included for testing the portions of the Application Function that should prefer the use of the domain name alias over the canonical name or vice-versa. - -## `ContentHostingConfiguration_Big-Buck-Bunny_pull-ingest_domain-name_http_and_https.json` - -This file is the same as `ContentHostingConfiguration_Big-Buck-Bunny_pull-ingest_http_and_https.json` except that the distribution configuration also has "media.example.com" as a domain name alias. This file is included for testing the portions of the Application Function that should prefer the use of the domain name alias over the canonical name or vice-versa. - -## `Test_http_canonical-msaf.yaml` - -This is a 5GMS Application Function YAML configuration file used for testing. This configuration uses the `ContentHostingConfiguration_Big-Buck-Bunny_pull-ingest.json` ContentHostingConfiguration to test http protocol distribution using `localhost` as the canonical domain name of the Application Server. - -## `Test_http_domain_alias-msaf.yaml` - -This is a 5GMS Application Function YAML configuration file used for testing. This configuration uses the `ContentHostingConfiguration_Big-Buck-Bunny_pull-ingest_domain-name_http.json` ContentHostingConfiguration to test http protocol distribution using `localhost` as the canonical domain name of the Application Server and `media.example.com` as a domain name alias for the distribution configuration. - -## `Test_https_canonical-msaf.yaml` - -This is a 5GMS Application Function YAML configuration file used for testing. This configuration uses the `ContentHostingConfiguration_Big-Buck-Bunny_pull-ingest_http_and_https.json` ContentHostingConfiguration and the `Certificates.json` certificates index file to test https protocol distribution using `localhost` as the canonical domain name of the Application Server. - -Generate the certificates before using the configuration file. See [`Certificates.json`](#certificatesjson) for more information. +## `ContentHostingConfiguration_Big-Buck-Bunny_pull-ingest_http_and_https.json.tmpl` -## `Test_https_domain_alias-msaf.yaml` +This template file is an alternative to `ContentHostingConfiguration_Big-Buck-Bunny_pull-ingest.json` (see above) and can be used to configure a rt-5gms-application-function to provision a 5GMS Application Server which will provide both HTTP and HTTPS distribution points. Before the contents of this file can be used as a configuration `@certificate-id@` must be substituted for a valid certificate id for the provisioning session you wish to add the configuration to. -This is a 5GMS Application Function YAML configuration file used for testing. This configuration uses the `ContentHostingConfiguration_Big-Buck-Bunny_pull-ingest_domain-name_http_and_https.json` ContentHostingConfiguration and the `Certificates.json` certificates index file to test https protocol distribution using `localhost` as the canonical domain name of the Application Server and `media.example.com` as a domain name alias for the distribution configurations. +## `ContentHostingConfiguration_Big-Buck-Bunny_pull-ingest_https.json.tmpl` -Generate the certificates before using the configuration file. See [`Certificates.json`](#certificatesjson) for more information. +This template file is an alternative to `ContentHostingConfiguration_Big-Buck-Bunny_pull-ingest.json` (see above) and can be used to configure a rt-5gms-application-function to provision a 5GMS Application Server which will provide an HTTPS distribution point only. Before the contents of this file can be used as a configuration `@certificate-id@` must be substituted for a valid certific +ate id for the provisioning session you wish to add the configuration to. diff --git a/examples/Test_http_canonical-msaf.yaml b/examples/Test_http_canonical-msaf.yaml deleted file mode 100644 index af4affc..0000000 --- a/examples/Test_http_canonical-msaf.yaml +++ /dev/null @@ -1,28 +0,0 @@ -logger: - level: debug - domain: msaf - -msaf: - open5gsIntegration: false - sbi: - - addr: 0.0.0.0 - port: 7778 - applicationServers: - - canonicalHostname: localhost - urlPathPrefixFormat: /m4d/provisioning-session-{provisioningSessionId}/ - m3Port: 7777 - certificate: Certificates.json - contentHostingConfiguration: ContentHostingConfiguration_Big-Buck-Bunny_pull-ingest.json - -nrf: - sbi: - - addr: - - 127.0.0.10 - - ::1 - port: 7777 - -parameter: - -max: - -time: diff --git a/examples/Test_http_domain_alias-msaf.yaml b/examples/Test_http_domain_alias-msaf.yaml deleted file mode 100644 index 7cd6dac..0000000 --- a/examples/Test_http_domain_alias-msaf.yaml +++ /dev/null @@ -1,28 +0,0 @@ -logger: - level: debug - domain: msaf - -msaf: - open5gsIntegration: false - sbi: - - addr: 0.0.0.0 - port: 7778 - applicationServers: - - canonicalHostname: localhost - urlPathPrefixFormat: /m4d/provisioning-session-{provisioningSessionId}/ - m3Port: 7777 - certificate: Certificates.json - contentHostingConfiguration: ContentHostingConfiguration_Big-Buck-Bunny_pull-ingest_domain-name_http.json - -nrf: - sbi: - - addr: - - 127.0.0.10 - - ::1 - port: 7777 - -parameter: - -max: - -time: diff --git a/examples/Test_https_canonical-msaf.yaml b/examples/Test_https_canonical-msaf.yaml deleted file mode 100644 index b00171b..0000000 --- a/examples/Test_https_canonical-msaf.yaml +++ /dev/null @@ -1,28 +0,0 @@ -logger: - level: debug - domain: msaf - -msaf: - open5gsIntegration: false - sbi: - - addr: 0.0.0.0 - port: 7778 - applicationServers: - - canonicalHostname: localhost - urlPathPrefixFormat: /m4d/provisioning-session-{provisioningSessionId}/ - m3Port: 7777 - certificate: Certificates.json - contentHostingConfiguration: ContentHostingConfiguration_Big-Buck-Bunny_pull-ingest_http_and_https.json - -nrf: - sbi: - - addr: - - 127.0.0.10 - - ::1 - port: 7777 - -parameter: - -max: - -time: diff --git a/examples/Test_https_domain_alias-msaf.yaml b/examples/Test_https_domain_alias-msaf.yaml deleted file mode 100644 index 6140003..0000000 --- a/examples/Test_https_domain_alias-msaf.yaml +++ /dev/null @@ -1,28 +0,0 @@ -logger: - level: debug - domain: msaf - -msaf: - open5gsIntegration: false - sbi: - - addr: 0.0.0.0 - port: 7778 - applicationServers: - - canonicalHostname: localhost - urlPathPrefixFormat: /m4d/provisioning-session-{provisioningSessionId}/ - m3Port: 7777 - certificate: Certificates.json - contentHostingConfiguration: ContentHostingConfiguration_Big-Buck-Bunny_pull-ingest_domain-name_http_and_https.json - -nrf: - sbi: - - addr: - - 127.0.0.10 - - ::1 - port: 7777 - -parameter: - -max: - -time: diff --git a/meson.build b/meson.build index a189662..b4eb904 100644 --- a/meson.build +++ b/meson.build @@ -6,15 +6,21 @@ # program. If this file is missing then the license can be retrieved from # https://drive.google.com/file/d/1cinCiA778IErENZ3JN52VFW-1ffHpx7Z/view -project('rt-5gms-af', 'c', - version : '1.1.0', +# Meson module fs and its functions like fs.hash_file require atleast meson 0.59.0 + +project('rt-5gms-application-function', 'c', + version : '1.2.0', license : '5G-MAG Public', - meson_version : '>= 0.47.0', + meson_version : '>= 0.59.0', default_options : [ 'c_std=gnu89', ], ) + +fiveg_api_release = '17' + sh_cmd = find_program('sh') patch_open5gs_result = run_command([sh_cmd, '-c', '"$MESON_SOURCE_ROOT/subprojects/patch_open5gs.sh" open5gs'], check: true, capture: false) open5gs_project=subproject('open5gs',required:true) +subdir('tools') subdir('src') diff --git a/src/5gmsaf/ContentProtocolsDiscovery_body.json b/src/5gmsaf/ContentProtocolsDiscovery_body.json new file mode 100644 index 0000000..824bb6c --- /dev/null +++ b/src/5gmsaf/ContentProtocolsDiscovery_body.json @@ -0,0 +1,8 @@ +{ + "downlinkIngestProtocols": [ + { + "termIdentifier": "urn:3gpp:5gms:content-protocol:http-pull-ingest", + "descriptionLocator": "https://www.etsi.org/deliver/etsi_ts/126500_126599/126512/17.03.00_60/ts_126512v170300p.pdf#page=67" + } + ] +} diff --git a/src/5gmsaf/api-info-head.mustache b/src/5gmsaf/api-info-head.mustache new file mode 100644 index 0000000..a310f9d --- /dev/null +++ b/src/5gmsaf/api-info-head.mustache @@ -0,0 +1,9 @@ +#ifndef _{{#lambda.uppercase}}{{projectName}}{{/lambda.uppercase}}_API_H +#define _{{#lambda.uppercase}}{{projectName}}{{/lambda.uppercase}}_API_H + +#define {{#lambda.uppercase}}{{projectName}}{{/lambda.uppercase}}_BASE_PATH "{{basePath}}" +#define {{#lambda.uppercase}}{{projectName}}{{/lambda.uppercase}}_API_NAME "{{appName}}" +#define {{#lambda.uppercase}}{{projectName}}{{/lambda.uppercase}}_API_VERSION "{{appVersion}}" +#define {{#lambda.uppercase}}{{projectName}}{{/lambda.uppercase}}_API_DESCRIPTION "{{appDescription}}" + +#endif /* _{{#lambda.uppercase}}{{projectName}}{{/lambda.uppercase}}_API_H */ diff --git a/src/5gmsaf/application-server-context.c b/src/5gmsaf/application-server-context.c index dabe052..6d9ebfa 100644 --- a/src/5gmsaf/application-server-context.c +++ b/src/5gmsaf/application-server-context.c @@ -8,19 +8,29 @@ program. If this file is missing then the license can be retrieved from https://drive.google.com/file/d/1cinCiA778IErENZ3JN52VFW-1ffHpx7Z/view */ -#include "application-server-context.h" +#include "certmgr.h" #include "context.h" #include "utilities.h" +#include "application-server-context.h" + +typedef struct client_request_info { + msaf_application_server_state_node_t *as_state; + purge_resource_id_node_t *purge_node; + +} client_request_info_t; + + static void application_server_state_init(msaf_application_server_node_t *msaf_as); static ogs_sbi_client_t *msaf_m3_client_init(const char *hostname, int port); -static int m3_client_as_state_requests(msaf_application_server_state_node_t *as_state, const char *type, const char *data, const char *method, const char *component); +static int +m3_client_as_state_requests(msaf_application_server_state_node_t *as_state, purge_resource_id_node_t *purge_node,const char *type, const char *data, const char *method, const char *component); static int client_notify_cb(int status, ogs_sbi_response_t *response, void *data); static void msaf_application_server_remove(msaf_application_server_node_t *msaf_as); /***** Public functions *****/ -void +int msaf_application_server_state_set_on_post( msaf_provisioning_session_t *provisioning_session) { msaf_application_server_node_t *msaf_as; @@ -33,57 +43,91 @@ msaf_application_server_state_set_on_post( msaf_provisioning_session_t *provisio msaf_as = ogs_list_first(&msaf_self()->config.applicationServers_list); ogs_assert(msaf_as); ogs_list_for_each(&msaf_self()->application_server_states, as_state){ - if(!strcmp(as_state->application_server->canonicalHostname, msaf_as->canonicalHostname)) { + if (as_state->application_server == msaf_as) { + msaf_application_server_state_ref_node_t *as_state_ref; certs = msaf_retrieve_certificates_from_map(provisioning_session); if (certs) { ogs_list_for_each_safe(certs, next_node, node) { + ogs_list_remove(certs, node); ogs_list_add(&as_state->upload_certificates, node); } ogs_free(certs); + } else { + return 0; } + chc = ogs_calloc(1, sizeof(resource_id_node_t)); ogs_assert(chc); chc->state = ogs_strdup(provisioning_session->provisioningSessionId); ogs_list_add(&as_state->upload_content_hosting_configurations, chc); + assigned_provisioning_sessions = ogs_calloc(1, sizeof(assigned_provisioning_sessions_node_t)); ogs_assert(assigned_provisioning_sessions); assigned_provisioning_sessions->assigned_provisioning_session = provisioning_session; - assigned_provisioning_sessions->assigned_provisioning_session->contentHostingConfiguration = provisioning_session->contentHostingConfiguration; ogs_list_add(&as_state->assigned_provisioning_sessions, assigned_provisioning_sessions); - ogs_list_init(&provisioning_session->msaf_application_server_state_nodes); - ogs_list_add(&provisioning_session->msaf_application_server_state_nodes, as_state); + ogs_list_init(&provisioning_session->application_server_states); + as_state_ref = ogs_calloc(1, sizeof(msaf_application_server_state_ref_node_t)); + ogs_assert(as_state_ref); + as_state_ref->as_state = as_state; + ogs_list_add(&provisioning_session->application_server_states, as_state_ref); next_action_for_application_server(as_state); } } + return 1; } void msaf_application_server_state_update( msaf_provisioning_session_t *provisioning_session) { - msaf_application_server_state_node_t *as_state; - resource_id_node_t *chc; - assigned_provisioning_sessions_node_t *assigned_provisioning_sessions; + msaf_application_server_state_ref_node_t *as_state_ref; + + ogs_list_for_each(&provisioning_session->application_server_states, as_state_ref){ + resource_id_node_t *chc; + msaf_application_server_state_node_t *as_state = as_state_ref->as_state; + ogs_list_t *certs = msaf_retrieve_certificates_from_map(provisioning_session); + if (certs) { + resource_id_node_t *next_node, *node; + ogs_list_for_each_safe(certs, next_node, node) { + int upload_cert = 1; + resource_id_node_t *cur_cert; + /* Check if the certificate is already uploaded */ + ogs_list_for_each(as_state->current_certificates, cur_cert) { + if (!strcmp(node->state, cur_cert->state)) { + upload_cert = 0; + break; + } + + } + /* If there is a new certificate for this AS, upload it */ + if (upload_cert) { + ogs_list_remove(certs, node); + ogs_list_add(&as_state->upload_certificates, node); + } + } + /* free any cert map nodes left in the list (didn't need update) */ + ogs_list_for_each_safe(certs, next_node, node) { + ogs_list_remove(certs, node); + if (node->state) ogs_free(node->state); + ogs_free(node); + } + ogs_free(certs); + } else { + continue; + } - ogs_list_for_each(&provisioning_session->msaf_application_server_state_nodes, as_state){ chc = ogs_calloc(1, sizeof(resource_id_node_t)); ogs_assert(chc); chc->state = ogs_strdup(provisioning_session->provisioningSessionId); ogs_list_add(&as_state->upload_content_hosting_configurations, chc); - ogs_list_for_each(&as_state->assigned_provisioning_sessions,assigned_provisioning_sessions){ - - assigned_provisioning_sessions->assigned_provisioning_session = provisioning_session; - assigned_provisioning_sessions->assigned_provisioning_session->contentHostingConfiguration = provisioning_session->contentHostingConfiguration; - } - next_action_for_application_server(as_state); } } -void +int msaf_application_server_state_set(msaf_application_server_state_node_t *as_state, msaf_provisioning_session_t *provisioning_session) { resource_id_node_t *chc; @@ -94,9 +138,12 @@ msaf_application_server_state_set(msaf_application_server_state_node_t *as_state certs = msaf_retrieve_certificates_from_map(provisioning_session); if (certs) { ogs_list_for_each_safe(certs, next_node, node) { + ogs_list_remove(certs, node); ogs_list_add(&as_state->upload_certificates, node); } ogs_free(certs); + } else { + return 0; } chc = ogs_calloc(1, sizeof(resource_id_node_t)); @@ -110,6 +157,8 @@ msaf_application_server_state_set(msaf_application_server_state_node_t *as_state ogs_list_add(&as_state->assigned_provisioning_sessions, assigned_provisioning_sessions); next_action_for_application_server(as_state); + + return 1; } msaf_application_server_node_t * @@ -148,19 +197,16 @@ void next_action_for_application_server(msaf_application_server_state_node_t *as ogs_assert(as_state); if (as_state->current_certificates == NULL) { - ogs_debug("M3 client: Sending GET method to Application Server [%s] to request the list of known certificates", as_state->application_server->canonicalHostname); - m3_client_as_state_requests(as_state, NULL, NULL, (char *)OGS_SBI_HTTP_METHOD_GET, "certificates"); + m3_client_as_state_requests(as_state, NULL, NULL, NULL, (char *)OGS_SBI_HTTP_METHOD_GET, "certificates"); } else if (as_state->current_content_hosting_configurations == NULL) { - ogs_debug("M3 client: Sending GET method to Application Server [%s] to request the list of known content-hosting-configurations", as_state->application_server->canonicalHostname); - m3_client_as_state_requests(as_state, NULL, NULL, (char *)OGS_SBI_HTTP_METHOD_GET, "content-hosting-configurations"); + m3_client_as_state_requests(as_state, NULL, NULL, NULL, (char *)OGS_SBI_HTTP_METHOD_GET, "content-hosting-configurations"); } else if (ogs_list_first(&as_state->upload_certificates) != NULL) { - const char *upload_cert_filename; char *upload_cert_id; char *provisioning_session; char *cert_id; - char *data; char *component; resource_id_node_t *cert_id_node; + msaf_certificate_t *certificate; resource_id_node_t *upload_cert = ogs_list_first(&as_state->upload_certificates); ogs_list_for_each(as_state->current_certificates, cert_id_node) { @@ -170,24 +216,17 @@ void next_action_for_application_server(msaf_application_server_state_node_t *as } upload_cert_id = ogs_strdup(upload_cert->state); provisioning_session = strtok_r(upload_cert_id,":",&cert_id); - upload_cert_filename = msaf_get_certificate_filename(provisioning_session, cert_id); - data = read_file(upload_cert_filename); - - if(!data) { - ogs_error("The certificate file [%s] referenced in the JSON cannot be read", upload_cert_filename); - } - + certificate = server_cert_get_servercert(cert_id); component = ogs_msprintf("certificates/%s:%s", provisioning_session, cert_id); if (cert_id_node) { ogs_debug("M3 client: Sending PUT method to Application Server [%s] for Certificate: [%s]", as_state->application_server->canonicalHostname, upload_cert->state); - m3_client_as_state_requests(as_state, "application/x-pem-file", data, (char *)OGS_SBI_HTTP_METHOD_PUT, component); - free(data); + m3_client_as_state_requests(as_state, NULL, "application/x-pem-file", certificate->certificate, (char *)OGS_SBI_HTTP_METHOD_PUT, component); } else { ogs_debug("M3 client: Sending POST method to Application Server [%s]for Certificate: [%s]", as_state->application_server->canonicalHostname, upload_cert->state); - m3_client_as_state_requests(as_state, "application/x-pem-file", data, (char *)OGS_SBI_HTTP_METHOD_POST, component); - free(data); + m3_client_as_state_requests(as_state, NULL, "application/x-pem-file", certificate->certificate, (char *)OGS_SBI_HTTP_METHOD_POST, component); } + msaf_certificate_free(certificate); ogs_free(component); ogs_free(upload_cert_id); @@ -212,28 +251,16 @@ void next_action_for_application_server(msaf_application_server_state_node_t *as chc_with_af_unique_cert_id = msaf_content_hosting_configuration_with_af_unique_cert_id(provisioning_session); json = OpenAPI_content_hosting_configuration_convertToJSON(chc_with_af_unique_cert_id); - - if(!json) { - ogs_error("OpenAPI function is not able to convert The contentHostingConfiguration to JSON"); - - } - data = cJSON_Print(json); - if(!data) { - ogs_error("The contentHostingConfiguration cannot be parsed"); - - } - - component = ogs_msprintf("content-hosting-configurations/%s", upload_chc->state); if (chc_id_node) { ogs_debug("M3 client: Sending PUT method to Application Server [%s] for Content Hosting Configuration: [%s]", as_state->application_server->canonicalHostname, upload_chc->state); - m3_client_as_state_requests(as_state, "application/json", data, (char *)OGS_SBI_HTTP_METHOD_PUT, component); + m3_client_as_state_requests(as_state, NULL, "application/json", data, (char *)OGS_SBI_HTTP_METHOD_PUT, component); } else { ogs_debug("M3 client: Sending POST method to Application Server [%s] for Content Hosting Configuration: [%s]", as_state->application_server->canonicalHostname, upload_chc->state); - m3_client_as_state_requests(as_state, "application/json", data, (char *)OGS_SBI_HTTP_METHOD_POST, component); + m3_client_as_state_requests(as_state, NULL, "application/json", data, (char *)OGS_SBI_HTTP_METHOD_POST, component); } if (chc_with_af_unique_cert_id) OpenAPI_content_hosting_configuration_free(chc_with_af_unique_cert_id); ogs_free(component); @@ -244,28 +271,35 @@ void next_action_for_application_server(msaf_application_server_state_node_t *as resource_id_node_t *delete_chc = ogs_list_first(&as_state->delete_content_hosting_configurations); ogs_debug("M3 client: Sending DELETE method for Content Hosting Configuration [%s] to the Application Server [%s]", delete_chc->state, as_state->application_server->canonicalHostname); component = ogs_msprintf("content-hosting-configurations/%s", delete_chc->state); - m3_client_as_state_requests(as_state, NULL, NULL, (char *)OGS_SBI_HTTP_METHOD_DELETE, component); + m3_client_as_state_requests(as_state, NULL, NULL, NULL, (char *)OGS_SBI_HTTP_METHOD_DELETE, component); ogs_free(component); } else if (ogs_list_first(&as_state->delete_certificates) != NULL) { char *component; resource_id_node_t *delete_cert = ogs_list_first(&as_state->delete_certificates); ogs_debug("M3 client: Sending DELETE method for certificate [%s] to the Application Server [%s]", delete_cert->state, as_state->application_server->canonicalHostname); component = ogs_msprintf("certificates/%s", delete_cert->state); - m3_client_as_state_requests(as_state, NULL, NULL, (char *)OGS_SBI_HTTP_METHOD_DELETE, component); + m3_client_as_state_requests(as_state, NULL, NULL, NULL, (char *)OGS_SBI_HTTP_METHOD_DELETE, component); ogs_free(component); - } else if (ogs_list_first(&as_state->purge_content_hosting_cache) != NULL) { + } else if(ogs_list_first(&as_state->purge_content_hosting_cache) != NULL){ purge_resource_id_node_t *purge_chc = ogs_list_first(&as_state->purge_content_hosting_cache); - char *component = ogs_msprintf("content-hosting-configurations/%s/purge", purge_chc->state); - if(purge_chc->purge_regex) { - ogs_debug("M3 client: Sending cache purge operation for resource [%s] using filter [%s] to the Application Server", purge_chc->state, purge_chc->purge_regex); - } else { - ogs_debug("M3 client: Sending Purge operation for cache [%s] to the Application Server", purge_chc->state); - } - m3_client_as_state_requests(as_state, "application/x-www-form-urlencoded", purge_chc->purge_regex, (char *)OGS_SBI_HTTP_METHOD_POST, component); - ogs_free(component); + ogs_assert(purge_chc); + ogs_assert(purge_chc->provisioning_session_id); + char *component = ogs_msprintf("content-hosting-configurations/%s/purge", purge_chc->provisioning_session_id); + if(purge_chc->purge_regex) { + ogs_debug("M3 client: Sending cache purge operation for resource [%s] to the Application Server", purge_chc->provisioning_session_id); + m3_client_as_state_requests(as_state, purge_chc, "application/x-www-form-urlencoded", purge_chc->purge_regex, OGS_SBI_HTTP_METHOD_POST, component); + } else { + ogs_debug("M3 client: Sending Purge operation for cache [%s] to the Application Server", purge_chc->provisioning_session_id); + m3_client_as_state_requests(as_state, purge_chc, "application/x-www-form-urlencoded", NULL, OGS_SBI_HTTP_METHOD_POST, component); + } + ogs_free(component); + } + + } + void msaf_application_server_remove_all() { msaf_application_server_node_t *msaf_as = NULL, *next = NULL; @@ -336,6 +370,7 @@ msaf_m3_client_init(const char *hostname, int port) static int m3_client_as_state_requests(msaf_application_server_state_node_t *as_state, + purge_resource_id_node_t *purge_node, const char *type, const char *data, const char *method, const char *component) { @@ -359,18 +394,22 @@ m3_client_as_state_requests(msaf_application_server_state_node_t *as_state, as_state->application_server->canonicalHostname, as_state->application_server->m3Port); } + client_request_info_t *request_info = ogs_calloc(1, sizeof(client_request_info_t)); + request_info->as_state = as_state; + request_info->purge_node = purge_node; - ogs_sbi_client_send_request(as_state->client, client_notify_cb, request, as_state); + ogs_sbi_client_send_request(as_state->client, client_notify_cb, request, request_info); ogs_sbi_request_free(request); return 1; -} +} static int client_notify_cb(int status, ogs_sbi_response_t *response, void *data) { int rv; + client_request_info_t *client_request_info = data; msaf_event_t *event; if (status != OGS_OK) { @@ -384,7 +423,8 @@ client_notify_cb(int status, ogs_sbi_response_t *response, void *data) event = (msaf_event_t*)ogs_event_new(OGS_EVENT_SBI_CLIENT); event->h.sbi.response = response; - event->application_server_state = data; + event->application_server_state = client_request_info->as_state; + event->purge_node = client_request_info->purge_node; rv = ogs_queue_push(ogs_app()->queue, event); if (rv !=OGS_OK) { ogs_error("OGS Queue Push failed %d", rv); @@ -392,6 +432,10 @@ client_notify_cb(int status, ogs_sbi_response_t *response, void *data) ogs_event_free(event); return OGS_ERROR; } - + if (client_request_info->purge_node == NULL) ogs_free(client_request_info->purge_node); + ogs_free(client_request_info); return OGS_OK; } + +/* vim:ts=8:sts=4:sw=4:expandtab: +*/ diff --git a/src/5gmsaf/application-server-context.h b/src/5gmsaf/application-server-context.h index 267f591..d4d4377 100644 --- a/src/5gmsaf/application-server-context.h +++ b/src/5gmsaf/application-server-context.h @@ -28,7 +28,6 @@ typedef struct msaf_application_server_node_s { typedef struct msaf_application_server_state_node_s { ogs_lnode_t node; ogs_sbi_client_t *client; - ogs_sbi_stream_t *stream; msaf_application_server_node_t *application_server; ogs_list_t assigned_provisioning_sessions; ogs_list_t *current_certificates; @@ -50,10 +49,19 @@ typedef struct application_server_state_node_s { char *state; } resource_id_node_t; -typedef struct purge_resource_node_s { +typedef struct m1_purge_information_s { + int refs; + int purged_entries_total; + + ogs_sbi_stream_t *m1_stream; + ogs_sbi_message_t m1_message; +} m1_purge_information_t; + +typedef struct purge_resource_id_node_s { ogs_lnode_t node; - char *state; + char *provisioning_session_id; char *purge_regex; + m1_purge_information_t *m1_purge_info; } purge_resource_id_node_t; /** @@ -62,13 +70,13 @@ typedef struct purge_resource_node_s { * @param as_state The application server state to add this CHC to. * @param provisioning_session The provisioning session of the CHC. */ -extern void msaf_application_server_state_set(msaf_application_server_state_node_t *as_state, msaf_provisioning_session_t *provisioning_session); +extern int msaf_application_server_state_set(msaf_application_server_state_node_t *as_state, msaf_provisioning_session_t *provisioning_session); extern void msaf_application_server_state_log(ogs_list_t *list, const char* list_name); extern msaf_application_server_node_t *msaf_application_server_add(char *canonical_hostname, char *url_path_prefix_format, int m3_port); extern void msaf_application_server_remove_all(void); extern void msaf_application_server_print_all(void); extern void next_action_for_application_server(msaf_application_server_state_node_t *as_state); -extern void msaf_application_server_state_set_on_post( msaf_provisioning_session_t *provisioning_session); +extern int msaf_application_server_state_set_on_post( msaf_provisioning_session_t *provisioning_session); extern void msaf_application_server_state_update( msaf_provisioning_session_t *provisioning_session); diff --git a/src/5gmsaf/certmgr.c b/src/5gmsaf/certmgr.c new file mode 100644 index 0000000..1b49bb6 --- /dev/null +++ b/src/5gmsaf/certmgr.c @@ -0,0 +1,373 @@ +/* + * License: 5G-MAG Public License (v1.0) + * Author: Dev Audsin + * Copyright: (C) 2022 British Broadcasting Corporation + * + * For full license terms please see the LICENSE file distributed with this + * program. If this file is missing then the license can be retrieved from + * https://drive.google.com/file/d/1cinCiA778IErENZ3JN52VFW-1ffHpx7Z/view + */ + +#include "ogs-core.h" + +#include "utilities.h" + +#include "certmgr.h" + +#define MAX_CHILD_PROCESS 16 + +static msaf_certificate_t *msaf_certificate_populate(const char *certid, const char *cert, int out_return_code); + +int server_cert_delete(const char *certid) +{ + const char *commandLine[OGS_ARG_MAX]; + ogs_proc_t *current = NULL; + FILE *out = NULL; + char buf[OGS_HUGE_LEN]; + int ret = 0, out_return_code = 0; + + commandLine[0] = msaf_self()->config.certificateManager; + commandLine[1] = "-c"; + commandLine[2] = "delete"; + commandLine[3] = certid; + commandLine[4] = NULL; + + current = (ogs_proc_t*)ogs_calloc(1, sizeof(*current)); + ret = ogs_proc_create(commandLine, + ogs_proc_option_combined_stdout_stderr| + ogs_proc_option_inherit_environment, + current); + ogs_assert(ret == 0); + out = ogs_proc_stdout(current); + ogs_assert(out); + + while(fgets(buf, OGS_HUGE_LEN, out)) { + printf("%s", buf); + } + ret = ogs_proc_join(current, &out_return_code); + ogs_assert(ret == 0); + ret = ogs_proc_destroy(current); + ogs_assert(ret == 0); + + ogs_free(current); + + return out_return_code; +} + +msaf_certificate_t *server_cert_retrieve(const char *certid) +{ + const char *commandLine[OGS_ARG_MAX]; + ogs_proc_t *current = NULL; + FILE *out = NULL; + char buf[OGS_HUGE_LEN]; + char *cert = NULL; + int ret = 0, out_return_code = 0; + msaf_certificate_t *msaf_certificate = NULL; + size_t cert_size = 0; + size_t cert_reserved = 0; + + commandLine[0] = msaf_self()->config.certificateManager; + commandLine[1] = "-c"; + commandLine[2] = "publiccert"; + commandLine[3] = certid; + commandLine[4] = NULL; + + current = (ogs_proc_t*)ogs_calloc(1, sizeof(*current)); + ret = ogs_proc_create(commandLine, + ogs_proc_option_combined_stdout_stderr| + ogs_proc_option_inherit_environment, + current); + ogs_assert(ret == 0); + out = ogs_proc_stdout(current); + ogs_assert(out); + + cert = ogs_calloc(1, 4096); + cert_reserved = 4096; + + while(fgets(buf, OGS_HUGE_LEN, out)) { + cert_size += strlen (buf); + if(cert_size > cert_reserved - 1) { + cert_reserved +=4096; + cert = ogs_realloc(cert,cert_reserved); + } + strcat(cert,buf); + } + ret = ogs_proc_join(current, &out_return_code); + ogs_assert(ret == 0); + ret = ogs_proc_destroy(current); + ogs_assert(ret == 0); + ogs_free(current); + + if(out_return_code == 0 || out_return_code == 4 || out_return_code == 8){ + msaf_certificate = msaf_certificate_populate(certid, cert, out_return_code); + ogs_assert(msaf_certificate); + } + ogs_free(cert); + return msaf_certificate; +} + +msaf_certificate_t *server_cert_get_servercert(const char *certid) +{ + const char *commandLine[OGS_ARG_MAX]; + ogs_proc_t *current = NULL; + FILE *out = NULL; + char buf[OGS_HUGE_LEN]; + char *cert = NULL; + int ret = 0, out_return_code = 0; + msaf_certificate_t *msaf_certificate = NULL; + size_t cert_size = 0; + size_t cert_reserved = 0; + + commandLine[0] = msaf_self()->config.certificateManager; + commandLine[1] = "-c"; + commandLine[2] = "servercert"; + commandLine[3] = certid; + commandLine[4] = NULL; + + current = (ogs_proc_t*)ogs_calloc(1, sizeof(*current)); + ret = ogs_proc_create(commandLine, + ogs_proc_option_combined_stdout_stderr| + ogs_proc_option_inherit_environment, + current); + ogs_assert(ret == 0); + out = ogs_proc_stdout(current); + ogs_assert(out); + + cert = ogs_calloc(1, 4096); + cert_reserved = 4096; + + while(fgets(buf, OGS_HUGE_LEN, out)) { + cert_size += strlen (buf); + if(cert_size > cert_reserved - 1) { + cert_reserved +=4096; + cert = ogs_realloc(cert,cert_reserved); + } + strcat(cert,buf); + } + ret = ogs_proc_join(current, &out_return_code); + ogs_assert(ret == 0); + ret = ogs_proc_destroy(current); + ogs_assert(ret == 0); + ogs_free(current); + + if(!out_return_code){ + msaf_certificate = msaf_certificate_populate(certid, cert, out_return_code); + ogs_assert(msaf_certificate); + } + ogs_free(cert); + return msaf_certificate; +} + +int server_cert_set(const char *cert_id, const char *cert) +{ + const char *commandLine[OGS_ARG_MAX]; + ogs_proc_t *current = NULL; + FILE *in = NULL; + int ret = 0, out_return_code = 0; + + commandLine[0] = msaf_self()->config.certificateManager; + commandLine[1] = "-c"; + commandLine[2] = "setcert"; + commandLine[3] = cert_id; + commandLine[4] = NULL; + + current = (ogs_proc_t*)ogs_calloc(1, sizeof(*current)); + ret = ogs_proc_create(commandLine, + ogs_proc_option_inherit_environment, + current); + ogs_assert(ret == 0); + + in = ogs_proc_stdin(current); + ogs_assert(in); + + if (cert) { + fputs(cert, in); + } + + ret = ogs_proc_join(current, &out_return_code); + ogs_assert(ret == 0); + ret = ogs_proc_destroy(current); + ogs_assert(ret == 0); + ogs_free(current); + + return out_return_code; +} + + +msaf_certificate_t *server_cert_new(const char *operation, const char *operation_params) +{ + const char *commandLine[OGS_ARG_MAX]; + ogs_proc_t *current = NULL; + FILE *out = NULL; + char buf[OGS_HUGE_LEN]; + char *cert; + int ret = 0, out_return_code = 0; + char *canonical_domain_name; + msaf_certificate_t *msaf_certificate = NULL; + size_t cert_size = 0; + size_t cert_reserved = 0; + msaf_application_server_node_t *msaf_as = NULL; + msaf_as = ogs_list_first(&msaf_self()->config.applicationServers_list); + canonical_domain_name = msaf_as->canonicalHostname; + + ogs_uuid_t uuid; + char id[OGS_UUID_FORMATTED_LENGTH + 1]; + + ogs_uuid_get(&uuid); + ogs_uuid_format(id, &uuid); + + commandLine[0] = msaf_self()->config.certificateManager; + commandLine[1] = "-c"; + commandLine[2] = operation; + commandLine[3] = id; + commandLine[4] = canonical_domain_name; + commandLine[5] = NULL; + + current = (ogs_proc_t*)ogs_calloc(1, sizeof(*current)); + ret = ogs_proc_create(commandLine, + ogs_proc_option_combined_stdout_stderr| + ogs_proc_option_inherit_environment, + current); + ogs_assert(ret == 0); + out = ogs_proc_stdout(current); + ogs_assert(out); + + cert = ogs_calloc(1, 4096); + cert_reserved = 4096; + + while(fgets(buf, OGS_HUGE_LEN, out)) { + cert_size += strlen (buf); + if(cert_size > cert_reserved - 1) { + cert_reserved =+ 4096; + cert = ogs_realloc(cert,cert_reserved); + } + strcat(cert,buf); + } + ret = ogs_proc_join(current, &out_return_code); + ogs_assert(ret == 0); + ret = ogs_proc_destroy(current); + ogs_assert(ret == 0); + ogs_free(current); + msaf_certificate = msaf_certificate_populate(id, cert, out_return_code); + ogs_assert(msaf_certificate); + ogs_free(cert); + return msaf_certificate; +} + +char *check_in_cert_list(const char *canonical_domain_name) +{ + const char *commandLine[OGS_ARG_MAX]; + ogs_proc_t *current = NULL; + FILE *out = NULL; + char buf[OGS_HUGE_LEN]; + int ret = 0, out_return_code = 0; + char *certificate = NULL; + char *cert_id; + + commandLine[0] = msaf_self()->config.certificateManager; + commandLine[1] = "-c"; + commandLine[2] = "list"; + commandLine[3] = NULL; + + current = (ogs_proc_t*)ogs_calloc(1, sizeof(*current)); + ret = ogs_proc_create(commandLine, + ogs_proc_option_combined_stdout_stderr| + ogs_proc_option_inherit_environment, + current); + ogs_assert(ret == 0); + out = ogs_proc_stdout(current); + ogs_assert(out); + + while(fgets(buf, OGS_HUGE_LEN, out)) { + + ogs_debug("buf=\"%s\", canonical_domain_name=\"%s\"", buf, canonical_domain_name); + if (str_match(buf, canonical_domain_name)) { + certificate = strtok_r(buf,"\t",&cert_id); + ogs_debug("buf=\"%s\", certificate=\"%s\", cert_id=\"%s\"", buf, certificate, cert_id); + break; + } + } + + ret = ogs_proc_join(current, &out_return_code); + ogs_assert(ret == 0); + ret = ogs_proc_destroy(current); + ogs_assert(ret == 0); + ogs_free(current); + + if (!certificate) return NULL; + + return ogs_strdup(certificate); +} + +static msaf_certificate_t *msaf_certificate_populate(const char *certid, const char *cert, int out_return_code) +{ + msaf_certificate_t *msaf_certificate; + const char *line; + const char *eol; + const char *hdr_value; + static const char begin_marker[] = "-----BEGIN"; + static const char max_age_str[] = "max-age="; + + msaf_certificate = ogs_calloc(1, sizeof(msaf_certificate_t)); + ogs_assert(msaf_certificate); + + msaf_certificate->id = ogs_strdup(certid); + msaf_certificate->return_code = out_return_code; + + msaf_certificate->headers = nf_headers_new(); + ogs_assert(msaf_certificate->headers); + + line = cert; + while ((eol = strchr(line, '\n')) != NULL) { + const char *end_field; + /* Stop when we get to the certificate, key or CSR */ + if (strncmp(line, begin_marker, sizeof(begin_marker)-1) == 0) + break; + /* otherwise try and interpret as "Field: Value" */ + end_field = strchr(line, ':'); + if (end_field) { + char *field; + char *value; + const char *value_start; + const char *value_end; + field = ogs_strndup(line, end_field-line); + value_start = end_field+1; + while (*value_start && *value_start == ' ') value_start++; + value_end = eol-1; + while (value_end>value_start && *value_end == ' ') value_end--; + value = ogs_strndup(value_start, value_end-value_start+1); + nf_headers_set(msaf_certificate->headers, field, value); + ogs_free(field); + ogs_free(value); + } + line = eol+1; + } + + msaf_certificate->certificate = ogs_strdup(line); + + hdr_value = nf_headers_get(msaf_certificate->headers, "Last-Modified"); + if (hdr_value) { + msaf_certificate->last_modified = str_to_time(hdr_value); + } + + hdr_value = nf_headers_get(msaf_certificate->headers, "Cache-Control"); + if (hdr_value && strncmp(hdr_value, max_age_str, sizeof(max_age_str)-1)==0) { + msaf_certificate->cache_control_max_age = ascii_to_long(hdr_value+sizeof(max_age_str)-1); + } + + hdr_value = nf_headers_get(msaf_certificate->headers, "ETag"); + if (hdr_value) { + msaf_certificate->server_certificate_hash = ogs_strdup(hdr_value); + } + + return msaf_certificate; +} + +void msaf_certificate_free(msaf_certificate_t *cert) +{ + if (cert->headers) nf_headers_free(cert->headers); + if (cert->certificate) ogs_free(cert->certificate); + if (cert->server_certificate_hash) ogs_free(cert->server_certificate_hash); + if (cert->id) ogs_free(cert->id); + ogs_free(cert); +} diff --git a/src/5gmsaf/certmgr.h b/src/5gmsaf/certmgr.h new file mode 100644 index 0000000..0317049 --- /dev/null +++ b/src/5gmsaf/certmgr.h @@ -0,0 +1,48 @@ +/* + * License: 5G-MAG Public License (v1.0) + * Author: Dev Audsin + * Copyright: (C) 2022 British Broadcasting Corporation + * + * For full license terms please see the LICENSE file distributed with this + * program. If this file is missing then the license can be retrieved from + * https://drive.google.com/file/d/1cinCiA778IErENZ3JN52VFW-1ffHpx7Z/view + */ + +#ifndef MSAF_CERT_MGR_H +#define MSAF_CERT_MGR_H + +#include "context.h" +#include "headers.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct msaf_certificate_s { + char *id; + char *certificate; + time_t last_modified; + char *server_certificate_hash; + nf_headers_t *headers; + int cache_control_max_age; + int return_code; +} msaf_certificate_t; + +typedef struct msaf_assigned_certificate_s { + ogs_lnode_t node; + char *certificate_id; +} msaf_assigned_certificate_t; + +extern msaf_certificate_t *server_cert_new(const char *operation, const char *operation_params); +extern int server_cert_set(const char *cert_id, const char *cert); +extern msaf_certificate_t *server_cert_retrieve(const char *certid); +extern msaf_certificate_t *server_cert_get_servercert(const char *certid); +extern char *check_in_cert_list(const char *canonical_domain_name); +extern int server_cert_delete(const char *certid); +extern void msaf_certificate_free(msaf_certificate_t *cert); + +#ifdef __cplusplus +} +#endif + +#endif /* MSAF_CERT_MGR_H */ diff --git a/src/5gmsaf/context.c b/src/5gmsaf/context.c index 363a000..72020bf 100644 --- a/src/5gmsaf/context.c +++ b/src/5gmsaf/context.c @@ -30,12 +30,16 @@ static int msaf_context_validation(void); static int free_ogs_hash_entry(void *free_ogs_hash_context, const void *key, int klen, const void *value); static void safe_ogs_free(void *memory); +static void msaf_context_server_addr_add(const char *server_addr); + + static void msaf_context_application_server_state_certificates_remove_all(void); static void msaf_context_application_server_state_content_hosting_configuration_remove_all(void); static void msaf_context_application_server_state_assigned_provisioning_sessions_remove_all(void); static void msaf_context_application_server_state_remove_all(void); - -/***** Public functions *****/ +static void msaf_context_server_addr_remove_all(void); +static void msaf_context_server_addr_remove(msaf_context_server_addr_t *msaf_server_addr); +static void msaf_context_server_sockaddr_remove(void); void msaf_context_init(void) { @@ -48,6 +52,8 @@ void msaf_context_init(void) ogs_list_init(&self->config.applicationServers_list); + ogs_list_init(&self->config.server_addr_list); + ogs_list_init(&self->application_server_states); self->provisioningSessions_map = ogs_hash_make(); @@ -56,6 +62,9 @@ void msaf_context_init(void) self->content_hosting_configuration_file_map = ogs_hash_make(); ogs_assert(self->content_hosting_configuration_file_map); + + msaf_server_response_cache_control_set(); + } void msaf_context_final(void) @@ -80,11 +89,15 @@ void msaf_context_final(void) ogs_hash_destroy(self->content_hosting_configuration_file_map); } - if(self->config.contentHostingConfiguration) - ogs_free(self->config.contentHostingConfiguration); - - if(self->config.certificate) - ogs_free(self->config.certificate); + if (self->config.server_response_cache_control) + { + ogs_free(self->config.server_response_cache_control); + } + + if (self->config.certificateManager) + ogs_free(self->config.certificateManager); + + msaf_context_server_addr_remove_all(); msaf_application_server_remove_all(); @@ -96,6 +109,8 @@ void msaf_context_final(void) msaf_context_application_server_state_remove_all(); + msaf_context_server_sockaddr_remove(); + ogs_free(self); self = NULL; } @@ -132,10 +147,8 @@ int msaf_context_parse_config(void) if (!strcmp(open5gs, "true")) { self->config.open5gsIntegration_flag = 1; } - } else if (!strcmp(msaf_key, "certificate")) { - self->config.certificate = rebase_path(ogs_app()->file, ogs_yaml_iter_value(&msaf_iter)); - } else if (!strcmp(msaf_key, "contentHostingConfiguration")) { - self->config.contentHostingConfiguration = rebase_path(ogs_app()->file, ogs_yaml_iter_value(&msaf_iter)); + } else if (!strcmp(msaf_key, "certificateManager")) { + self->config.certificateManager = ogs_strdup(ogs_yaml_iter_value(&msaf_iter)); } else if (!strcmp(msaf_key, "applicationServers")) { ogs_yaml_iter_t as_iter, as_array; ogs_yaml_iter_recurse(&msaf_iter, &as_array); @@ -164,7 +177,43 @@ int msaf_context_parse_config(void) } } msaf_application_server_add(canonical_hostname, url_path_prefix_format, m3_port); - } else if (!strcmp(msaf_key, "sbi")) { + } else if (!strcmp(msaf_key, "serverResponseCacheControl")) { + ogs_yaml_iter_t cc_iter, cc_array; + ogs_yaml_iter_recurse(&msaf_iter, &cc_array); + if (ogs_yaml_iter_type(&cc_array) == YAML_MAPPING_NODE) { + memcpy(&cc_iter, &cc_array, sizeof(ogs_yaml_iter_t)); + } else if (ogs_yaml_iter_type(&cc_array) == YAML_SEQUENCE_NODE) { + if (!ogs_yaml_iter_next(&cc_array)) + break; + ogs_yaml_iter_recurse(&cc_array, &cc_iter); + } else if (ogs_yaml_iter_type(&cc_array) == YAML_SCALAR_NODE) { + break; + } else + ogs_assert_if_reached(); + + int m1_provisioning_session_response_max_age = SERVER_RESPONSE_MAX_AGE; + int m1_content_hosting_configurations_response_max_age = SERVER_RESPONSE_MAX_AGE; + int m1_server_certificates_response_max_age = SERVER_RESPONSE_MAX_AGE; + int m1_content_protocols_response_max_age = M1_CONTENT_PROTOCOLS_RESPONSE_MAX_AGE; + int m5_service_access_information_response_max_age = SERVER_RESPONSE_MAX_AGE; + while (ogs_yaml_iter_next(&cc_iter)) { + const char *cc_key = ogs_yaml_iter_key(&cc_iter); + ogs_assert(cc_key); + if (!strcmp(cc_key, "m1ProvisioningSessions")) { + m1_provisioning_session_response_max_age = ascii_to_long(ogs_yaml_iter_value(&cc_iter)); + } else if (!strcmp(cc_key, "m1ServerCertificates")) { + m1_server_certificates_response_max_age = ascii_to_long(ogs_yaml_iter_value(&cc_iter)); + } else if (!strcmp(cc_key, "m1ContentHostingConfigurations")) { + m1_content_hosting_configurations_response_max_age = ascii_to_long(ogs_yaml_iter_value(&cc_iter)); + } else if (!strcmp(cc_key, "m1ContentProtocols")) { + m1_content_protocols_response_max_age = ascii_to_long(ogs_yaml_iter_value(&cc_iter)); + } else if (!strcmp(cc_key, "m5ServiceAccessInformation")) { + m5_service_access_information_response_max_age = ascii_to_long(ogs_yaml_iter_value(&cc_iter)); + } + } + msaf_server_response_cache_control_set_from_config(m1_provisioning_session_response_max_age, m1_content_hosting_configurations_response_max_age, m1_server_certificates_response_max_age, m1_content_protocols_response_max_age, m5_service_access_information_response_max_age); + + } else if (!strcmp(msaf_key, "sbi") || !strcmp(msaf_key, "m1") || !strcmp(msaf_key, "m5") || !strcmp(msaf_key, "maf")) { if(!self->config.open5gsIntegration_flag) { ogs_list_t list, list6; ogs_socknode_t *node = NULL, *node6 = NULL; @@ -270,10 +319,15 @@ int msaf_context_parse_config(void) } else ogs_warn("unknown key `%s`", sbi_key); } + + if (port == 0){ + ogs_warn("Specify the [%s] port, otherwise a random port will be used", msaf_key); + } addr = NULL; for (i = 0; i < num; i++) { rv = ogs_addaddrinfo(&addr, family, hostname[i], port, 0); + msaf_context_server_addr_add(hostname[i]); ogs_assert(rv == OGS_OK); } @@ -305,29 +359,126 @@ int msaf_context_parse_config(void) node = ogs_list_first(&list); if (node) { - ogs_sbi_server_t *server = ogs_sbi_server_add( - node->addr, is_option ? &option : NULL); - ogs_assert(server); - - if (addr && ogs_app()->parameter.no_ipv4 == 0) - ogs_sbi_server_set_advertise( - server, AF_INET, addr); - - if (key) server->tls.key = key; - if (pem) server->tls.pem = pem; + int i; + int matches = 0; + ogs_sbi_server_t *server; + ogs_sockaddr_t *check_addrs[] = { + self->config.sbi_server_sockaddr, + self->config.m1_server_sockaddr, + self->config.m5_server_sockaddr, + self->config.maf_mgmt_server_sockaddr + }; + for (i=0; i<(sizeof(check_addrs)/sizeof(*check_addrs)); i++) { + if (check_addrs[i] && ogs_sockaddr_is_equal(node->addr, check_addrs[i])) { + matches = 1; + break; + } + } + if(!matches) { + server = ogs_sbi_server_add( + node->addr, is_option ? &option : NULL); + ogs_assert(server); + + + if (addr && ogs_app()->parameter.no_ipv4 == 0) + ogs_sbi_server_set_advertise( + server, AF_INET, addr); + + if (key) server->tls.key = key; + if (pem) server->tls.pem = pem; + } + + if (!strcmp(msaf_key, "sbi")) { + + ogs_assert(OGS_OK == ogs_copyaddrinfo(&self->config.sbi_server_sockaddr, server->node.addr)); + if(!self->config.m1_server_sockaddr){ + ogs_assert(OGS_OK == ogs_copyaddrinfo(&self->config.m1_server_sockaddr, server->node.addr)); + } + if(!self->config.m5_server_sockaddr){ + ogs_assert(OGS_OK == ogs_copyaddrinfo(&self->config.m5_server_sockaddr, server->node.addr)); + } + if(!self->config.maf_mgmt_server_sockaddr){ + ogs_assert(OGS_OK == ogs_copyaddrinfo(&self->config.maf_mgmt_server_sockaddr, server->node.addr)); + } + } + if (!strcmp(msaf_key, "m1")) { + if(self->config.m1_server_sockaddr){ + ogs_freeaddrinfo(self->config.m1_server_sockaddr); + } + ogs_assert(OGS_OK == ogs_copyaddrinfo(&self->config.m1_server_sockaddr, server->node.addr)); + } + if (!strcmp(msaf_key, "m5")) { + if(self->config.m5_server_sockaddr){ + ogs_freeaddrinfo(self->config.m5_server_sockaddr); + } + ogs_assert(OGS_OK == ogs_copyaddrinfo(&self->config.m5_server_sockaddr, server->node.addr)); + } + if (!strcmp(msaf_key, "maf")) { + if(self->config.maf_mgmt_server_sockaddr){ + ogs_freeaddrinfo(self->config.maf_mgmt_server_sockaddr); + } + ogs_assert(OGS_OK == ogs_copyaddrinfo(&self->config.maf_mgmt_server_sockaddr, server->node.addr)); + } } node6 = ogs_list_first(&list6); if (node6) { - ogs_sbi_server_t *server = ogs_sbi_server_add( - node6->addr, is_option ? &option : NULL); - ogs_assert(server); - - if (addr && ogs_app()->parameter.no_ipv6 == 0) - ogs_sbi_server_set_advertise( - server, AF_INET6, addr); - - if (key) server->tls.key = key; - if (pem) server->tls.pem = pem; + int i; + int matches = 0; + ogs_sbi_server_t *server; + ogs_sockaddr_t *check_addrs[] = { + self->config.sbi_server_sockaddr_v6, + self->config.m1_server_sockaddr_v6, + self->config.m5_server_sockaddr_v6, + self->config.maf_mgmt_server_sockaddr_v6 + }; + for (i=0; i<(sizeof(check_addrs)/sizeof(*check_addrs)); i++) { + if (check_addrs[i] && ogs_sockaddr_is_equal(node->addr, check_addrs[i])) { + matches = 1; + break; + } + } + if(!matches) { + server = ogs_sbi_server_add( + node6->addr, is_option ? &option : NULL); + ogs_assert(server); + + if (addr && ogs_app()->parameter.no_ipv6 == 0) + ogs_sbi_server_set_advertise( + server, AF_INET6, addr); + + if (key) server->tls.key = key; + if (pem) server->tls.pem = pem; + if (!strcmp(msaf_key, "sbi")) { + ogs_assert(OGS_OK == ogs_copyaddrinfo(&self->config.sbi_server_sockaddr_v6, server->node.addr)); + if(!self->config.m1_server_sockaddr_v6){ + ogs_assert(OGS_OK == ogs_copyaddrinfo(&self->config.m1_server_sockaddr_v6, server->node.addr)); + } + if(!self->config.m5_server_sockaddr_v6){ + ogs_assert(OGS_OK == ogs_copyaddrinfo(&self->config.m5_server_sockaddr_v6, server->node.addr)); + } + if(!self->config.maf_mgmt_server_sockaddr_v6){ + ogs_assert(OGS_OK == ogs_copyaddrinfo(&self->config.maf_mgmt_server_sockaddr_v6, server->node.addr)); + } + } + if (!strcmp(msaf_key, "m1")) { + if(self->config.m1_server_sockaddr_v6){ + ogs_freeaddrinfo(self->config.m1_server_sockaddr_v6); + } + ogs_assert(OGS_OK == ogs_copyaddrinfo(&self->config.m1_server_sockaddr_v6, server->node.addr)); + } + if (!strcmp(msaf_key, "m5")) { + if(self->config.m5_server_sockaddr_v6){ + ogs_freeaddrinfo(self->config.m5_server_sockaddr_v6); + } + ogs_assert(OGS_OK == ogs_copyaddrinfo(&self->config.m5_server_sockaddr_v6, server->node.addr)); + } + if (!strcmp(msaf_key, "maf")) { + if(self->config.maf_mgmt_server_sockaddr_v6){ + ogs_freeaddrinfo(self->config.maf_mgmt_server_sockaddr_v6); + } + ogs_assert(OGS_OK == ogs_copyaddrinfo(&self->config.maf_mgmt_server_sockaddr_v6, server->node.addr)); + } + } } if (addr) @@ -367,119 +518,19 @@ msaf_context_get_content_hosting_configuration_resource_identifier(const char *c /***** Private functions *****/ -static void msaf_context_delete_certificate(const char *resource_id) { - - msaf_application_server_state_node_t *as_state; - ogs_list_for_each(&self->application_server_states, as_state) { - resource_id_node_t *certificate, *next = NULL; - resource_id_node_t *upload_certificate, *next_node = NULL; - resource_id_node_t *delete_cert = NULL; - ogs_list_init(&as_state->delete_certificates); - - if (as_state->current_certificates) { - - char *current_cert_id; - char *provisioning_session; - char *cert_id; - - ogs_list_for_each_safe(as_state->current_certificates, next, certificate){ - - current_cert_id = ogs_strdup(certificate->state); - provisioning_session = strtok_r(current_cert_id,":",&cert_id); - - if(!strcmp(provisioning_session, resource_id)) - break; - - if(current_cert_id) - ogs_free(current_cert_id); - - } - - if(certificate) { - delete_cert = ogs_calloc(1, sizeof(resource_id_node_t)); - ogs_assert(delete_cert); - delete_cert->state = ogs_strdup(certificate->state); - ogs_list_add(&as_state->delete_certificates, delete_cert); - - //ogs_list_add(&as_state->delete_certificates, certificate); - } - - if(current_cert_id) - ogs_free(current_cert_id); - } - - { - char *upload_cert_id = NULL; - char *provisioning_session; - char *cert_id; - - ogs_list_for_each_safe(&as_state->upload_certificates, next_node, upload_certificate) { - - upload_cert_id = ogs_strdup(upload_certificate->state); - provisioning_session = strtok_r(upload_cert_id,":",&cert_id); - if(!strcmp(provisioning_session, resource_id)) - break; - } - - if (upload_certificate) { - - ogs_list_remove(&as_state->upload_certificates, upload_certificate); - - ogs_list_add(&as_state->delete_certificates, upload_certificate); - - } - - if (upload_cert_id) - ogs_free(upload_cert_id); - } - - //next_action_for_application_server(as_state); +static void msaf_context_server_addr_remove_all() +{ + msaf_context_server_addr_t *msaf_server_addr = NULL, *next = NULL; + ogs_list_for_each_safe(&msaf_self()->config.server_addr_list, next, msaf_server_addr) + msaf_context_server_addr_remove(msaf_server_addr); - } } -static void msaf_context_delete_content_hosting_configuration(const char *resource_id) { - - msaf_application_server_state_node_t *as_state; - ogs_list_for_each(&self->application_server_states, as_state) { - - resource_id_node_t *content_hosting_configuration, *next = NULL; - resource_id_node_t *upload_content_hosting_configuration, *next_node = NULL; - resource_id_node_t *delete_chc = NULL; - - ogs_list_init(&as_state->delete_content_hosting_configurations); - - if (as_state->current_content_hosting_configurations) { - - ogs_list_for_each_safe(as_state->current_content_hosting_configurations, next, content_hosting_configuration){ - - if(!strcmp(content_hosting_configuration->state, resource_id)) - break; - } - if(content_hosting_configuration) { - - delete_chc = ogs_calloc(1, sizeof(resource_id_node_t)); - ogs_assert(delete_chc); - delete_chc->state = ogs_strdup(content_hosting_configuration->state); - ogs_list_add(&as_state->delete_content_hosting_configurations, delete_chc); - - } - } - - ogs_list_for_each_safe(&as_state->upload_content_hosting_configurations, next_node, upload_content_hosting_configuration){ - if(!strcmp(upload_content_hosting_configuration->state, resource_id)) - break; - } - if (upload_content_hosting_configuration) { - - ogs_list_remove(&as_state->upload_content_hosting_configurations, upload_content_hosting_configuration); - - ogs_list_add(&as_state->delete_content_hosting_configurations, upload_content_hosting_configuration); - - } - - next_action_for_application_server(as_state); - } +static void msaf_context_server_addr_remove(msaf_context_server_addr_t *msaf_server_addr) +{ + ogs_assert(msaf_server_addr); + ogs_list_remove(&msaf_self()->config.server_addr_list, msaf_server_addr); + ogs_free(msaf_server_addr); } static void msaf_context_application_server_state_certificates_remove_all(void) { @@ -592,6 +643,17 @@ static void msaf_context_application_server_state_remove_all(void) { } } +static void msaf_context_server_sockaddr_remove(void){ + if(self->config.sbi_server_sockaddr) ogs_freeaddrinfo(self->config.sbi_server_sockaddr); + if(self->config.m1_server_sockaddr) ogs_freeaddrinfo(self->config.m1_server_sockaddr); + if(self->config.m5_server_sockaddr) ogs_freeaddrinfo(self->config.m5_server_sockaddr); + if(self->config.maf_mgmt_server_sockaddr) ogs_freeaddrinfo(self->config.maf_mgmt_server_sockaddr); + if(self->config.sbi_server_sockaddr_v6) ogs_freeaddrinfo(self->config.sbi_server_sockaddr_v6); + if(self->config.m1_server_sockaddr_v6) ogs_freeaddrinfo(self->config.m1_server_sockaddr_v6); + if(self->config.m5_server_sockaddr_v6) ogs_freeaddrinfo(self->config.m5_server_sockaddr_v6); + if(self->config.maf_mgmt_server_sockaddr_v6) ogs_freeaddrinfo(self->config.maf_mgmt_server_sockaddr_v6); +} + static int msaf_context_prepare(void) { return OGS_OK; @@ -616,6 +678,8 @@ free_ogs_hash_entry(void *rec, const void *key, int klen, const void *value) void msaf_context_provisioning_session_free(msaf_provisioning_session_t *provisioning_session) { + msaf_application_server_state_ref_node_t *next_as_state_ref, *as_state_ref; + ogs_assert(provisioning_session); if (provisioning_session->certificate_map) { free_ogs_hash_context_t fohc = { @@ -628,8 +692,19 @@ msaf_context_provisioning_session_free(msaf_provisioning_session_t *provisioning if (provisioning_session->provisioningSessionId) ogs_free(provisioning_session->provisioningSessionId); if (provisioning_session->aspId) ogs_free(provisioning_session->aspId); if (provisioning_session->externalApplicationId) ogs_free(provisioning_session->externalApplicationId); + if (provisioning_session->provisioningSessionHash) ogs_free(provisioning_session->provisioningSessionHash); + if (provisioning_session->contentHostingConfiguration) OpenAPI_content_hosting_configuration_free(provisioning_session->contentHostingConfiguration); + if (provisioning_session->contentHostingConfigurationHash) ogs_free(provisioning_session->contentHostingConfigurationHash); + if (provisioning_session->serviceAccessInformation) OpenAPI_service_access_information_resource_free(provisioning_session->serviceAccessInformation); + if (provisioning_session->serviceAccessInformationHash) ogs_free(provisioning_session->serviceAccessInformationHash); + + ogs_list_for_each_safe(&provisioning_session->application_server_states, next_as_state_ref, as_state_ref) { + ogs_list_remove(&provisioning_session->application_server_states, as_state_ref); + ogs_free(as_state_ref); + } + ogs_free(provisioning_session); } @@ -640,11 +715,33 @@ safe_ogs_free(void *memory) ogs_free(memory); } -ogs_hash_t * -msaf_context_content_hosting_configuration_file_map(char *provisioning_session_id) -{ - ogs_hash_set(self->content_hosting_configuration_file_map, ogs_strdup(self->config.contentHostingConfiguration), OGS_HASH_KEY_STRING, ogs_strdup(provisioning_session_id)); - return self->content_hosting_configuration_file_map; +static void msaf_context_server_addr_add(const char *server_addr) { + msaf_context_server_addr_t *msaf_server_addr; + msaf_server_addr = ogs_calloc(1, sizeof(msaf_context_server_addr_t)); + ogs_assert(msaf_server_addr); + msaf_server_addr->server_addr = server_addr; + ogs_list_add(&self->config.server_addr_list, msaf_server_addr); + +} + +int msaf_context_server_name_set(void) { + + msaf_context_server_addr_t *msaf_server_addr; + struct sockaddr_in sa; + msaf_server_addr = ogs_list_first(&self->config.server_addr_list); + memset(&sa, 0, sizeof sa); + sa.sin_family = AF_INET; + inet_pton(AF_INET, msaf_server_addr->server_addr, &sa.sin_addr); + int res = getnameinfo((struct sockaddr*)&sa, sizeof(sa), + self->server_name, sizeof(self->server_name), + NULL, 0, NI_NAMEREQD); + if (res) { + ogs_error("error retrieving server name: %d\n", res); + } else { + ogs_debug("node=%s", self->server_name); + } + + return 0; } /* vim:ts=8:sts=4:sw=4:expandtab: diff --git a/src/5gmsaf/context.h b/src/5gmsaf/context.h index 4bc26eb..dbc4b8d 100644 --- a/src/5gmsaf/context.h +++ b/src/5gmsaf/context.h @@ -11,21 +11,27 @@ program. If this file is missing then the license can be retrieved from #ifndef MSAF_CONTEXT_H #define MSAF_CONTEXT_H -#include +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif +#include +#include +#include +#include +#include +#include #include "ogs-sbi.h" #include "ogs-app.h" - #include "event.h" #include "msaf-sm.h" - -#include -#include +#include "msaf-fsm.h" #include "openapi/model/content_hosting_configuration.h" #include "openapi/model/service_access_information_resource.h" #include "provisioning-session.h" #include "application-server-context.h" #include "service-access-information.h" +#include "response-cache-control.h" #ifdef __cplusplus extern "C" { @@ -39,8 +45,18 @@ extern int __msaf_log_domain; typedef struct msaf_configuration_s { int open5gsIntegration_flag; ogs_list_t applicationServers_list; - char *contentHostingConfiguration; - char *certificate; + ogs_list_t server_addr_list; // Nodes for this list are of type msaf_sbi_addr_t * + char *certificateManager; + + ogs_sockaddr_t *m1_server_sockaddr; + ogs_sockaddr_t *m5_server_sockaddr; + ogs_sockaddr_t *maf_mgmt_server_sockaddr; + ogs_sockaddr_t *sbi_server_sockaddr; + ogs_sockaddr_t *sbi_server_sockaddr_v6; + ogs_sockaddr_t *m1_server_sockaddr_v6; + ogs_sockaddr_t *m5_server_sockaddr_v6; + ogs_sockaddr_t *maf_mgmt_server_sockaddr_v6; + msaf_server_response_cache_control_t *server_response_cache_control; int number_of_application_servers; } msaf_configuration_t; @@ -49,14 +65,22 @@ typedef struct msaf_context_s { ogs_hash_t *provisioningSessions_map; ogs_list_t application_server_states; ogs_hash_t *content_hosting_configuration_file_map; + msaf_fsm_t msaf_fsm; + char server_name[NI_MAXHOST]; } msaf_context_t; +typedef struct msaf_server_addr_s { + ogs_lnode_t node; + const char *server_addr; +} msaf_context_server_addr_t; + extern void msaf_context_init(void); extern void msaf_context_final(void); extern msaf_context_t *msaf_self(void); extern int msaf_context_parse_config(void); extern void msaf_context_provisioning_session_free(msaf_provisioning_session_t *provisioning_session); +extern int msaf_context_server_name_set(void); #ifdef __cplusplus } diff --git a/src/5gmsaf/event.c b/src/5gmsaf/event.c index bcb845b..e742f10 100644 --- a/src/5gmsaf/event.c +++ b/src/5gmsaf/event.c @@ -27,3 +27,27 @@ const char *msaf_event_get_name(msaf_event_t *e) return ogs_event_get_name(&e->h); } + + +int check_event_addresses(msaf_event_t *e, ogs_sockaddr_t *sockaddr_v4, ogs_sockaddr_t *sockaddr_v6) +{ + ogs_sbi_stream_t *stream = e->h.sbi.data; + + if (stream) { + ogs_sbi_server_t *server; + + server = ogs_sbi_server_from_stream(stream); + ogs_assert(server); + + if(sockaddr_v4 && (ogs_sockaddr_is_equal(server->node.addr, sockaddr_v4) || (sockaddr_v6 && ogs_sockaddr_is_equal(server->node.addr, sockaddr_v6)) )){ + + return 1; + } + + } + return 0; + +} + +/* vim:ts=8:sts=4:sw=4:expandtab: +*/ diff --git a/src/5gmsaf/event.h b/src/5gmsaf/event.h index 81d3bcf..b61212b 100644 --- a/src/5gmsaf/event.h +++ b/src/5gmsaf/event.h @@ -13,11 +13,7 @@ program. If this file is missing then the license can be retrieved from #include "ogs-proto.h" #include "ogs-sbi.h" - - -//#ifndef MSAF_CONTEXT_H #include "context.h" -//#endif #ifdef __cplusplus extern "C" { @@ -34,12 +30,25 @@ typedef enum { } msaf_event_e; +typedef enum { + MSAF_SBI_SERVER , + MSAF_M1_SERVER, + MSAF_M5_SERVER, + MSAF_MAF_MGMT_SERVER, + +} msaf_server_type_e; + typedef struct msaf_application_server_state_node_s msaf_application_server_state_node_t; +typedef struct purge_resource_id_node_s purge_resource_id_node_t; + typedef struct msaf_event_s { ogs_event_t h; int local_id; msaf_application_server_state_node_t *application_server_state; + purge_resource_id_node_t *purge_node; + ogs_sbi_message_t *message; + ogs_pkbuf_t *pkbuf; @@ -55,6 +64,8 @@ typedef struct msaf_event_s { OGS_STATIC_ASSERT(OGS_EVENT_SIZE >= sizeof(msaf_event_t)); extern const char *msaf_event_get_name(msaf_event_t *e); +extern int check_event_addresses(msaf_event_t *e, ogs_sockaddr_t *sockaddr_v4, ogs_sockaddr_t *sockaddr_v6); + #ifdef __cplusplus } diff --git a/src/5gmsaf/generator-5gmsaf b/src/5gmsaf/generator-5gmsaf index 7bac86a..e116c46 100755 --- a/src/5gmsaf/generator-5gmsaf +++ b/src/5gmsaf/generator-5gmsaf @@ -29,7 +29,7 @@ scriptdir=`cd "$scriptdir"; pwd` # Command line option defaults default_branch='REL-17' -default_apis="TS26512_M1_ProvisioningSessions TS26512_M1_ContentHostingProvisioning TS26512_M5_ServiceAccessInformation" +default_apis="TS26512_M1_ProvisioningSessions TS26512_M1_ContentHostingProvisioning TS26512_M1_ServerCertificatesProvisioning TS26512_M1_ContentProtocolsDiscovery M3_ContentHostingProvisioning M3_ServerCertificatesProvisioning TS26512_M5_ServiceAccessInformation Maf_Management" # Parse command line arguments ARGS=`getopt -n "$scriptname" -o 'a:b:h' -l 'api:,branch:,help' -s sh -- "$@"` @@ -109,6 +109,13 @@ destdir=`realpath -m "$scriptdir/openapi"` openapi_gen_dir=`realpath "$scriptdir/../../subprojects/open5gs/lib/sbi/support/20210629/openapi-generator"` sed "s@^templateDir:.*@templateDir: \"${openapi_gen_dir}/templates\"@" $openapi_gen_dir/config.yaml > $scriptdir/config.yaml +cp "${scriptdir}/"*.mustache "${openapi_gen_dir}/templates/" +cat >> $scriptdir/config.yaml << EOF +files: + api-info-head.mustache: + templateType: API + destinationFilename: -info.h +EOF # call the common generate_openapi script if [ ! -d "$scriptdir/openapi" ]; then diff --git a/src/5gmsaf/hash.c b/src/5gmsaf/hash.c new file mode 100644 index 0000000..30e32cb --- /dev/null +++ b/src/5gmsaf/hash.c @@ -0,0 +1,32 @@ +/* +License: 5G-MAG Public License (v1.0) +Author: Dev Audsin +Copyright: (C) 2022 British Broadcasting Corporation + +For full license terms please see the LICENSE file distributed with this +program. If this file is missing then the license can be retrieved from +https://drive.google.com/file/d/1cinCiA778IErENZ3JN52VFW-1ffHpx7Z/view +*/ + +#include "hash.h" + +const char *calculate_hash(const char *buf) { + unsigned char *result = NULL; + size_t result_len; + gnutls_datum_t data; + static char hash[1024]; + size_t i; + + result_len = gnutls_hash_get_len(GNUTLS_DIG_SHA256); + data.data = (unsigned char *)buf; + data.size = strlen(buf); + result = ogs_calloc(1, result_len); + gnutls_fingerprint(GNUTLS_DIG_SHA256, &data, result, &result_len); + for (i = 0; i < result_len; i++) + { + sprintf(&(hash[i*2]), "%02x", result[i]); + } + hash[sizeof (hash) - 1] = '\0'; + ogs_free(result); + return hash; +} diff --git a/src/5gmsaf/hash.h b/src/5gmsaf/hash.h new file mode 100644 index 0000000..5fa7685 --- /dev/null +++ b/src/5gmsaf/hash.h @@ -0,0 +1,29 @@ +/* +License: 5G-MAG Public License (v1.0) +Author: Dev Audsin +Copyright: (C) 2022 British Broadcasting Corporation + +For full license terms please see the LICENSE file distributed with this +program. If this file is missing then the license can be retrieved from +https://drive.google.com/file/d/1cinCiA778IErENZ3JN52VFW-1ffHpx7Z/view +*/ + +#ifndef MSAF_HASH_H +#define MSAF_HASH_H + +#include +#include +#include "ogs-app.h" +#include "context.h" + +#ifdef __cplusplus +extern "C" { +#endif + +extern const char *calculate_hash(const char *buf); + +#ifdef __cplusplus +} +#endif + +#endif /* MSAF_HASH_H */ diff --git a/src/5gmsaf/headers.c b/src/5gmsaf/headers.c new file mode 100644 index 0000000..e4d34da --- /dev/null +++ b/src/5gmsaf/headers.c @@ -0,0 +1,185 @@ +/* + * License: 5G-MAG Public License (v1.0) + * Author: David Waring + * Copyright: (C) 2023 British Broadcasting Corporation + * + * For full license terms please see the LICENSE file distributed with this + * program. If this file is missing then the license can be retrieved from + * https://drive.google.com/file/d/1cinCiA778IErENZ3JN52VFW-1ffHpx7Z/view + */ + +#include "ogs-core.h" + +#include "headers.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* nf_headers_t methods */ +nf_headers_t *nf_headers_new() +{ + nf_headers_t *hdrs; + + hdrs = ogs_calloc(1, sizeof(nf_headers_t)); + hdrs->hdrs = ogs_hash_make(); + + return hdrs; +} + +static int headers_hash_do_free(void *rec, const void *key, int klen, const void *value) +{ + ogs_free((void*)value); + ogs_hash_set((ogs_hash_t*)rec, key, klen, NULL); + ogs_free((void*)key); + return 1; +} + +void nf_headers_free(nf_headers_t *headers) +{ + if (headers->hdrs) { + ogs_hash_do(headers_hash_do_free, headers->hdrs, headers->hdrs); + ogs_hash_destroy(headers->hdrs); + } + ogs_free(headers); +} + +const char *nf_headers_get(nf_headers_t *headers, const char *fieldname) +{ + nf_headers_iter_t *iter; + + iter = nf_headers_iter_find(headers, fieldname); + if (iter) { + const char *ret; + ret = nf_headers_iter_value(iter); + nf_headers_iter_free(iter); + return ret; + } + return NULL; +} + +int nf_headers_set(nf_headers_t *headers, const char *fieldname, const char *value) +{ + nf_headers_iter_t *iter; + iter = nf_headers_iter_find(headers, fieldname); + if (iter) { + ogs_hash_set(headers->hdrs, nf_headers_iter_fieldname(iter), OGS_HASH_KEY_STRING, NULL); + nf_headers_iter_free(iter); + } + ogs_hash_set(headers->hdrs, ogs_strdup(fieldname), OGS_HASH_KEY_STRING, ogs_strdup(value)); + return 1; +} + +int nf_headers_add(nf_headers_t *headers, const char *fieldname, const char *value) { + nf_headers_iter_t *iter; + iter = nf_headers_iter_find(headers, fieldname); + if (iter) { + char *new_value = ogs_msprintf("%s, %s", nf_headers_iter_value(iter), value); + ogs_hash_set(headers->hdrs, nf_headers_iter_fieldname(iter), OGS_HASH_KEY_STRING, new_value); + } else { + ogs_hash_set(headers->hdrs, ogs_strdup(fieldname), OGS_HASH_KEY_STRING, ogs_strdup(value)); + } + return 1; +} + +int nf_headers_delete(nf_headers_t *headers, const char *fieldname) +{ + nf_headers_iter_t *iter; + iter = nf_headers_iter_find(headers, fieldname); + if (iter) { + ogs_hash_set(headers->hdrs, nf_headers_iter_fieldname(iter), OGS_HASH_KEY_STRING, NULL); + return 1; + } + return 0; +} + +int nf_headers_clear(nf_headers_t *headers) +{ + ogs_hash_clear(headers->hdrs); + return 1; +} + +int nf_headers_count(nf_headers_t *headers) +{ + return ogs_hash_count(headers->hdrs); +} + +typedef struct hdrs_hash_do_data_s { + nf_headers_do_callback_fn_t *fn; + nf_headers_t *headers; + void *user_data; +} hdrs_hash_do_data_t; + +static int _hash_do_callback(void *rec, const void *key, int key_len, const void *value) +{ + hdrs_hash_do_data_t *data = (hdrs_hash_do_data_t*)rec; + + return data->fn((const char *)key, (const char *)value, data->user_data); +} + +int nf_headers_do(nf_headers_t *headers, nf_headers_do_callback_fn_t *fn, void *user_data) +{ + hdrs_hash_do_data_t data = {fn, headers, user_data}; + + return ogs_hash_do(_hash_do_callback, &data, headers->hdrs); +} + +/* Iterator for nf_headers_t */ +nf_headers_iter_t *nf_headers_iter_new(nf_headers_t *headers) +{ + nf_headers_iter_t *iter; + + iter = ogs_calloc(1, sizeof(nf_headers_iter_t)); + iter->ptr = ogs_hash_first(headers->hdrs); + if (!iter->ptr) { + ogs_free(iter); + return NULL; + } + return iter; +} + +nf_headers_iter_t *nf_headers_iter_find(nf_headers_t *headers, const char *fieldname) +{ + nf_headers_iter_t *iter; + + for (iter = nf_headers_iter_new(headers); iter; iter = nf_headers_iter_next(iter)) { + if (!strcasecmp(nf_headers_iter_fieldname(iter), fieldname)) { + return iter; + } + } + return NULL; +} + +nf_headers_iter_t *nf_headers_iter_next(nf_headers_iter_t *iter) +{ + iter->ptr = ogs_hash_next(iter->ptr); + if (!iter->ptr) { + ogs_free(iter); + return NULL; + } + return iter; +} + +const char *nf_headers_iter_fieldname(nf_headers_iter_t *iter) +{ + if (!iter) return NULL; + return (const char *)ogs_hash_this_key(iter->ptr); +} + +const char *nf_headers_iter_value(nf_headers_iter_t *iter) +{ + if (!iter) return NULL; + return (const char *)ogs_hash_this_val(iter->ptr); +} + +void nf_headers_iter_free(nf_headers_iter_t *iter) +{ + ogs_free(iter); +} + +#ifdef __cplusplus +} +#endif + +/* vim:ts=8:sts=4:sw=4:expandtab: + */ diff --git a/src/5gmsaf/headers.h b/src/5gmsaf/headers.h new file mode 100644 index 0000000..71da4bf --- /dev/null +++ b/src/5gmsaf/headers.h @@ -0,0 +1,57 @@ +/* + * License: 5G-MAG Public License (v1.0) + * Author: David Waring + * Copyright: (C) 2023 British Broadcasting Corporation + * + * For full license terms please see the LICENSE file distributed with this + * program. If this file is missing then the license can be retrieved from + * https://drive.google.com/file/d/1cinCiA778IErENZ3JN52VFW-1ffHpx7Z/view + */ + +#ifndef NF_HEADERS_H +#define NF_HEADERS_H + +#include "ogs-core.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct nf_headers_s { + ogs_hash_t *hdrs; +} nf_headers_t; + +typedef struct nf_headers_iter_s { + ogs_hash_index_t *ptr; +} nf_headers_iter_t; + +typedef int (nf_headers_do_callback_fn_t)(const char *fieldname, const char *value, void *data); + +/* nf_headers_t methods */ +extern nf_headers_t *nf_headers_new(); +extern void nf_headers_free(nf_headers_t *); + +extern const char *nf_headers_get(nf_headers_t *headers, const char *fieldname); +extern int nf_headers_set(nf_headers_t *headers, const char *fieldname, const char *value); +extern int nf_headers_add(nf_headers_t *headers, const char *fieldname, const char *value); +extern int nf_headers_delete(nf_headers_t *headers, const char *fieldname); + +extern int nf_headers_clear(nf_headers_t *headers); + +extern int nf_headers_count(nf_headers_t *headers); + +extern int nf_headers_do(nf_headers_t *headers, nf_headers_do_callback_fn_t *fn, void *data); + +/* Iterator for nf_headers_t */ +extern nf_headers_iter_t *nf_headers_iter_new(nf_headers_t *headers); +extern nf_headers_iter_t *nf_headers_iter_next(nf_headers_iter_t *); +extern const char *nf_headers_iter_fieldname(nf_headers_iter_t *); +extern const char *nf_headers_iter_value(nf_headers_iter_t *); +extern nf_headers_iter_t *nf_headers_iter_find(nf_headers_t *headers, const char *fieldname); +extern void nf_headers_iter_free(nf_headers_iter_t *); + +#ifdef __cplusplus +} +#endif + +#endif /* NF_HEADERS_H */ diff --git a/src/5gmsaf/init.c b/src/5gmsaf/init.c index c0274c2..60ca878 100644 --- a/src/5gmsaf/init.c +++ b/src/5gmsaf/init.c @@ -10,11 +10,12 @@ program. If this file is missing then the license can be retrieved from #include "context.h" #include "sbi-path.h" - #include "init.h" static ogs_thread_t *thread; + static void msaf_main(void *data); +static int msaf_set_time(void); static int initialized = 0; @@ -22,6 +23,9 @@ int msaf_initialize() { int rv; + rv = msaf_set_time(); + if(rv != 0) return OGS_ERROR; + ogs_sbi_context_init(); msaf_context_init(); @@ -39,8 +43,7 @@ int msaf_initialize() return OGS_ERROR; } - rv = ogs_log_config_domain( - ogs_app()->logger.domain, ogs_app()->logger.level); + rv = ogs_log_config_domain(ogs_app()->logger.domain, ogs_app()->logger.level); if (rv != OGS_OK) return rv; rv = msaf_sbi_open(); @@ -92,6 +95,17 @@ static void msaf_main(void *data) int rv; ogs_fsm_init(&msaf_sm, msaf_state_initial, msaf_state_final, 0); + + /* + ogs_fsm_init(&msaf_self()->msaf_fsm.msaf_m1_sm, msaf_m1_state_initial, msaf_m1_state_final, 0); + ogs_fsm_init(&msaf_self()->msaf_fsm.msaf_m5_sm, msaf_m5_state_initial, msaf_m5_state_final, 0); + if(msaf_self()->config.sbi_server_sockaddr || msaf_self()->config.sbi_server_sockaddr_v6) { + ogs_fsm_init(&msaf_self()->msaf_fsm.msaf_sbi_sm, msaf_sbi_state_initial, msaf_sbi_state_final, 0); + } + if(msaf_self()->config.maf_mgmt_server_sockaddr || msaf_self()->config.maf_mgmt_server_sockaddr_v6) { + ogs_fsm_init(&msaf_self()->msaf_fsm.msaf_maf_mgmt_sm, msaf_maf_mgmt_state_initial, msaf_maf_mgmt_state_final, 0); + } + */ for ( ;; ) { ogs_pollset_poll(ogs_app()->pollset, @@ -112,11 +126,45 @@ static void msaf_main(void *data) break; ogs_assert(e); + ogs_fsm_dispatch(&msaf_sm, e); + /* + rv = get_server_type_from_event(e); + if (rv == MSAF_M1_SERVER) { + ogs_fsm_dispatch(&msaf_self()->msaf_fsm.msaf_m1_sm, e); + } + if (rv == MSAF_M5_SERVER) { + ogs_fsm_dispatch(&msaf_self()->msaf_fsm.msaf_m5_sm, e); + } + if(rv == MSAF_MAF_MGMT_SERVER) { + ogs_fsm_dispatch(&msaf_self()->msaf_fsm.msaf_maf_mgmt_sm, e); + } + if (rv == MSAF_SBI_SERVER) { + ogs_fsm_dispatch(&msaf_self()->msaf_fsm.msaf_sbi_sm, e); + } + */ ogs_event_free(e); } } done: - + ogs_fsm_fini(&msaf_sm, 0); + +} + +static int msaf_set_time(void) +{ + if(ogs_env_set("TZ", "GMT") != OGS_OK) + { + ogs_error("Failed to set TZ to GMT"); + return OGS_ERROR; + } + + if(ogs_env_set("LC_TIME", "C") != OGS_OK) + { + ogs_error("Failed to set LC_TIME to C"); + return OGS_ERROR; + } + return OGS_OK; + } diff --git a/src/5gmsaf/meson.build b/src/5gmsaf/meson.build index 574c970..bd3657f 100644 --- a/src/5gmsaf/meson.build +++ b/src/5gmsaf/meson.build @@ -17,11 +17,23 @@ python3_exe = open5gs_project.get_variable('python3_exe') mkdir_p = open5gs_project.get_variable('mkdir_p') install_conf = open5gs_project.get_variable('install_conf') +fs = import('fs') + libmsaf_dist_sources = files(''' context.c context.h event.c event.h + server.h + server.c + certmgr.c + certmgr.h + hash.h + hash.c + headers.c + headers.h + response-cache-control.h + response-cache-control.c provisioning-session.h provisioning-session.c application-server-context.h @@ -32,8 +44,13 @@ libmsaf_dist_sources = files(''' sbi-path.h service-access-information.h service-access-information.c - msaf-sm.c + msaf-fsm.h + msaf-fsm.c msaf-sm.h + msaf-sm.c + msaf-mgmt-sm.c + msaf-m1-sm.c + msaf-m5-sm.c utilities.h utilities.c init.c @@ -172,7 +189,31 @@ libmsaf_gen_sources = ''' '''.split() gen_5gmsaf_openapi = find_program('sh') -openapi_gen_result = run_command([gen_5gmsaf_openapi,'-c','$MESON_SOURCE_ROOT/$MESON_SUBDIR/generator-5gmsaf'], check: true, capture: false) +openapi_gen_result = run_command([gen_5gmsaf_openapi,'-c','$MESON_SOURCE_ROOT/$MESON_SUBDIR/generator-5gmsaf', '-b', 'REL-'+fiveg_api_release], check: true, capture: false) + +json_file = files(['ContentProtocolsDiscovery_body.json']) +file_content = '' +data = fs.read(json_file).split('\n') + +foreach line : data + file_content = file_content + ''.join(line + '\\n') +endforeach + +file_hash = fs.hash(json_file, 'sha256') +json_file_time = run_command(['stat', '-c', '%Y', json_file]) +file_time = json_file_time.stdout().strip() + +content_hosting_discovery_protocols_conf = configuration_data() +content_hosting_discovery_protocols_conf.set_quoted('CONTENT_PROTOCOLS_DISCOVERY_JSON', file_content) +content_hosting_discovery_protocols_conf.set_quoted('CONTENT_PROTOCOLS_DISCOVERY_JSON_HASH', file_hash) +content_hosting_discovery_protocols_conf.set('CONTENT_PROTOCOLS_DISCOVERY_JSON_TIME', file_time) +libmsaf_gen_sources += [configure_file(output : 'ContentProtocolsDiscovery_body.h', configuration : content_hosting_discovery_protocols_conf)] + +version_conf = configuration_data() +version_conf.set_quoted('MSAF_NAME', meson.project_name()) +version_conf.set_quoted('MSAF_VERSION', meson.project_version()) +version_conf.set_quoted('FIVEG_API_RELEASE', fiveg_api_release) +libmsaf_gen_sources += [configure_file(output : 'msaf-version.h', configuration : version_conf)] libmsaf_sources = libmsaf_dist_sources + libmsaf_gen_sources @@ -206,7 +247,9 @@ executable('open5gs-msafd', install : true) meson.add_install_script(python3_exe, '-c', mkdir_p.format(open5gs_sysconfdir)) +conf_configuration = configuration_data() +conf_configuration.set('default-certmgr', get_option('prefix') / self_signed_certmgr_runtime) foreach conf_file : msaf_config_source - gen = configure_file(input : conf_file, copy : true, output : conf_file) + gen = configure_file(input : conf_file + '.in', configuration : conf_configuration, output : conf_file) meson.add_install_script(python3_exe, '-c', install_conf.format(gen, open5gs_sysconfdir)) endforeach diff --git a/src/5gmsaf/msaf-fsm.c b/src/5gmsaf/msaf-fsm.c new file mode 100644 index 0000000..29d909e --- /dev/null +++ b/src/5gmsaf/msaf-fsm.c @@ -0,0 +1,30 @@ +/* +License: 5G-MAG Public License (v1.0) +Author: Dev Audsin +Copyright: (C) 2022 British Broadcasting Corporation + +For full license terms please see the LICENSE file distributed with this +program. If this file is missing then the license can be retrieved from +https://drive.google.com/file/d/1cinCiA778IErENZ3JN52VFW-1ffHpx7Z/view +*/ + +#include "msaf-sm.h" +#include "msaf-fsm.h" + +void msaf_fsm_init(void) { + + ogs_fsm_init(&msaf_self()->msaf_fsm.msaf_m1_sm, msaf_m1_state_initial, msaf_m1_state_final, 0); + ogs_fsm_init(&msaf_self()->msaf_fsm.msaf_m5_sm, msaf_m5_state_initial, msaf_m5_state_final, 0); + if(msaf_self()->config.maf_mgmt_server_sockaddr || msaf_self()->config.maf_mgmt_server_sockaddr_v6) { + ogs_fsm_init(&msaf_self()->msaf_fsm.msaf_maf_mgmt_sm, msaf_maf_mgmt_state_initial, msaf_maf_mgmt_state_final, 0); + } + +} + +void msaf_fsm_fini(void) { + ogs_fsm_fini(&msaf_self()->msaf_fsm.msaf_m1_sm, 0); + ogs_fsm_fini(&msaf_self()->msaf_fsm.msaf_m5_sm, 0); + if(msaf_self()->config.maf_mgmt_server_sockaddr || msaf_self()->config.maf_mgmt_server_sockaddr_v6) { + ogs_fsm_fini(&msaf_self()->msaf_fsm.msaf_maf_mgmt_sm, 0); + } +} diff --git a/src/5gmsaf/msaf-fsm.h b/src/5gmsaf/msaf-fsm.h new file mode 100644 index 0000000..ae7441e --- /dev/null +++ b/src/5gmsaf/msaf-fsm.h @@ -0,0 +1,34 @@ +/* +License: 5G-MAG Public License (v1.0) +Copyright: (C) 2022 British Broadcasting Corporation + +For full license terms please see the LICENSE file distributed with this +program. If this file is missing then the license can be retrieved from +https://drive.google.com/file/d/1cinCiA778IErENZ3JN52VFW-1ffHpx7Z/view +*/ + +#ifndef MSAF_FSM_H +#define MSAF_FSM_H + +#include "ogs-app.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct msaf_fsm_s { + ogs_fsm_t msaf_sbi_sm; + ogs_fsm_t msaf_m1_sm; + ogs_fsm_t msaf_m5_sm; + ogs_fsm_t msaf_maf_mgmt_sm; +} msaf_fsm_t; + +extern void msaf_fsm_init(void); +extern void msaf_fsm_fini(void); + + +#ifdef __cplusplus +} +#endif + +#endif /* MSAF_FSM_H */ diff --git a/src/5gmsaf/msaf-m1-sm.c b/src/5gmsaf/msaf-m1-sm.c new file mode 100644 index 0000000..39d7ba4 --- /dev/null +++ b/src/5gmsaf/msaf-m1-sm.c @@ -0,0 +1,1859 @@ +/* + * License: 5G-MAG Public License (v1.0) + * Author: Dev Audsin + * Copyright: (C) 2022 British Broadcasting Corporation + * + * For full license terms please see the LICENSE file distributed with this + * program. If this file is missing then the license can be retrieved from + * https://drive.google.com/file/d/1cinCiA778IErENZ3JN52VFW-1ffHpx7Z/view + */ + + +#include "ogs-sbi.h" +#include "sbi-path.h" +#include "context.h" +#include "certmgr.h" +#include "server.h" +#include "response-cache-control.h" +#include "msaf-version.h" +#include "msaf-sm.h" +#include "ContentProtocolsDiscovery_body.h" +#include "openapi/api/TS26512_M1_ProvisioningSessionsAPI-info.h" +#include "openapi/api/TS26512_M1_ServerCertificatesProvisioningAPI-info.h" +#include "openapi/api/TS26512_M1_ContentHostingProvisioningAPI-info.h" +#include "openapi/api/M3_ServerCertificatesProvisioningAPI-info.h" +#include "openapi/api/M3_ContentHostingProvisioningAPI-info.h" +#include "openapi/api/TS26512_M1_ContentProtocolsDiscoveryAPI-info.h" +#include "openapi/api/Maf_ManagementAPI-info.h" + +const nf_server_interface_metadata_t +m1_provisioningsession_api_metadata = { + M1_PROVISIONINGSESSIONS_API_NAME, + M1_PROVISIONINGSESSIONS_API_VERSION +}; + +const nf_server_interface_metadata_t +m1_contenthostingprovisioning_api_metadata = { + M1_CONTENTHOSTINGPROVISIONING_API_NAME, + M1_CONTENTHOSTINGPROVISIONING_API_VERSION +}; + +const nf_server_interface_metadata_t +m1_contentprotocolsdiscovery_api_metadata = { + M1_CONTENTPROTOCOLSDISCOVERY_API_NAME, + M1_CONTENTPROTOCOLSDISCOVERY_API_VERSION +}; + +const nf_server_interface_metadata_t +m1_servercertificatesprovisioning_api_metadata = { + M1_SERVERCERTIFICATESPROVISIONING_API_NAME, + M1_SERVERCERTIFICATESPROVISIONING_API_VERSION +}; + +const nf_server_interface_metadata_t +m3_contenthostingprovisioning_api_metatdata = { + M3_CONTENTHOSTINGPROVISIONING_API_NAME, + M3_CONTENTHOSTINGPROVISIONING_API_VERSION +}; + +const nf_server_interface_metadata_t +maf_management_api_metadata = { + MAF_MANAGEMENT_API_NAME, + MAF_MANAGEMENT_API_VERSION +}; + +void msaf_m1_state_initial(ogs_fsm_t *s, msaf_event_t *e) +{ + msaf_sm_debug(e); + + ogs_assert(s); + + OGS_FSM_TRAN(s, &msaf_m1_state_functional); +} + +void msaf_m1_state_final(ogs_fsm_t *s, msaf_event_t *e) +{ + msaf_sm_debug(e); + + ogs_assert(s); +} + +void msaf_m1_state_functional(ogs_fsm_t *s, msaf_event_t *e) +{ + int rv; + + ogs_sbi_stream_t *stream = NULL; + ogs_sbi_request_t *request = NULL; + ogs_sbi_response_t *response = NULL; + ogs_sbi_message_t message; + + msaf_sm_debug(e); + + char *nf_name = ogs_msprintf("5GMSdAF-%s", msaf_self()->server_name); + const nf_server_app_metadata_t app_metadata = { MSAF_NAME, MSAF_VERSION, nf_name}; + const nf_server_interface_metadata_t *m1_provisioningsession_api = &m1_provisioningsession_api_metadata; + const nf_server_interface_metadata_t *m1_contenthostingprovisioning_api = &m1_contenthostingprovisioning_api_metadata; + const nf_server_interface_metadata_t *m1_contentprotocolsdiscovery_api = &m1_contentprotocolsdiscovery_api_metadata; + const nf_server_interface_metadata_t *m1_servercertificatesprovisioning_api = &m1_servercertificatesprovisioning_api_metadata; + const nf_server_interface_metadata_t *m3_contenthostingprovisioning_api = &m3_contenthostingprovisioning_api_metatdata; + const nf_server_interface_metadata_t *maf_management_api = &maf_management_api_metadata; + const nf_server_app_metadata_t *app_meta = &app_metadata; + + ogs_assert(s); + + switch (e->h.id) { + case OGS_FSM_ENTRY_SIG: + ogs_info("[%s] MSAF M1 Running", ogs_sbi_self()->nf_instance->id); + + break; + + case OGS_FSM_EXIT_SIG: + break; + + case OGS_EVENT_SBI_SERVER: + request = e->h.sbi.request; + ogs_assert(request); + stream = e->h.sbi.data; + ogs_assert(stream); + message = *(e->message); + + SWITCH(message.h.service.name) + CASE("3gpp-m1") + if (strcmp(message.h.api.version, "v2") != 0) { + char *error; + error = ogs_msprintf("Version [%s] not supported", message.h.api.version); + ogs_error("%s", error); + ogs_assert(true == nf_server_send_error(stream, OGS_SBI_HTTP_STATUS_BAD_REQUEST, 1, NULL, "Not supported version", error, NULL, NULL, app_meta)); + + ogs_sbi_message_free(&message); + break; + } + if (!message.h.resource.component[0]) { + char *error; + error = ogs_strdup("Protocol on M1 requires a resource"); + ogs_error("%s", error); + ogs_assert(true == nf_server_send_error(stream, 404, 1, NULL, "No resource given", error, NULL, NULL, app_meta)); + ogs_sbi_message_free(&message); + break; + } + + SWITCH(message.h.resource.component[0]) + CASE("provisioning-sessions") + SWITCH(message.h.method) + CASE(OGS_SBI_HTTP_METHOD_POST) + + if (message.h.resource.component[1] && message.h.resource.component[2] && message.h.resource.component[3] && !message.h.resource.component[4]) { + msaf_provisioning_session_t *msaf_provisioning_session; + + if (!strcmp(message.h.resource.component[2],"content-hosting-configuration") && !strcmp(message.h.resource.component[3],"purge")) { + ogs_hash_index_t *hi; + for (hi = ogs_hash_first(request->http.headers); + hi; hi = ogs_hash_next(hi)) { + if (!ogs_strcasecmp(ogs_hash_this_key(hi), OGS_SBI_CONTENT_TYPE)) { + if (ogs_strcasecmp(ogs_hash_this_val(hi), "application/x-www-form-urlencoded")) { + char *err = NULL; + const char *type; + type = (const char *)ogs_hash_this_val(hi); + asprintf(&err, "Unsupported Media Type: received type: %s, should have been application/x-www-form-urlencoded", type); + ogs_error("%s", err); + + ogs_assert(true == nf_server_send_error(stream, 415, 3, &message, "Unsupported Media Type.", err, NULL, m1_contenthostingprovisioning_api, app_meta)); + ogs_sbi_message_free(&message); + return; + + } + } + } + msaf_provisioning_session = msaf_provisioning_session_find_by_provisioningSessionId(message.h.resource.component[1]); + if(msaf_provisioning_session) { + // process the POST body + purge_resource_id_node_t *purge_cache; + msaf_application_server_state_ref_node_t *as_state_ref; + assigned_provisioning_sessions_node_t *assigned_provisioning_sessions_resource; + m1_purge_information_t *m1_purge_info = ogs_calloc(1, sizeof(m1_purge_information_t)); + m1_purge_info->m1_stream = stream; + m1_purge_info->m1_message = message; + + ogs_list_for_each(&msaf_provisioning_session->application_server_states, as_state_ref) { + msaf_application_server_state_node_t *as_state = as_state_ref->as_state; + if (as_state->application_server && as_state->application_server->canonicalHostname) { + ogs_list_for_each(&as_state->assigned_provisioning_sessions,assigned_provisioning_sessions_resource){ + if(assigned_provisioning_sessions_resource->assigned_provisioning_session == msaf_provisioning_session) { + + purge_cache = ogs_calloc(1, sizeof(purge_resource_id_node_t)); + ogs_assert(purge_cache); + purge_cache->provisioning_session_id = ogs_strdup(assigned_provisioning_sessions_resource->assigned_provisioning_session->provisioningSessionId); + + purge_cache->m1_purge_info = m1_purge_info; + m1_purge_info->refs++; + if(request->http.content) + purge_cache->purge_regex = ogs_strdup(request->http.content); + else + purge_cache->purge_regex = NULL; + + if (ogs_list_first(&as_state->purge_content_hosting_cache) == NULL) + ogs_list_init(&as_state->purge_content_hosting_cache); + + ogs_list_add(&as_state->purge_content_hosting_cache, purge_cache); + } else { + char *err = NULL; + asprintf(&err,"Provisioning Session [%s] is not assigned to an Application Server", message.h.resource.component[1]); + ogs_error("%s", err); + ogs_assert(true == nf_server_send_error(stream, 500, 3, &message, "Provisioning session is not assigned to an Application Server.", err, NULL, m1_contenthostingprovisioning_api, app_meta)); + } + } + } else { + char *err = NULL; + asprintf(&err,"Provisioning Session [%s] : Unable to get information about Application Server", message.h.resource.component[1]); + ogs_error("%s", err); + ogs_assert(true == nf_server_send_error(stream, 500, 3, &message, "Unable to get information about Application Server", err, NULL, m1_contenthostingprovisioning_api, app_meta)); + } + + next_action_for_application_server(as_state); + } + if (m1_purge_info->refs == 0) { + ogs_free(m1_purge_info); + // Send 204 back to M1 client + ogs_sbi_response_t *response; + response = nf_server_new_response(NULL, NULL, 0, NULL, 0, NULL, m1_contenthostingprovisioning_api, app_meta); + nf_server_populate_response(response, 0, NULL, 204); + ogs_assert(response); + ogs_assert(true == ogs_sbi_server_send_response(stream, response)); + } + } else { + char *err = NULL; + asprintf(&err,"Provisioning session [%s] does not exist.", message.h.resource.component[1]); + ogs_error("%s", err); + ogs_assert(true == nf_server_send_error(stream, 404, 3, &message, "Provisioning session does not exist.", err, NULL, m1_contenthostingprovisioning_api, app_meta)); + } + + } + + } else if (message.h.resource.component[1] && message.h.resource.component[2] && !message.h.resource.component[3]) { + msaf_provisioning_session_t *msaf_provisioning_session; + if (!strcmp(message.h.resource.component[2],"content-hosting-configuration")) { + msaf_provisioning_session = msaf_provisioning_session_find_by_provisioningSessionId(message.h.resource.component[1]); + if(msaf_provisioning_session) { + // process the POST body + cJSON *entry; + int rv; + cJSON *chc; + cJSON *content_hosting_config = cJSON_Parse(request->http.content); + char *txt = cJSON_Print(content_hosting_config); + ogs_debug("body:%s", request->http.content); + ogs_debug("txt:%s", txt); + + cJSON_ArrayForEach(entry, content_hosting_config) { + if(!strcmp(entry->string, "entryPointPath")){ + if(!uri_relative_check(entry->valuestring)) { + char *err = NULL; + asprintf(&err,"While creating the Content Hosting Configuration for the Provisioning Session [%s], entryPointPath does not match the regular expression [%s].",message.h.resource.component[1], entry->valuestring ); + ogs_error("%s", err); + ogs_assert(true == nf_server_send_error(stream, 422, 2, &message, "Entry Point Path does not match the regular expression.", err, NULL, m1_contenthostingprovisioning_api, app_meta)); + cJSON_Delete(content_hosting_config); + break; + } + } + } + + if(msaf_provisioning_session->contentHostingConfiguration) { + OpenAPI_content_hosting_configuration_free(msaf_provisioning_session->contentHostingConfiguration); + msaf_provisioning_session->contentHostingConfiguration = NULL; + + } + + if (msaf_provisioning_session->serviceAccessInformation) { + OpenAPI_service_access_information_resource_free(msaf_provisioning_session->serviceAccessInformation); + msaf_provisioning_session->serviceAccessInformation = NULL; + } + + rv = msaf_distribution_create(content_hosting_config, msaf_provisioning_session); + + if(rv){ + + ogs_debug("Content Hosting Configuration created successfully"); + if (msaf_application_server_state_set_on_post(msaf_provisioning_session)) { + chc = msaf_get_content_hosting_configuration_by_provisioning_session_id(message.h.resource.component[1]); + if (chc != NULL) { + char *text; + msaf_provisioning_session = msaf_provisioning_session_find_by_provisioningSessionId(message.h.resource.component[1]); + response = nf_server_new_response(request->h.uri, "application/json", msaf_provisioning_session->contentHostingConfigurationReceived, msaf_provisioning_session->contentHostingConfigurationHash, msaf_self()->config.server_response_cache_control->m1_content_hosting_configurations_response_max_age, NULL, m1_contenthostingprovisioning_api, app_meta); + text = cJSON_Print(chc); + nf_server_populate_response(response, strlen(text), text, 201); + ogs_assert(response); + ogs_assert(true == ogs_sbi_server_send_response(stream, response)); + cJSON_Delete(chc); + cJSON_Delete(content_hosting_config); + } else { + char *err = NULL; + asprintf(&err,"Unable to retrieve the Content Hosting Configuration for the Provisioning Session [%s].", message.h.resource.component[1]); + ogs_error("%s", err); + ogs_assert(true == nf_server_send_error(stream, 404, 2, &message, "Unable to retrieve the Content Hosting Configuration.", err, NULL, m1_contenthostingprovisioning_api, app_meta)); + } + } else { + char *err = NULL; + asprintf(&err,"Verification error on Content Hosting Configuration for the Provisioning Session [%s].", message.h.resource.component[1]); + ogs_error("%s", err); + ogs_assert(true == nf_server_send_error(stream, 400, 2, &message, "Bad Content Hosting Configuration.", err, NULL, m1_contenthostingprovisioning_api, app_meta)); + } + } else { + char *err = NULL; + asprintf(&err,"Creation of the Content Hosting Configuration failed for the Provisioning Session [%s]", message.h.resource.component[1]); + ogs_error("%s", err); + ogs_assert(true == nf_server_send_error(stream, 500, 2, &message, "Creation of the Content Hosting Configuration failed.", err, NULL, m1_contenthostingprovisioning_api, app_meta)); + } + + } else { + char *err = NULL; + asprintf(&err,"Provisioning session [%s]does not exist.", message.h.resource.component[1]); + ogs_error("%s",err); + ogs_assert(true == nf_server_send_error(stream, 404, 2, &message, "Provisioning session does not exist.", err, NULL, m1_contenthostingprovisioning_api, app_meta)); + } + + } + if (!strcmp(message.h.resource.component[2],"certificates")) { + ogs_info("POST certificates"); + ogs_hash_index_t *hi; + char *canonical_domain_name; + char *cert; + int csr = 0; + + for (hi = ogs_hash_first(request->http.params); + hi; hi = ogs_hash_next(hi)) { + if (!ogs_strcasecmp(ogs_hash_this_key(hi), "csr")) { + csr = 1; + break; + } + } + + msaf_provisioning_session = msaf_provisioning_session_find_by_provisioningSessionId(message.h.resource.component[1]); + if (msaf_provisioning_session) { + msaf_application_server_node_t *msaf_as = NULL; + msaf_as = ogs_list_first(&msaf_self()->config.applicationServers_list); + canonical_domain_name = msaf_as->canonicalHostname; + ogs_info("canonical_domain_name: %s", canonical_domain_name); + + if (csr) { + msaf_certificate_t *csr_cert; + char *location; + int m1_server_certificates_response_max_age; + csr_cert = server_cert_new("newcsr", canonical_domain_name); + ogs_hash_set(msaf_provisioning_session->certificate_map, ogs_strdup(csr_cert->id), OGS_HASH_KEY_STRING, ogs_strdup(csr_cert->id)); + ogs_sbi_response_t *response; + location = ogs_msprintf("%s/%s", request->h.uri, csr_cert->id); + if(csr_cert->cache_control_max_age){ + m1_server_certificates_response_max_age = csr_cert->cache_control_max_age; + } else { + m1_server_certificates_response_max_age = msaf_self()->config.server_response_cache_control->m1_server_certificates_response_max_age; + } + response = nf_server_new_response(location, "application/x-pem-file", csr_cert->last_modified, csr_cert->server_certificate_hash, m1_server_certificates_response_max_age, NULL, m1_servercertificatesprovisioning_api, app_meta); + + nf_server_populate_response(response, strlen(csr_cert->certificate), ogs_strdup(csr_cert->certificate), 200); + + ogs_assert(response); + ogs_assert(true == ogs_sbi_server_send_response(stream, response)); + ogs_free(location); + msaf_certificate_free(csr_cert); + + break; + } + + cert = check_in_cert_list(canonical_domain_name); + if (cert != NULL) { + ogs_sbi_response_t *response; + char *location; + + ogs_hash_set(msaf_provisioning_session->certificate_map, ogs_strdup(cert), OGS_HASH_KEY_STRING, ogs_strdup(cert)); + + location = ogs_msprintf("%s/%s", request->h.uri, cert); + response = nf_server_new_response(location, NULL, 0, NULL, 0, NULL, m1_servercertificatesprovisioning_api, app_meta); + nf_server_populate_response(response, 0, NULL, 200); + ogs_assert(response); + ogs_assert(true == ogs_sbi_server_send_response(stream, response)); + ogs_free(location); + } else { + msaf_certificate_t *new_cert; + int m1_server_certificates_response_max_age; + ogs_sbi_response_t *response; + char *location; + new_cert = server_cert_new("newcert", canonical_domain_name); + ogs_hash_set(msaf_provisioning_session->certificate_map, ogs_strdup(new_cert->id), OGS_HASH_KEY_STRING, ogs_strdup(new_cert->id)); + + location = ogs_msprintf("%s/%s", request->h.uri, new_cert->id); + if(new_cert->cache_control_max_age){ + m1_server_certificates_response_max_age = new_cert->cache_control_max_age; + } else { + m1_server_certificates_response_max_age = msaf_self()->config.server_response_cache_control->m1_server_certificates_response_max_age; + } + response = nf_server_new_response(location, NULL, new_cert->last_modified, new_cert->server_certificate_hash, m1_server_certificates_response_max_age, NULL, m1_servercertificatesprovisioning_api, app_meta); + nf_server_populate_response(response, 0, NULL, 200); + ogs_assert(response); + ogs_assert(true == ogs_sbi_server_send_response(stream, response)); + ogs_free(location); + msaf_certificate_free(new_cert); + } + } else { + char *err = NULL; + asprintf(&err,"Provisioning session [%s] does not exists.", message.h.resource.component[1]); + ogs_error("%s", err); + ogs_assert(true == nf_server_send_error(stream, 404, 2, &message, "Provisioning session does not exists.", err, NULL, m1_servercertificatesprovisioning_api, app_meta)); + } + } + + } else if (message.h.resource.component[1] && !message.h.resource.component[2]){ + msaf_provisioning_session_t *msaf_provisioning_session; + msaf_provisioning_session = msaf_provisioning_session_find_by_provisioningSessionId(message.h.resource.component[1]); + if(msaf_provisioning_session) { + char *err = NULL; + asprintf(&err,"Method [%s] not allowed for [%s].", message.h.method, message.h.resource.component[1]); + ogs_error("%s", err); + ogs_assert(true == nf_server_send_error(stream, 405, 1, &message, "Method not allowed.", err, NULL, m1_provisioningsession_api, app_meta)); + + } else { + char *err = NULL; + asprintf(&err,"Provisioning session [%s] does not exist.", message.h.resource.component[1]); + ogs_error("%s", err); + ogs_assert(true == nf_server_send_error(stream, 404, 1, &message, "Provisioning session does not exist.", err, NULL, m1_provisioningsession_api, app_meta)); + } + + } else { + cJSON *entry; + cJSON *prov_sess = cJSON_Parse(request->http.content); + cJSON *provisioning_session; + char *provisioning_session_type, *external_app_id, *asp_id = NULL; + msaf_provisioning_session_t *msaf_provisioning_session; + + ogs_debug("createProvisioningSession: received=\"%s\"", request->http.content); + + entry = cJSON_GetObjectItemCaseSensitive(prov_sess, "provisioningSessionType"); + if (!entry) { + const char *err = "createProvisioningSession: \"provisioningSessionType\" is not present"; + ogs_error(err); + ogs_assert(true == nf_server_send_error(stream, 400, 1, &message, "Creation of the Provisioning session failed.", ogs_strdup(err), NULL, m1_provisioningsession_api, app_meta)); + break; + } + if (!cJSON_IsString(entry)) { + const char *err = "createProvisioningSession: \"provisioningSessionType\" is not a string"; + ogs_error(err); + ogs_assert(true == nf_server_send_error(stream, 400, 1, &message, "Creation of the Provisioning session failed.", ogs_strdup(err), NULL, m1_provisioningsession_api, app_meta)); + break; + } + provisioning_session_type = entry->valuestring; + + entry = cJSON_GetObjectItemCaseSensitive(prov_sess, "externalApplicationId"); + if (!entry) { + const char *err = "createProvisioningSession: \"externalApplicationId\" is not present"; + ogs_error(err); + ogs_assert(true == nf_server_send_error(stream, 400, 1, &message, "Creation of the Provisioning session failed.", ogs_strdup(err), NULL, m1_provisioningsession_api, app_meta)); + break; + } + if (!cJSON_IsString(entry)) { + const char *err = "createProvisioningSession: \"externalApplicationId\" is not a string"; + ogs_error(err); + ogs_assert(true == nf_server_send_error(stream, 400, 1, &message, "Creation of the Provisioning session failed.", ogs_strdup(err), NULL, m1_provisioningsession_api, app_meta)); + break; + } + external_app_id = entry->valuestring; + + entry = cJSON_GetObjectItemCaseSensitive(prov_sess, "aspId"); + if (entry) { + if (!cJSON_IsString(entry)) { + const char *err = "createProvisioningSession: \"aspId\" is not a string"; + ogs_error(err); + ogs_assert(true == nf_server_send_error(stream, 400, 1, &message, "Creation of the Provisioning session failed.", ogs_strdup(err), NULL, m1_provisioningsession_api, app_meta)); + break; + } + asp_id = entry->valuestring; + } + + msaf_provisioning_session = msaf_provisioning_session_create(provisioning_session_type, asp_id, external_app_id); + provisioning_session = msaf_provisioning_session_get_json(msaf_provisioning_session->provisioningSessionId); + if (provisioning_session != NULL) { + ogs_sbi_response_t *response; + char *text; + char *location; + text = cJSON_Print(provisioning_session); + if (request->h.uri[strlen(request->h.uri)-1] != '/') { + location = ogs_msprintf("%s/%s", request->h.uri,msaf_provisioning_session->provisioningSessionId); + } else { + location = ogs_msprintf("%s%s", request->h.uri,msaf_provisioning_session->provisioningSessionId); + } + response = nf_server_new_response(location, "application/json", msaf_provisioning_session->provisioningSessionReceived, msaf_provisioning_session->provisioningSessionHash, msaf_self()->config.server_response_cache_control->m1_provisioning_session_response_max_age, NULL, m1_provisioningsession_api, app_meta); + + nf_server_populate_response(response, strlen(text), text, 201); + ogs_assert(response); + ogs_assert(true == ogs_sbi_server_send_response(stream, response)); + ogs_free(location); + cJSON_Delete(provisioning_session); + cJSON_Delete(prov_sess); + } else { + const char *err = "Creation of the Provisioning session failed."; + ogs_error(err); + ogs_assert(true == nf_server_send_error(stream, 404, 1, &message, "Creation of the Provisioning session failed.", ogs_strdup(err), NULL, m1_provisioningsession_api, app_meta)); + } + } + + break; + + CASE(OGS_SBI_HTTP_METHOD_GET) + if (message.h.resource.component[1] && message.h.resource.component[2] && message.h.resource.component[3] && !message.h.resource.component[4]) { + if (!strcmp(message.h.resource.component[2],"certificates") ) { + msaf_provisioning_session_t *msaf_provisioning_session; + msaf_provisioning_session = msaf_provisioning_session_find_by_provisioningSessionId(message.h.resource.component[1]); + if (msaf_provisioning_session) { + msaf_certificate_t *cert; + ogs_sbi_response_t *response; + const char *provisioning_session_cert; + provisioning_session_cert = ogs_hash_get(msaf_provisioning_session->certificate_map, message.h.resource.component[3], OGS_HASH_KEY_STRING); + if(!provisioning_session_cert) { + char *err = NULL; + asprintf(&err,"Certificate [%s] not found in provisioning session [%s]", message.h.resource.component[3], message.h.resource.component[1]); + ogs_error("%s", err); + ogs_assert(true == nf_server_send_error(stream, 404, 3, &message, "Certificate not found.", err, NULL, m1_servercertificatesprovisioning_api, app_meta)); + break; + } + cert = server_cert_retrieve(message.h.resource.component[3]); + if (!cert) { + char *err = NULL; + asprintf(&err,"Certificate [%s] management problem", message.h.resource.component[3]); + ogs_error("%s", err); + ogs_assert(true == nf_server_send_error(stream, 500, 3, &message, "Certificate management problem.", err, NULL, m1_servercertificatesprovisioning_api, app_meta)); + break; + } + + if(!cert->return_code) { + int m1_server_certificates_response_max_age; + if(cert->cache_control_max_age){ + m1_server_certificates_response_max_age = cert->cache_control_max_age; + } else { + m1_server_certificates_response_max_age = msaf_self()->config.server_response_cache_control->m1_server_certificates_response_max_age; + } + response = nf_server_new_response(NULL, "application/x-pem-file", cert->last_modified, cert->server_certificate_hash, m1_server_certificates_response_max_age, NULL, m1_servercertificatesprovisioning_api, app_meta); + nf_server_populate_response(response, strlen(cert->certificate), ogs_strdup(cert->certificate), 200); + ogs_assert(response); + ogs_assert(true == ogs_sbi_server_send_response(stream, response)); + } else if(cert->return_code == 4){ + char *err = NULL; + asprintf(&err,"Certificate [%s] does not exists.", cert->id); + ogs_error("%s", err); + ogs_assert(true == nf_server_send_error(stream, 404, 3, &message, "Certificate does not exists.", err, NULL, m1_servercertificatesprovisioning_api, app_meta)); + + } else if(cert->return_code == 8){ + ogs_sbi_response_t *response; + response = nf_server_new_response(NULL, NULL, 0, NULL, 0, NULL, m1_servercertificatesprovisioning_api, app_meta); + nf_server_populate_response(response, 0, NULL, 204); + ogs_assert(response); + ogs_assert(true == ogs_sbi_server_send_response(stream, response)); + } else { + char *err = NULL; + asprintf(&err,"Certificate [%s] management problem.", cert->id); + ogs_error("%s", err); + ogs_assert(true == nf_server_send_error(stream, 500, 3, &message, "Certificate management problem.", err, NULL, m1_servercertificatesprovisioning_api, app_meta)); + } + msaf_certificate_free(cert); + + } else { + char *err = NULL; + asprintf(&err,"Provisioning session [%s] is not available.", message.h.resource.component[1]); + ogs_error("%s", err); + ogs_assert(true == nf_server_send_error(stream, 404, 3, &message, "Provisioning session does not exists.", err, NULL, m1_servercertificatesprovisioning_api, app_meta)); + } + } + } else if (message.h.resource.component[1] && message.h.resource.component[2] && !message.h.resource.component[3]) { + msaf_provisioning_session_t *msaf_provisioning_session; + msaf_provisioning_session = msaf_provisioning_session_find_by_provisioningSessionId(message.h.resource.component[1]); + if (!strcmp(message.h.resource.component[2],"content-hosting-configuration")) { + if(msaf_provisioning_session) { + cJSON *chc; + chc = msaf_get_content_hosting_configuration_by_provisioning_session_id(message.h.resource.component[1]); + if (chc != NULL) { + ogs_sbi_response_t *response; + char *text; + text = cJSON_Print(chc); + + response = nf_server_new_response(request->h.uri, "application/json", msaf_provisioning_session->contentHostingConfigurationReceived, msaf_provisioning_session->contentHostingConfigurationHash, msaf_self()->config.server_response_cache_control->m1_content_hosting_configurations_response_max_age, NULL, m1_contenthostingprovisioning_api, app_meta); + ogs_assert(response); + nf_server_populate_response(response, strlen(text), text, 200); + ogs_assert(true == ogs_sbi_server_send_response(stream, response)); + + cJSON_Delete(chc); + } else { + char *err = NULL; + asprintf(&err,"Provisioning Session [%s]: Unable to retrieve the Content Hosting Configuration", message.h.resource.component[1]); + ogs_error("%s", err); + ogs_assert(true == nf_server_send_error(stream, 404, 2, &message, "Unable to retrieve the Content Hosting Configuration.", err, NULL, m1_contenthostingprovisioning_api, app_meta)); + } + + } else { + char *err = NULL; + asprintf(&err,"Provisioning Session [%s] does not exist.", message.h.resource.component[1]); + ogs_error("%s", err); + + ogs_assert(true == nf_server_send_error(stream, 404, 2, &message, "Provisioning session does not exist.", err, NULL, m1_contenthostingprovisioning_api, app_meta)); + } + + } else if (!strcmp(message.h.resource.component[2],"protocols")) { + if(msaf_provisioning_session) { + ogs_sbi_response_t *response; + ogs_info("CONTENT_PROTOCOLS_DISCOVERY_JSON: %s", CONTENT_PROTOCOLS_DISCOVERY_JSON); + response = nf_server_new_response(NULL, "application/json", CONTENT_PROTOCOLS_DISCOVERY_JSON_TIME, CONTENT_PROTOCOLS_DISCOVERY_JSON_HASH, msaf_self()->config.server_response_cache_control->m1_content_protocols_response_max_age, NULL, m1_contentprotocolsdiscovery_api, app_meta); + ogs_assert(response); + nf_server_populate_response(response, strlen(CONTENT_PROTOCOLS_DISCOVERY_JSON), ogs_strdup(CONTENT_PROTOCOLS_DISCOVERY_JSON), 200); + ogs_assert(true == ogs_sbi_server_send_response(stream, response)); + } else { + char *err = NULL; + asprintf(&err,"Provisioning Session [%s] does not exist.", message.h.resource.component[1]); + ogs_error("%s", err); + ogs_assert(true == nf_server_send_error(stream, 404, 2, &message, "Provisioning session does not exist.", err, NULL, m1_contentprotocolsdiscovery_api, app_meta)); + } + } + } else if (message.h.resource.component[1] && !message.h.resource.component[2]) { + msaf_provisioning_session_t *msaf_provisioning_session = NULL; + cJSON *provisioning_session = NULL; + + msaf_provisioning_session = msaf_provisioning_session_find_by_provisioningSessionId(message.h.resource.component[1]); + + provisioning_session = msaf_provisioning_session_get_json(message.h.resource.component[1]); + + if (provisioning_session && msaf_provisioning_session && !msaf_provisioning_session->marked_for_deletion) { + ogs_sbi_response_t *response; + char *text; + text = cJSON_Print(provisioning_session); + + response = nf_server_new_response(NULL, "application/json", msaf_provisioning_session->provisioningSessionReceived, msaf_provisioning_session->provisioningSessionHash, msaf_self()->config.server_response_cache_control->m1_provisioning_session_response_max_age, NULL, m1_provisioningsession_api, app_meta); + + nf_server_populate_response(response, strlen(text), text, 200); + ogs_assert(response); + ogs_assert(true == ogs_sbi_server_send_response(stream, response)); + cJSON_Delete(provisioning_session); + + } else { + char *err = NULL; + asprintf(&err,"Provisioning Session [%s] is not available.", message.h.resource.component[1]); + ogs_error("%s", err); + + ogs_assert(true == nf_server_send_error(stream, 404, 2, &message, "Provisioning session does not exists.", err, NULL, m1_provisioningsession_api, app_meta)); + } + } + break; + + CASE(OGS_SBI_HTTP_METHOD_PUT) + if (message.h.resource.component[1] && message.h.resource.component[2]) { + + ogs_info("PUT: %s", message.h.resource.component[1]); + msaf_provisioning_session_t *msaf_provisioning_session; + msaf_provisioning_session = msaf_provisioning_session_find_by_provisioningSessionId(message.h.resource.component[1]); + if(msaf_provisioning_session) { + ogs_info("PUT: with msaf_provisioning_session: %s", message.h.resource.component[1]); + if (!strcmp(message.h.resource.component[2],"content-hosting-configuration") && !message.h.resource.component[3]) { + + // process the PUT body + cJSON *entry; + int rv; + cJSON *content_hosting_config = cJSON_Parse(request->http.content); + + if (!content_hosting_config) { + char *err = NULL; + asprintf(&err,"While updating the Content Hosting Configuration for the Provisioning Session [%s], Failure parsing ContentHostingConfiguration JSON.",message.h.resource.component[1]); + ogs_error("%s", err); + ogs_assert(true == nf_server_send_error(stream, 422, 2, &message, "Bad ContentHosting Configuration JSON.", err, NULL, m1_contenthostingprovisioning_api, app_meta)); + break; + } + + { + char *txt = cJSON_Print(content_hosting_config); + ogs_debug("txt:%s", txt); + ogs_free(txt); + } + + cJSON_ArrayForEach(entry, content_hosting_config) { + if(!strcmp(entry->string, "entryPointPath")){ + if(!uri_relative_check(entry->valuestring)) { + char *err = NULL; + asprintf(&err,"While updating the Content Hosting Configuration for the Provisioning Session [%s], Entry Point Path does not match the regular expression [%s].",message.h.resource.component[1], entry->valuestring ); + ogs_error("%s", err); + + ogs_assert(true == nf_server_send_error(stream, 422, 2, &message, "Entry Point Path does not match the regular expression.", err, NULL, m1_contenthostingprovisioning_api, app_meta)); + cJSON_Delete(content_hosting_config); + break; + } + } + } + if(msaf_provisioning_session->contentHostingConfiguration) { + OpenAPI_content_hosting_configuration_free(msaf_provisioning_session->contentHostingConfiguration); + msaf_provisioning_session->contentHostingConfiguration = NULL; + } + + if (msaf_provisioning_session->serviceAccessInformation) { + OpenAPI_service_access_information_resource_free(msaf_provisioning_session->serviceAccessInformation); + msaf_provisioning_session->serviceAccessInformation = NULL; + } + + rv = msaf_distribution_create(content_hosting_config, msaf_provisioning_session); + if(rv){ + + msaf_application_server_state_update(msaf_provisioning_session); + + ogs_debug("Content Hosting Configuration updated successfully"); + + ogs_sbi_response_t *response; + response = ogs_sbi_response_new(); + response->status = 204; + ogs_sbi_header_set(response->http.headers, "Content-Type", "application/json"); + ogs_sbi_header_set(response->http.headers, "Location", request->h.uri); + ogs_assert(response); + ogs_assert(true == ogs_sbi_server_send_response(stream, response)); + cJSON_Delete(content_hosting_config); + + } else { + char *err = NULL; + asprintf(&err,"Provisioning Session [%s]: Update to Content Hosting Configuration failed.", message.h.resource.component[1]); + ogs_error("%s", err); + ogs_assert(true == nf_server_send_error(stream, 404, 2, &message, "Failed to update the contentHostingConfiguration.", err, NULL, m1_contenthostingprovisioning_api, app_meta)); + } + } + if (!strcmp(message.h.resource.component[2],"certificates") && message.h.resource.component[3] && !message.h.resource.component[4]) { + char *cert_id; + char *cert; + int rv; + ogs_sbi_response_t *response; + msaf_provisioning_session_t *msaf_provisioning_session; + + { + ogs_hash_index_t *hi; + for (hi = ogs_hash_first(request->http.headers); + hi; hi = ogs_hash_next(hi)) { + if (!ogs_strcasecmp(ogs_hash_this_key(hi), OGS_SBI_CONTENT_TYPE)) { + if (ogs_strcasecmp(ogs_hash_this_val(hi), "application/x-pem-file")) { + char *err = NULL; + const char *type; + type = ogs_hash_this_val(hi); + asprintf(&err, "Unsupported Media Type: received type: %s, should have been application/x-pem-file", type); + ogs_error("%s", err); + + ogs_assert(true == nf_server_send_error(stream, 415, 3, &message, "Unsupported Media Type.", err, NULL, m1_servercertificatesprovisioning_api, app_meta)); + ogs_sbi_message_free(&message); + return; + + } + } + } + } + + msaf_provisioning_session = msaf_provisioning_session_find_by_provisioningSessionId(message.h.resource.component[1]); + + if(msaf_provisioning_session) { + const char *provisioning_session_cert; + provisioning_session_cert = ogs_hash_get(msaf_provisioning_session->certificate_map, message.h.resource.component[3], OGS_HASH_KEY_STRING); + cert_id = message.h.resource.component[3]; + cert = ogs_strdup(request->http.content); + rv = server_cert_set(cert_id, cert); + // response = ogs_sbi_response_new(); + + if (rv == 0 && provisioning_session_cert){ + response = nf_server_new_response(NULL, NULL, 0, NULL, 0, NULL, m1_servercertificatesprovisioning_api, app_meta); + nf_server_populate_response(response, 0, NULL, 204); + ogs_assert(response); + ogs_assert(true == ogs_sbi_server_send_response(stream, response)); + } else if (rv == 3 && provisioning_session_cert ) { + + char *err = NULL; + asprintf(&err,"A server certificate with id [%s] already exist", cert_id); + ogs_error("%s", err); + ogs_assert(true == nf_server_send_error(stream, 403, 3, &message, "A server certificate already exist.", err, NULL, m1_servercertificatesprovisioning_api, app_meta)); + + } else if(rv == 4 || ! provisioning_session_cert) { + char *err = NULL; + asprintf(&err,"Server certificate with id [%s] does not exist", cert_id); + ogs_error("%s", err); + ogs_assert(true == nf_server_send_error(stream, 404, 3, &message, "Server certificate does not exist.", err, NULL, m1_servercertificatesprovisioning_api, app_meta)); + + } else if(rv == 5) { + char *err = NULL; + asprintf(&err,"CSR was never generated for this certificate Id [%s]", cert_id); + ogs_error("%s", err); + ogs_assert(true == nf_server_send_error(stream, 400, 3, &message, "CSR was never generated for the certificate.", err, NULL, m1_servercertificatesprovisioning_api, app_meta)); + } else if(rv == 6) { + char *err = NULL; + asprintf(&err,"The public certificate [%s] provided does not match the key", cert_id); + ogs_error("%s", err); + ogs_assert(true == nf_server_send_error(stream, 400, 3, &message, "The public certificate provided does not match the key.", err, NULL, m1_servercertificatesprovisioning_api, app_meta)); + } else { + char *err = NULL; + asprintf(&err,"There was a certificate management problem for the certificate id [%s].", cert_id); + ogs_error("%s", err); + + ogs_assert(true == nf_server_send_error(stream, 500, 3, &message, "There was a certificate management problem.", err, NULL, m1_servercertificatesprovisioning_api, app_meta)); + } + ogs_free(cert); + } + + } else { + char *err = NULL; + asprintf(&err,"[%s]: Resource not found.", message.h.method); + ogs_error("%s", err); + ogs_assert(true == nf_server_send_error(stream, 404, 1, &message, "Resource not found.", err, NULL, m1_provisioningsession_api, app_meta)); + } + } else { + char *err = NULL; + asprintf(&err,"Provisioning Session [%s] does not exist.", message.h.resource.component[1]); + ogs_error("%s", err); + ogs_assert(true == nf_server_send_error(stream, 404, 3, &message, "Provisioning session does not exist.", err, NULL, m1_servercertificatesprovisioning_api, app_meta)); + } + + + } else { + char *err = NULL; + asprintf(&err,"[%s]: Resource not found.", message.h.method); + ogs_error("%s", err); + ogs_assert(true == nf_server_send_error(stream, 404, 1, &message, "Resource not found.", err, NULL, m1_provisioningsession_api, app_meta)); + } + break; + + CASE(OGS_SBI_HTTP_METHOD_DELETE) + + if (message.h.resource.component[1] && message.h.resource.component[2] && !strcmp(message.h.resource.component[2],"certificates") && message.h.resource.component[3] && !message.h.resource.component[4]) { + ogs_sbi_response_t *response; + msaf_provisioning_session_t *provisioning_session = NULL; + provisioning_session = msaf_provisioning_session_find_by_provisioningSessionId(message.h.resource.component[1]); + if (provisioning_session) { + int rv; + rv = server_cert_delete(message.h.resource.component[3]); + if ((rv == 0) || (rv == 8)){ + response = nf_server_new_response(NULL, NULL, 0, NULL, 0, NULL, m1_servercertificatesprovisioning_api, app_meta); + nf_server_populate_response(response, 0, NULL, 204); + ogs_assert(response); + ogs_assert(true == ogs_sbi_server_send_response(stream, response)); + msaf_provisioning_session_certificate_hash_remove(message.h.resource.component[1], message.h.resource.component[3]); + + } else if (rv == 4 ) { + char *err = NULL; + asprintf(&err,"Certificate [%s] does not exist.", message.h.resource.component[3]); + ogs_error("%s", err); + + ogs_assert(true == nf_server_send_error(stream, 404, 3, &message, "Certificate does not exist.", err, NULL, m1_servercertificatesprovisioning_api, app_meta)); + + } else { + char *err = NULL; + asprintf(&err,"Certificate management problem for certificate [%s].", message.h.resource.component[3]); + ogs_error("%s", err); + + ogs_assert(true == nf_server_send_error(stream, 500, 3, &message, "Certificate management problem.", err, NULL, m1_servercertificatesprovisioning_api, app_meta)); + } + + } else { + char *err = NULL; + asprintf(&err,"Provisioning Session [%s] does not exist.", message.h.resource.component[1]); + ogs_error("%s", err); + + ogs_assert(true == nf_server_send_error(stream, 404, 3, &message, "Provisioning session does not exist.", err, NULL, m1_servercertificatesprovisioning_api, app_meta)); + } + } else if (message.h.resource.component[1] && message.h.resource.component[2] && !message.h.resource.component[3]) { + msaf_provisioning_session_t *msaf_provisioning_session; + ogs_sbi_response_t *response; + if (!strcmp(message.h.resource.component[2],"content-hosting-configuration")) { + msaf_provisioning_session = msaf_provisioning_session_find_by_provisioningSessionId(message.h.resource.component[1]); + if(msaf_provisioning_session){ + if(msaf_provisioning_session && msaf_provisioning_session->contentHostingConfiguration) { + msaf_delete_content_hosting_configuration(message.h.resource.component[1]); + OpenAPI_content_hosting_configuration_free(msaf_provisioning_session->contentHostingConfiguration); + msaf_provisioning_session->contentHostingConfiguration = NULL; + response = nf_server_new_response(NULL, NULL, 0, NULL, 0, NULL, m1_contenthostingprovisioning_api, app_meta); + ogs_assert(response); + nf_server_populate_response(response, 0, NULL, 204); + ogs_assert(true == ogs_sbi_server_send_response(stream, response)); + break; + } else { + char *err = NULL; + asprintf(&err,"Provisioning Session [%s] has no Content Hosting Configuration.", message.h.resource.component[1]); + ogs_error("%s", err); + ogs_assert(true == nf_server_send_error(stream, 404, 2, &message, "Content Hosting Configuration does not exist.", err, NULL, m1_contenthostingprovisioning_api, app_meta)); + } + } else { + char *err = NULL; + asprintf(&err,"Provisioning Session [%s] does not exists.", message.h.resource.component[1]); + ogs_error("%s", err); + ogs_assert(true == nf_server_send_error(stream, 404, 2, &message, "Provisioning session does not exist.", err, NULL, m1_contenthostingprovisioning_api, app_meta)); + } + + } + + } else if (message.h.resource.component[1] && !message.h.resource.component[2]) { + ogs_sbi_response_t *response; + msaf_provisioning_session_t *provisioning_session = NULL; + provisioning_session = msaf_provisioning_session_find_by_provisioningSessionId(message.h.resource.component[1]); + if(!provisioning_session || provisioning_session->marked_for_deletion){ + char *err = NULL; + asprintf(&err,"Provisioning session [%s] either not found or already marked for deletion.", message.h.resource.component[1]); + + ogs_error("%s", err); + + ogs_assert(true == nf_server_send_error(stream, 500, 3, &message, "Provisioning session either not found or already marked for deletion.", err, NULL, m1_provisioningsession_api, app_meta)); + } else { + provisioning_session->marked_for_deletion = 1; + response = nf_server_new_response(NULL, NULL, 0, NULL, 0, NULL, m1_provisioningsession_api, app_meta); + ogs_assert(response); + nf_server_populate_response(response, 0, NULL, 202); + ogs_assert(true == ogs_sbi_server_send_response(stream, response)); + msaf_delete_content_hosting_configuration(message.h.resource.component[1]); + msaf_delete_certificates(message.h.resource.component[1]); + msaf_context_provisioning_session_free(provisioning_session); + msaf_provisioning_session_hash_remove(message.h.resource.component[1]); + } + } + + break; + CASE(OGS_SBI_HTTP_METHOD_OPTIONS) + + if (!strcmp(message.h.resource.component[0],"provisioning-sessions")){ + ogs_sbi_response_t *response; + char *methods = NULL; + + if (message.h.resource.component[1]) { + msaf_provisioning_session_t *provisioning_session = NULL; + provisioning_session = msaf_provisioning_session_find_by_provisioningSessionId(message.h.resource.component[1]); + if (provisioning_session) { + if (message.h.resource.component[2]) { + + + if (!strcmp(message.h.resource.component[2],"certificates")) { + if (message.h.resource.component[3]) { + msaf_certificate_t *cert; + cert = server_cert_retrieve(message.h.resource.component[3]); + if(cert){ + methods = ogs_msprintf("%s, %s, %s, %s",OGS_SBI_HTTP_METHOD_GET, OGS_SBI_HTTP_METHOD_PUT, OGS_SBI_HTTP_METHOD_DELETE, OGS_SBI_HTTP_METHOD_OPTIONS); + response = nf_server_new_response(request->h.uri, NULL, 0, NULL, 0, methods, m1_servercertificatesprovisioning_api, app_meta); + nf_server_populate_response(response, 0, NULL, 204); + ogs_assert(response); + ogs_assert(true == ogs_sbi_server_send_response(stream, response)); + msaf_certificate_free(cert); + } else { + char *err = NULL; + asprintf(&err,"Certificate [%s] management problem", message.h.resource.component[3]); + ogs_error("%s", err); + ogs_assert(true == nf_server_send_error(stream, 500, 3, &message, "Certificate management problem.", err, NULL, m1_servercertificatesprovisioning_api, app_meta)); + break; + } + } else { + methods = ogs_msprintf("%s",OGS_SBI_HTTP_METHOD_POST); + response = nf_server_new_response(request->h.uri, NULL, 0, NULL, 0, methods, m1_servercertificatesprovisioning_api, app_meta); + nf_server_populate_response(response, 0, NULL, 204); + ogs_assert(response); + ogs_assert(true == ogs_sbi_server_send_response(stream, response)); + } + + } else if (!strcmp(message.h.resource.component[2],"content-hosting-configuration")) { + methods = ogs_msprintf("%s, %s, %s, %s, %s",OGS_SBI_HTTP_METHOD_POST, OGS_SBI_HTTP_METHOD_GET, OGS_SBI_HTTP_METHOD_PUT, OGS_SBI_HTTP_METHOD_DELETE, OGS_SBI_HTTP_METHOD_OPTIONS); + response = nf_server_new_response(request->h.uri, NULL, 0, NULL, 0, methods, m1_contenthostingprovisioning_api, app_meta); + nf_server_populate_response(response, 0, NULL, 204); + ogs_assert(response); + ogs_assert(true == ogs_sbi_server_send_response(stream, response)); + + } else { + char *err = NULL; + asprintf(&err,"Method [%s]: Target [%s] not yet supported.", message.h.method, message.h.resource.component[2]); + ogs_error("%s", err); + ogs_assert(true == nf_server_send_error(stream, 404, 2, &message, "Target not yet supported.", err, NULL, NULL, app_meta)); + } + } else { + methods = ogs_msprintf("%s, %s, %s", OGS_SBI_HTTP_METHOD_GET, OGS_SBI_HTTP_METHOD_DELETE, OGS_SBI_HTTP_METHOD_OPTIONS); + response = nf_server_new_response(request->h.uri, NULL, 0, NULL, 0, methods, m1_provisioningsession_api, app_meta); + nf_server_populate_response(response, 0, NULL, 204); + ogs_assert(response); + ogs_assert(true == ogs_sbi_server_send_response(stream, response)); + + } + /* + nf_server_populate_response(response, 0, NULL, 204); + ogs_assert(response); + ogs_assert(true == ogs_sbi_server_send_response(stream, response)); + + if(methods) ogs_free(methods); + */ + } else { + char *err = NULL; + int number_of_components; + const nf_server_interface_metadata_t *interface; + if (message.h.resource.component[2]){ + if (!strcmp(message.h.resource.component[2],"certificates")) { + number_of_components = 2; + if (message.h.resource.component[3]) { + number_of_components = 3; + } + interface = m1_servercertificatesprovisioning_api; + } else if (!strcmp(message.h.resource.component[2],"content-hosting-configuration")) { + number_of_components = 2; + interface = m1_contenthostingprovisioning_api; + + } + } else if (message.h.resource.component[0]){ + if (!strcmp(message.h.resource.component[0],"provisioning-sessions")){ + number_of_components = 0; + if (message.h.resource.component[1]) { + number_of_components = 1; + } + interface = m1_provisioningsession_api; + + } + } + asprintf(&err,"Method [%s]: [%s] - Provisioning Session [%s] does not exist.", message.h.method, message.h.resource.component[2], message.h.resource.component[1]); + ogs_error("%s", err); + ogs_assert(true == nf_server_send_error(stream, 404, number_of_components, &message, "Provisioning Session does not exists.", err, NULL, interface, app_meta)); + } + + } else { + methods = ogs_msprintf("%s, %s",OGS_SBI_HTTP_METHOD_POST, OGS_SBI_HTTP_METHOD_OPTIONS); + response = nf_server_new_response(request->h.uri, NULL, 0, NULL, 0, methods, m1_provisioningsession_api, app_meta); + nf_server_populate_response(response, 0, NULL, 204); + ogs_assert(response); + ogs_assert(true == ogs_sbi_server_send_response(stream, response)); + + } + if(methods) ogs_free(methods); + } else { + char *err = NULL; + asprintf(&err,"Method [%s]: Target [%s] not yet supported.", message.h.method, message.h.resource.component[0]); + ogs_error("%s", err); + ogs_assert(true == nf_server_send_error(stream, 404, 0, &message, "Target not yet supported.", err, NULL, m1_provisioningsession_api, app_meta)); + } + break; + + DEFAULT + ogs_error("Invalid HTTP method [%s]", message.h.method); + ogs_assert(true == nf_server_send_error(stream, OGS_SBI_HTTP_STATUS_FORBIDDEN, 0, &message, "Invalid HTTP method.", ogs_strdup(message.h.method), NULL, NULL, app_meta)); + END + break; + + DEFAULT + char *err = NULL; + asprintf(&err,"Invalid resource name [%s]", message.h.resource.component[0]); + ogs_error("%s", err); + ogs_assert(true == nf_server_send_error(stream, OGS_SBI_HTTP_STATUS_BAD_REQUEST, 0, &message, "Invalid resource name", err, NULL, NULL, app_meta)); + END + ogs_sbi_message_free(&message); + break; + + CASE("5gmag-rt-management") + if (strcmp(message.h.api.version, "v1") != 0) { + char *error; + error = ogs_msprintf("Version [%s] not supported", message.h.api.version); + ogs_error("%s", error); + ogs_assert(true == nf_server_send_error(stream, OGS_SBI_HTTP_STATUS_BAD_REQUEST, 1, NULL, "Not supported version", error, NULL, maf_management_api, app_meta)); + + ogs_sbi_message_free(&message); + break; + } + if (!message.h.resource.component[0]) { + char *error; + error = ogs_strdup("Resource required for Management interface"); + ogs_error("%s", error); + ogs_assert(true == nf_server_send_error(stream, 404, 1, NULL, "Resource name required", error, NULL, maf_management_api, app_meta)); + ogs_sbi_message_free(&message); + break; + } + + SWITCH(message.h.resource.component[0]) + + CASE("provisioning-sessions") + SWITCH(message.h.method) + CASE(OGS_SBI_HTTP_METHOD_GET) + char *provisioning_sessions = NULL; + ogs_sbi_response_t *response; + provisioning_sessions = enumerate_provisioning_sessions(); + if(provisioning_sessions) { + response = nf_server_new_response(NULL, "application/json", 0, NULL, msaf_self()->config.server_response_cache_control->m1_provisioning_session_response_max_age, NULL, maf_management_api, app_meta); + + nf_server_populate_response(response, strlen(provisioning_sessions), ogs_strdup(provisioning_sessions), 200); + ogs_assert(response); + ogs_assert(true == ogs_sbi_server_send_response(stream, response)); + if (strcmp(provisioning_sessions,"[]")) ogs_free(provisioning_sessions); + break; + } else { + ogs_error("Internal Server Error."); + ogs_assert(true == nf_server_send_error(stream, OGS_SBI_HTTP_STATUS_INTERNAL_SERVER_ERROR, 0, &message, "Internal Server Error.", ogs_strdup(message.h.method), NULL, maf_management_api, app_meta)); + } + DEFAULT + ogs_error("Invalid HTTP method [%s]", message.h.method); + ogs_assert(true == nf_server_send_error(stream, OGS_SBI_HTTP_STATUS_FORBIDDEN, 0, &message, "Invalid HTTP method.", ogs_strdup(message.h.method), NULL, maf_management_api, app_meta)); + END + break; + + DEFAULT + char *err = NULL; + asprintf(&err,"Invalid resource name [%s]", message.h.resource.component[0]); + ogs_error("%s", err); + ogs_assert(true == nf_server_send_error(stream, OGS_SBI_HTTP_STATUS_BAD_REQUEST, 0, &message, "Invalid resource name", err, NULL, NULL, app_meta)); + END + ogs_sbi_message_free(&message); + break; + DEFAULT + ogs_error("Invalid API name [%s]", message.h.service.name); + ogs_assert(true == nf_server_send_error(stream, OGS_SBI_HTTP_STATUS_BAD_REQUEST, 1, &message, "Invalid API name.", ogs_strdup(message.h.service.name), NULL, NULL, app_meta)); + + END + break; + + case OGS_EVENT_SBI_CLIENT: + ogs_assert(e); + + response = e->h.sbi.response; + ogs_assert(response); + rv = ogs_sbi_parse_header(&message, &response->h); + if (rv != OGS_OK) { + ogs_error("ogs_sbi_parse_header() failed"); + ogs_sbi_message_free(&message); + ogs_sbi_response_free(response); + break; + } + { + ogs_hash_index_t *hi; + for (hi = ogs_hash_first(response->http.headers); + hi; hi = ogs_hash_next(hi)) { + if (!ogs_strcasecmp(ogs_hash_this_key(hi), OGS_SBI_CONTENT_TYPE)) { + message.http.content_type = ogs_hash_this_val(hi); + } else if (!ogs_strcasecmp(ogs_hash_this_key(hi), OGS_SBI_LOCATION)) { + message.http.location = ogs_hash_this_val(hi); + } + } + } + + message.res_status = response->status; + + SWITCH(message.h.service.name) + CASE("3gpp-m3") + SWITCH(message.h.resource.component[0]) + CASE("content-hosting-configurations") + + msaf_application_server_state_node_t *as_state; + as_state = e->application_server_state; + ogs_assert(as_state); + + if (message.h.resource.component[1] && message.h.resource.component[2]) { + + if (!strcmp(message.h.resource.component[2],"purge")) { + + SWITCH(message.h.method) + CASE(OGS_SBI_HTTP_METHOD_POST) + purge_resource_id_node_t *purge_node = e->purge_node; + + if (response->status == 204 || response->status == 200) { + + purge_resource_id_node_t *content_hosting_cache, *next = NULL; + + if (response->status == 200) { + //parse the int in response body + //Add the integer to purge_node->m1_purge_info->purged_entries_total; + { + ogs_hash_index_t *hi; + for (hi = ogs_hash_first(request->http.headers); + hi; hi = ogs_hash_next(hi)) { + if (!ogs_strcasecmp(ogs_hash_this_key(hi), OGS_SBI_CONTENT_TYPE)) { + if (ogs_strcasecmp(ogs_hash_this_val(hi), "application/json")) { + char *err = NULL; + const char *type; + type = ogs_hash_this_val(hi); + asprintf(&err, "Unsupported Media Type: received type: %s, should have been application/x-www-form-urlencoded", type); + ogs_error("%s", err); + + ogs_assert(true == nf_server_send_error(stream, 415, 2, &message, "Provisioning session does not exist.", err, NULL, m3_contenthostingprovisioning_api, app_meta)); + ogs_sbi_message_free(&message); + return; + } + } + } + } + + int purged_items_from_as = 0; + cJSON *entry; + cJSON *number_of_cache_entries = cJSON_Parse(response->http.content); + cJSON_ArrayForEach(entry, number_of_cache_entries) { + ogs_debug("Purged entries return %d\n", entry->valueint); + purged_items_from_as = entry->valueint; + + } + purge_node->m1_purge_info->purged_entries_total += purged_items_from_as; + + } + + + ogs_list_for_each_safe(&as_state->purge_content_hosting_cache, next, content_hosting_cache){ + if (purge_node->purge_regex) { + if(!strcmp(content_hosting_cache->provisioning_session_id, purge_node->provisioning_session_id) && !strcmp(content_hosting_cache->purge_regex, purge_node->purge_regex)) + break; + } else if(!strcmp(content_hosting_cache->provisioning_session_id, purge_node->provisioning_session_id)) { + break; + } + } + if(content_hosting_cache){ + ogs_list_remove(&as_state->purge_content_hosting_cache, content_hosting_cache); + ogs_debug("M1 List Purge refs: %d, Event Purge node refs: %d ", content_hosting_cache->m1_purge_info->refs, purge_node->m1_purge_info->refs); + + purge_node->m1_purge_info->refs--; + ogs_debug(" After decrement, M1 List Purge refs: %d, Event Purge node refs: %d ", content_hosting_cache->m1_purge_info->refs, purge_node->m1_purge_info->refs); + if(!purge_node->m1_purge_info->refs){ + // send M1 response with total from purge_node->m1_purge_info->purged_entries_total + // ogs_free(purge_node->m1_purge_info); + ogs_sbi_response_t *response; + cJSON *purged_entries_total_json = cJSON_CreateNumber(purge_node->m1_purge_info->purged_entries_total); + char *purged_entries_total = cJSON_Print(purged_entries_total_json); + response = ogs_sbi_response_new(); + response->http.content_length = strlen(purged_entries_total); + response->http.content = purged_entries_total; + response->status = 200; + ogs_sbi_header_set(response->http.headers, "Content-Type", "application/json"); + ogs_assert(response); + ogs_assert(true == ogs_sbi_server_send_response(purge_node->m1_purge_info->m1_stream, response)); + + if(content_hosting_cache->m1_purge_info) ogs_free(content_hosting_cache->m1_purge_info); + if (content_hosting_cache->provisioning_session_id) ogs_free(content_hosting_cache->provisioning_session_id); + if(content_hosting_cache->purge_regex) ogs_free(content_hosting_cache->purge_regex); + ogs_free(content_hosting_cache); + } + msaf_application_server_state_log(&as_state->purge_content_hosting_cache, "Purge Content Hosting Cache list"); + + } + } + + + if((response->status == 404) || (response->status == 413) || (response->status == 414) || (response->status == 415) || (response->status == 422) || (response->status == 500) || (response->status == 503)) { + char *error; + purge_resource_id_node_t *content_hosting_cache, *next = NULL; + cJSON *purge_cache_err = NULL; + if(response->http.content){ + purge_cache_err = cJSON_Parse(response->http.content); + char *txt = cJSON_Print(purge_cache_err); + ogs_debug("txt:%s", txt); + } + + if(response->status == 404) { + + ogs_error("Error message from the Application Server [%s] with response code [%d]: Cache not found\n", as_state->application_server->canonicalHostname, response->status); + } else if(response->status == 413) { + ogs_error("Error message from the Application Server [%s] with response code [%d]: Pay load too large\n", as_state->application_server->canonicalHostname, response->status); + } else if(response->status == 414) { + ogs_error("Error message from the Application Server [%s] with response code [%d]: URI too long\n", as_state->application_server->canonicalHostname, response->status); + } else if(response->status == 415) { + ogs_error("Error message from the Application Server [%s] with response code [%d]: Unsupported media type\n", as_state->application_server->canonicalHostname, response->status); + } else if(response->status == 422) { + ogs_error("Error message from the Application Server [%s] with response code [%d]: Unprocessable Entity\n", as_state->application_server->canonicalHostname, response->status); + } else if(response->status == 500) { + ogs_error("Error message from the Application Server [%s] with response code [%d]: Internal server error\n", as_state->application_server->canonicalHostname, response->status); + } else if(response->status == 503) { + ogs_error("Error message from the Application Server [%s] with response code [%d]: Service Unavailable\n", as_state->application_server->canonicalHostname, response->status); + } else { + + ogs_error("Application Server [%s] sent unrecognised response code [%d]", as_state->application_server->canonicalHostname, response->status); + } + + if (purge_node->purge_regex) { + error = ogs_msprintf("Application Server possibly encountered problem with regex %s", purge_node->purge_regex); + } else { + error = ogs_msprintf("Application Server unable to process the contained instructions"); + } + + + ogs_assert(true == nf_server_send_error( purge_node->m1_purge_info->m1_stream, + response->status, 3, &purge_node->m1_purge_info->m1_message, "Problem occured during cache purge", error, purge_cache_err, m1_contenthostingprovisioning_api, app_meta)); + + ogs_list_for_each_safe(&as_state->purge_content_hosting_cache, next, content_hosting_cache){ + if (purge_node->purge_regex) { + if(!strcmp(content_hosting_cache->provisioning_session_id, purge_node->provisioning_session_id) && !strcmp(content_hosting_cache->purge_regex, purge_node->purge_regex)) { + + ogs_list_remove(&as_state->purge_content_hosting_cache, content_hosting_cache); + ogs_debug("M1 List Purge refs: %d, Event Purge node refs: %d ", content_hosting_cache->m1_purge_info->refs, purge_node->m1_purge_info->refs); + if(content_hosting_cache->m1_purge_info) ogs_free(content_hosting_cache->m1_purge_info); + if (content_hosting_cache->provisioning_session_id) ogs_free(content_hosting_cache->provisioning_session_id); + if(content_hosting_cache->purge_regex) ogs_free(content_hosting_cache->purge_regex); + ogs_free(content_hosting_cache); + + } + } else if(!strcmp(content_hosting_cache->provisioning_session_id, purge_node->provisioning_session_id)) { + + ogs_list_remove(&as_state->purge_content_hosting_cache, content_hosting_cache); + ogs_debug("M1 List Purge refs: %d, Event Purge node refs: %d ", content_hosting_cache->m1_purge_info->refs, purge_node->m1_purge_info->refs); + if(content_hosting_cache->m1_purge_info) ogs_free(content_hosting_cache->m1_purge_info); + if (content_hosting_cache->provisioning_session_id) ogs_free(content_hosting_cache->provisioning_session_id); + if(content_hosting_cache->purge_regex) ogs_free(content_hosting_cache->purge_regex); + ogs_free(content_hosting_cache); + + } + } + cJSON_Delete(purge_cache_err); + + } + + next_action_for_application_server(as_state); + break; + END + break; + + } + } else if (message.h.resource.component[1]) { + + SWITCH(message.h.method) + CASE(OGS_SBI_HTTP_METHOD_POST) + + if (response->status == 201) { + + ogs_debug("[%s] Method [%s] with Response [%d] recieved for Content Hosting Configuration [%s]", message.h.resource.component[0], message.h.method, response->status, message.h.resource.component[1]); + + resource_id_node_t *content_hosting_configuration; + ogs_list_for_each(&as_state->upload_content_hosting_configurations,content_hosting_configuration) { + if(!strcmp(content_hosting_configuration->state, message.h.resource.component[1])) + break; + } + if(content_hosting_configuration) { + + ogs_debug("Removing %s from upload_content_hosting_configurations", content_hosting_configuration->state); + ogs_list_remove(&as_state->upload_content_hosting_configurations, content_hosting_configuration); + ogs_debug("Adding %s to current_content_hosting_configurations",content_hosting_configuration->state); + ogs_list_add(as_state->current_content_hosting_configurations, content_hosting_configuration); + } + + } + if(response->status == 405){ + ogs_error("Content Hosting Configuration resource already exist at the specified path\n"); + } + if(response->status == 413){ + ogs_error("Payload too large\n"); + } + if(response->status == 414){ + ogs_error("URI too long\n"); + } + if(response->status == 415){ + ogs_error("Unsupported media type\n"); + } + if(response->status == 500){ + ogs_error("Internal server error\n"); + } + if(response->status == 503){ + ogs_error("Service unavailable\n"); + } + next_action_for_application_server(as_state); + break; + CASE(OGS_SBI_HTTP_METHOD_PUT) + if(response->status == 200 || response->status == 204) { + + ogs_debug("[%s] Method [%s] with Response [%d] recieved for Content Hosting Configuration [%s]", message.h.resource.component[0], message.h.method, response->status, message.h.resource.component[1]); + resource_id_node_t *content_hosting_configuration; + ogs_list_for_each(&as_state->upload_content_hosting_configurations,content_hosting_configuration){ + if(!strcmp(content_hosting_configuration->state, message.h.resource.component[1])) + break; + } + if(content_hosting_configuration) { + + ogs_debug("Removing %s from upload_content_hosting_configurations", content_hosting_configuration->state); + ogs_free(content_hosting_configuration->state); + ogs_list_remove(&as_state->upload_content_hosting_configurations, content_hosting_configuration); + ogs_free(content_hosting_configuration); + } + + } + if(response->status == 404){ + ogs_error("Not Found\n"); + } + if(response->status == 413){ + ogs_error("Payload too large\n"); + } + if(response->status == 414){ + ogs_error("URI too long\n"); + } + if(response->status == 415){ + ogs_error("Unsupported Media Type\n"); + } + if(response->status == 500){ + ogs_error("Internal Server Error\n"); + } + if(response->status == 503){ + ogs_error("Service Unavailable\n"); + } + next_action_for_application_server(as_state); + break; + CASE(OGS_SBI_HTTP_METHOD_DELETE) + if(response->status == 204) { + + ogs_debug("[%s] Method [%s] with Response [%d] recieved for Content Hosting Configuration [%s]", message.h.resource.component[0], message.h.method, response->status,message.h.resource.component[1]); + + resource_id_node_t *content_hosting_configuration, *next = NULL; + resource_id_node_t *delete_content_hosting_configuration, *node = NULL; + + if(as_state->current_content_hosting_configurations) { + + ogs_list_for_each_safe(as_state->current_content_hosting_configurations, next, content_hosting_configuration){ + + if(!strcmp(content_hosting_configuration->state, message.h.resource.component[1])) + break; + } + } + + if(content_hosting_configuration) { + + msaf_application_server_state_log(as_state->current_content_hosting_configurations, "Current Content Hosting Configurations"); + + ogs_debug("Removing %s from current_content_hosting_configurations", content_hosting_configuration->state); + ogs_free(content_hosting_configuration->state); + ogs_list_remove(as_state->current_content_hosting_configurations, content_hosting_configuration); + ogs_free(content_hosting_configuration); + msaf_application_server_state_log(as_state->current_content_hosting_configurations, "Current Content Hosting Configurations"); + } + + ogs_list_for_each_safe(&as_state->delete_content_hosting_configurations, node, delete_content_hosting_configuration) { + + if (!strcmp(delete_content_hosting_configuration->state, message.h.resource.component[1])) { + + msaf_application_server_state_log(&as_state->delete_content_hosting_configurations, "Delete Content Hosting Configurations"); + + ogs_debug("Destroying Content Hosting Configuration: %s", delete_content_hosting_configuration->state); + ogs_free(delete_content_hosting_configuration->state); + ogs_list_remove(&as_state->delete_content_hosting_configurations, delete_content_hosting_configuration); + ogs_free(delete_content_hosting_configuration); + + msaf_application_server_state_log(&as_state->delete_content_hosting_configurations, "Delete Content Hosting Configurations"); + } + } + + } + if(response->status == 404){ + ogs_error("Not Found\n"); + } + if(response->status == 413){ + ogs_error("Payload too large\n"); + } + if(response->status == 414){ + ogs_error("URI too long\n"); + } + if(response->status == 415){ + ogs_error("Unsupported Media Type\n"); + } + if(response->status == 500){ + ogs_error("Internal Server Error\n"); + } + if(response->status == 503){ + ogs_error("Service Unavailable\n"); + } + next_action_for_application_server(as_state); + break; + DEFAULT + ogs_error("Unknown M3 Content Hosting Configuration operation [%s]", message.h.resource.component[1]); + ogs_assert(true == nf_server_send_error(stream, OGS_SBI_HTTP_STATUS_BAD_REQUEST, 0, &message, "Unknown M3 Content Hosting Configuration operation", ogs_strdup(message.h.resource.component[1]), NULL, NULL, app_meta)); + break; + END + break; + } else { + cJSON *entry; + cJSON *chc_array = cJSON_Parse(response->http.content); + resource_id_node_t *current_chc; + SWITCH(message.h.method) + CASE(OGS_SBI_HTTP_METHOD_GET) + + if(response->status == 200) { + + ogs_debug("[%s] Method [%s] with Response [%d] for Content Hosting Configuration operation [%s]", + message.h.resource.component[0], message.h.method, response->status, message.h.resource.component[1]); + + if (as_state->current_content_hosting_configurations == NULL) { + as_state->current_content_hosting_configurations = ogs_calloc(1,sizeof(*as_state->current_content_hosting_configurations)); + ogs_assert(as_state->current_content_hosting_configurations); + ogs_list_init(as_state->current_content_hosting_configurations); + + } else { + resource_id_node_t *next, *node; + ogs_list_for_each_safe(as_state->current_content_hosting_configurations, next, node) { + ogs_free(node->state); + ogs_list_remove(as_state->current_content_hosting_configurations, node); + ogs_free(node); + } + } + cJSON_ArrayForEach(entry, chc_array) { + char *id = strrchr(entry->valuestring, '/'); + if (id == NULL) { + id = entry->valuestring; + } else { + id++; + } + current_chc = ogs_calloc(1, sizeof(*current_chc)); + current_chc->state = ogs_strdup(id); + ogs_debug("Adding [%s] to the current Content Hosting Configuration list",current_chc->state); + ogs_list_add(as_state->current_content_hosting_configurations, current_chc); + } + + cJSON_Delete(chc_array); + } + if (response->status == 500){ + ogs_error("Received Internal Server error\n"); + } + if (response->status == 503) { + ogs_error("Service Unavailable\n"); + } + next_action_for_application_server(as_state); + break; + DEFAULT + char *err = NULL; + asprintf(&err, "Unknown M3 Content Hosting Configuration operation [%s] with method [%s]", message.h.resource.component[1], message.h.method); + ogs_error("%s", err); + ogs_assert(true == nf_server_send_error(stream, OGS_SBI_HTTP_STATUS_BAD_REQUEST, 0, &message, "Unknown M3 Content Hosting Configuration operation", err, NULL, NULL, app_meta)); + break; + END + break; + } + next_action_for_application_server(as_state); + + break; + + CASE("certificates") + + msaf_application_server_state_node_t *as_state; + as_state = e->application_server_state; + ogs_assert(as_state); + if (message.h.resource.component[1]) { + SWITCH(message.h.method) + CASE(OGS_SBI_HTTP_METHOD_POST) + if(response->status == 201) { + + ogs_debug("[%s] Method [%s] with Response [%d] recieved for certificate [%s]", message.h.resource.component[0], message.h.method, response->status, message.h.resource.component[1]); + + resource_id_node_t *certificate; + + //Iterate upload_certs and find match strcmp resource component 0 + ogs_list_for_each(&as_state->upload_certificates,certificate){ + if(!strcmp(certificate->state, message.h.resource.component[1])) + break; + } + if(certificate) { + + ogs_debug("Removing certificate [%s] from upload_certificates", certificate->state); + + ogs_list_remove(&as_state->upload_certificates, certificate); + + ogs_debug("Adding certificate [%s] to current_certificates", certificate->state); + + ogs_list_add(as_state->current_certificates, certificate); + // ogs_free(upload_cert_id); + } + } + if(response->status == 405){ + ogs_error("Server Certificate resource already exist at the specified path\n"); + } + if(response->status == 413){ + ogs_error("Payload too large\n"); + } + if(response->status == 414){ + ogs_error("URI too long\n"); + } + if(response->status == 415){ + ogs_error("Unsupported media type\n"); + } + if(response->status == 500){ + ogs_error("Internal server error\n"); + } + if(response->status == 503){ + ogs_error("Service unavailable\n"); + } + next_action_for_application_server(as_state); + break; + CASE(OGS_SBI_HTTP_METHOD_PUT) + if(response->status == 200 || response->status == 204) { + + ogs_debug("[%s] Method [%s] with Response [%d] recieved for certificate [%s]", message.h.resource.component[0], message.h.method, response->status,message.h.resource.component[1]); + + resource_id_node_t *certificate; + + msaf_application_server_state_log(&as_state->upload_certificates, "Upload Certificates"); + + //Iterate upload_certs and find match strcmp resource component 0 + ogs_list_for_each(&as_state->upload_certificates,certificate){ + + if(!strcmp(certificate->state, message.h.resource.component[1])) + break; + } + + if(!certificate){ + ogs_debug("Certificate %s not found in upload certificates", message.h.resource.component[1]); + } else { + ogs_debug("Removing certificate [%s] from upload_certificates", certificate->state); + ogs_free(certificate->state); + + ogs_list_remove(&as_state->upload_certificates, certificate); + ogs_free(certificate); + } + } + if(response->status == 404){ + ogs_error("Not Found\n"); + } + if(response->status == 413){ + ogs_error("Payload too large\n"); + } + if(response->status == 414){ + ogs_error("URI too long\n"); + } + if(response->status == 415){ + ogs_error("Unsupported Media Type\n"); + } + if(response->status == 500){ + ogs_error("Internal Server Error\n"); + } + if(response->status == 503){ + ogs_error("Service Unavailable\n"); + } + next_action_for_application_server(as_state); + break; + CASE(OGS_SBI_HTTP_METHOD_DELETE) + if(response->status == 204) { + + ogs_debug("[%s] Method [%s] with Response [%d] recieved for Certificate [%s]", message.h.resource.component[0], message.h.method, response->status,message.h.resource.component[1]); + + resource_id_node_t *certificate, *next = NULL; + resource_id_node_t *delete_certificate, *node = NULL; + + if(as_state->current_certificates) { + ogs_list_for_each_safe(as_state->current_certificates, next, certificate){ + + if(!strcmp(certificate->state, message.h.resource.component[1])) + break; + } + } + + if(certificate) { + + msaf_application_server_state_log(as_state->current_certificates, "Current Certificates"); + + ogs_debug("Removing certificate [%s] from current_certificates", certificate->state); + ogs_free(certificate->state); + + ogs_list_remove(as_state->current_certificates, certificate); + ogs_free(certificate); + msaf_application_server_state_log(as_state->current_certificates, "Current Certificates"); + } + + + ogs_list_for_each_safe(&as_state->delete_certificates, node, delete_certificate){ + + if(!strcmp(delete_certificate->state, message.h.resource.component[1])) { + msaf_application_server_state_log(&as_state->delete_certificates, "Delete Certificates"); + + ogs_debug("Destroying Certificate: %s", delete_certificate->state); + ogs_free(delete_certificate->state); + ogs_list_remove(&as_state->delete_certificates, delete_certificate); + ogs_free(delete_certificate); + msaf_application_server_state_log(&as_state->delete_certificates, "Delete Certificates"); + + } + } + } + if(response->status == 404){ + ogs_error("Not Found\n"); + } + if(response->status == 413){ + ogs_error("Payload too large\n"); + } + if(response->status == 414){ + ogs_error("URI too long\n"); + } + if(response->status == 415){ + ogs_error("Unsupported Media Type\n"); + } + if(response->status == 500){ + ogs_error("Internal Server Error\n"); + } + if(response->status == 503){ + ogs_error("Service Unavailable\n"); + } + next_action_for_application_server(as_state); + break; + DEFAULT + ogs_error("Unknown M3 certificate operation [%s]", message.h.resource.component[1]); + ogs_assert(true == nf_server_send_error(stream, OGS_SBI_HTTP_STATUS_BAD_REQUEST, 0, &message, "Unknown M3 certificate operation.", ogs_strdup(message.h.resource.component[1]), NULL, NULL, app_meta)); + break; + END + break; + } else { + cJSON *entry; + cJSON *cert_array = cJSON_Parse(response->http.content); + resource_id_node_t *current_cert; + SWITCH(message.h.method) + CASE(OGS_SBI_HTTP_METHOD_GET) + + if(response->status == 200) { + + ogs_debug("[%s] Method [%s] with Response [%d] received", + message.h.resource.component[0], message.h.method, response->status); + + if (as_state->current_certificates == NULL) { + as_state->current_certificates = ogs_calloc(1,sizeof(*as_state->current_certificates)); + ogs_assert(as_state->current_certificates); + ogs_list_init(as_state->current_certificates); + + } else { + resource_id_node_t *next, *node; + ogs_list_for_each_safe(as_state->current_certificates, next, node) { + + ogs_debug("Removing certificate [%s] from current_certificates", node->state); + + ogs_free(node->state); + ogs_list_remove(as_state->current_certificates, node); + ogs_free(node); + } + } + cJSON_ArrayForEach(entry, cert_array) { + char *id = strrchr(entry->valuestring, '/'); + if (id == NULL) { + id = entry->valuestring; + } else { + id++; + } + current_cert = ogs_calloc(1, sizeof(*current_cert)); + current_cert->state = ogs_strdup(id); + ogs_debug("Adding certificate [%s] to Current certificates", current_cert->state); + ogs_list_add(as_state->current_certificates, current_cert); + } + + cJSON_Delete(cert_array); + } + if (response->status == 500){ + ogs_error("Received Internal Server error"); + } + if (response->status == 503) { + ogs_error("Service Unavailable"); + } + next_action_for_application_server(as_state); + break; + DEFAULT + char *err = NULL; + asprintf(&err, "Unsupported M3 Certificate operation [%s] with method [%s]", message.h.resource.component[1], message.h.method); + ogs_error("%s", err); + ogs_assert(true == nf_server_send_error(stream, OGS_SBI_HTTP_STATUS_BAD_REQUEST, 0, &message, "Unknown M3 Certificate operation", err, NULL, NULL, app_meta)); + break; + END + break; + } + next_action_for_application_server(as_state); + + break; + + DEFAULT + ogs_error("Unknown M3 operation [%s]", message.h.resource.component[0]); + ogs_assert(true == nf_server_send_error(stream, OGS_SBI_HTTP_STATUS_BAD_REQUEST, 0, &message, "Unsupported M3 operation", ogs_strdup(message.h.resource.component[0]), NULL, NULL, app_meta)); + break; + END + break; +#if 0 + CASE(OGS_SBI_SERVICE_NAME_NNRF_NFM) + + SWITCH(message.h.resource.component[0]) + CASE(OGS_SBI_RESOURCE_NAME_NF_INSTANCES) + nf_instance = e->h.sbi.data; + ogs_assert(nf_instance); + ogs_assert(OGS_FSM_STATE(&nf_instance->sm)); + + e->h.sbi.message = &message; + ogs_fsm_dispatch(&nf_instance->sm, e); + break; + + CASE(OGS_SBI_RESOURCE_NAME_SUBSCRIPTIONS) + subscription_data = e->h.sbi.data; + ogs_assert(subscription_data); + + SWITCH(message.h.method) + CASE(OGS_SBI_HTTP_METHOD_POST) + if (message.res_status == OGS_SBI_HTTP_STATUS_CREATED || + message.res_status == OGS_SBI_HTTP_STATUS_OK) { + ogs_nnrf_nfm_handle_nf_status_subscribe( + subscription_data, &message); + } else { + ogs_error("HTTP response error : %d", + message.res_status); + } + break; + + CASE(OGS_SBI_HTTP_METHOD_DELETE) + if (message.res_status == OGS_SBI_HTTP_STATUS_NO_CONTENT) { + ogs_sbi_subscription_data_remove(subscription_data); + } else { + ogs_error("HTTP response error : %d", + message.res_status); + } + break; + + DEFAULT + ogs_error("Invalid HTTP method [%s]", message.h.method); + ogs_assert_if_reached(); + END + break; + + DEFAULT + ogs_error("Invalid resource name [%s]", + message.h.resource.component[0]); + ogs_assert_if_reached(); + END + break; +#endif + DEFAULT + ogs_error("Invalid service name [%s]", message.h.service.name); + ogs_assert_if_reached(); + END + + ogs_sbi_message_free(&message); + ogs_sbi_response_free(response); + break; +#if 0 + case OGS_EVENT_SBI_TIMER: + ogs_assert(e); + + switch(e->h.timer_id) { + case OGS_TIMER_NF_INSTANCE_REGISTRATION_INTERVAL: + case OGS_TIMER_NF_INSTANCE_HEARTBEAT_INTERVAL: + case OGS_TIMER_NF_INSTANCE_NO_HEARTBEAT: + case OGS_TIMER_NF_INSTANCE_VALIDITY: + nf_instance = e->h.sbi.data; + ogs_assert(nf_instance); + ogs_assert(OGS_FSM_STATE(&nf_instance->sm)); + + ogs_fsm_dispatch(&nf_instance->sm, e); + if (OGS_FSM_CHECK(&nf_instance->sm, ogs_sbi_nf_state_exception)) + ogs_error("State machine exception [%d]", e->h.timer_id); + break; + + case OGS_TIMER_SUBSCRIPTION_VALIDITY: + subscription_data = e->h.sbi.data; + ogs_assert(subscription_data); + + ogs_assert(true == + ogs_nnrf_nfm_send_nf_status_subscribe(subscription_data)); + + ogs_debug("Subscription validity expired [%s]", + subscription_data->id); + ogs_sbi_subscription_data_remove(subscription_data); + break; + + case OGS_TIMER_SBI_CLIENT_WAIT: + sbi_xact = e->h.sbi.data; + ogs_assert(sbi_xact); + + stream = sbi_xact->assoc_stream; + + ogs_sbi_xact_remove(sbi_xact); + + ogs_error("Cannot receive SBI message"); + if (stream) { + ogs_assert(true == + ogs_sbi_server_send_error(stream, + OGS_SBI_HTTP_STATUS_GATEWAY_TIMEOUT, NULL, + "Cannot receive SBI message", NULL)); + } + break; + + default: + ogs_error("Unknown timer[%s:%d]", + ogs_timer_get_name(e->h.timer_id), e->h.timer_id); + } + break; +#endif + default: + ogs_error("No handler for event %s", msaf_event_get_name(e)); + break; + } + ogs_free(nf_name); +} + +/* vim:ts=8:sts=4:sw=4:expandtab: +*/ diff --git a/src/5gmsaf/msaf-m5-sm.c b/src/5gmsaf/msaf-m5-sm.c new file mode 100644 index 0000000..eed271b --- /dev/null +++ b/src/5gmsaf/msaf-m5-sm.c @@ -0,0 +1,293 @@ +/* + * License: 5G-MAG Public License (v1.0) + * Author: Dev Audsin + * Copyright: (C) 2022 British Broadcasting Corporation + * + * For full license terms please see the LICENSE file distributed with this + * program. If this file is missing then the license can be retrieved from + * https://drive.google.com/file/d/1cinCiA778IErENZ3JN52VFW-1ffHpx7Z/view + */ + + +#include "ogs-sbi.h" +#include "sbi-path.h" +#include "context.h" +#include "certmgr.h" +#include "server.h" +#include "response-cache-control.h" +#include "msaf-version.h" +#include "msaf-sm.h" +#include "openapi/api/TS26512_M5_ServiceAccessInformationAPI-info.h" + +const nf_server_interface_metadata_t +m5_serviceaccessinformation_api_metadata = { + M5_SERVICEACCESSINFORMATION_API_NAME, + M5_SERVICEACCESSINFORMATION_API_VERSION +}; + +void msaf_m5_state_initial(ogs_fsm_t *s, msaf_event_t *e) +{ + msaf_sm_debug(e); + + ogs_assert(s); + + OGS_FSM_TRAN(s, &msaf_m5_state_functional); +} + +void msaf_m5_state_final(ogs_fsm_t *s, msaf_event_t *e) +{ + msaf_sm_debug(e); + + ogs_assert(s); +} + +void msaf_m5_state_functional(ogs_fsm_t *s, msaf_event_t *e) +{ + ogs_sbi_stream_t *stream = NULL; + ogs_sbi_request_t *request = NULL; + ogs_sbi_message_t message; + + msaf_sm_debug(e); + + char *nf_name = ogs_msprintf("5GMSdAF-%s", msaf_self()->server_name); + const nf_server_app_metadata_t app_metadata = { MSAF_NAME, MSAF_VERSION, nf_name}; + const nf_server_interface_metadata_t *m5_serviceaccessinformation_api = &m5_serviceaccessinformation_api_metadata; + const nf_server_app_metadata_t *app_meta = &app_metadata; + + ogs_assert(s); + + switch (e->h.id) { + case OGS_FSM_ENTRY_SIG: + ogs_info("[%s] MSAF M5 Running", ogs_sbi_self()->nf_instance->id); + + break; + + case OGS_FSM_EXIT_SIG: + break; + + case OGS_EVENT_SBI_SERVER: + request = e->h.sbi.request; + ogs_assert(request); + stream = e->h.sbi.data; + ogs_assert(stream); + message = *(e->message); + + SWITCH(message.h.service.name) + CASE("3gpp-m5") + if (strcmp(message.h.api.version, "v2") != 0) { + char *error; + error = ogs_msprintf("Version [%s] not supported", message.h.api.version); + ogs_error("%s", error); + ogs_assert(true == nf_server_send_error(stream, OGS_SBI_HTTP_STATUS_BAD_REQUEST, 1, NULL, "Not supported version", error, NULL, NULL, app_meta)); + + ogs_sbi_message_free(&message); + break; + } + SWITCH(message.h.resource.component[0]) + CASE("service-access-information") + SWITCH(message.h.method) + CASE(OGS_SBI_HTTP_METHOD_GET) + cJSON *service_access_information; + msaf_provisioning_session_t *msaf_provisioning_session = NULL; + msaf_provisioning_session = msaf_provisioning_session_find_by_provisioningSessionId(message.h.resource.component[1]); + + if(msaf_provisioning_session == NULL) { + char *err = NULL; + asprintf(&err,"Provisioning Session [%s] not found.", message.h.resource.component[1]); + ogs_error("%s", err); + ogs_assert(true == nf_server_send_error(stream, 404, 1, &message, "Provisioning Session not found.", err, NULL, m5_serviceaccessinformation_api, app_meta)); + } else if (msaf_provisioning_session->serviceAccessInformation) { + service_access_information = msaf_context_retrieve_service_access_information(message.h.resource.component[1]); + if (service_access_information != NULL) { + ogs_sbi_response_t *response; + char *text; + text = cJSON_Print(service_access_information); + response = nf_server_new_response(NULL, "application/json", msaf_provisioning_session->serviceAccessInformationCreated, msaf_provisioning_session->serviceAccessInformationHash, msaf_self()->config.server_response_cache_control->m5_service_access_information_response_max_age, NULL, m5_serviceaccessinformation_api, app_meta); + nf_server_populate_response(response, strlen(text), text, 201); + ogs_assert(response); + ogs_assert(true == ogs_sbi_server_send_response(stream, response)); + cJSON_Delete(service_access_information); + } else { + char *err = NULL; + asprintf(&err,"Service Access Information for the Provisioning Session [%s] not found.", message.h.resource.component[1]); + ogs_error("%s", err); + + ogs_assert(true == nf_server_send_error(stream, 404, 1, &message, "Service Access Information not found.", err, NULL, m5_serviceaccessinformation_api, app_meta)); + } + } else { + char *err = NULL; + asprintf(&err,"Provisioning Session [%s] has no Service Access Information associated with it.", message.h.resource.component[1]); + ogs_error("%s", err); + + ogs_assert(true == nf_server_send_error(stream, 404, 1, &message, "Service Access Information not found.", err, NULL, m5_serviceaccessinformation_api, app_meta)); + } + break; + DEFAULT + ogs_error("Invalid HTTP method [%s]", message.h.method); + + ogs_assert(true == nf_server_send_error(stream, OGS_SBI_HTTP_STATUS_FORBIDDEN, 1, &message, "Invalid HTTP method.", ogs_strdup(message.h.method), NULL, NULL, app_meta)); + + END + break; + DEFAULT + ogs_error("Invalid resource name [%s]", + message.h.resource.component[0]); + ogs_assert(true == nf_server_send_error(stream, OGS_SBI_HTTP_STATUS_BAD_REQUEST, 1, &message, "Invalid resource name.", ogs_strdup(message.h.resource.component[0]), NULL, NULL, app_meta)); + + END + ogs_sbi_message_free(&message); + break; + DEFAULT + ogs_error("Invalid API name [%s]", message.h.service.name); + ogs_assert(true == nf_server_send_error(stream, OGS_SBI_HTTP_STATUS_BAD_REQUEST, 1, &message, "Invalid API name.", ogs_strdup(message.h.service.name), NULL, NULL, app_meta)); + + END + break; +#if 0 + case OGS_EVENT_SBI_CLIENT: + ogs_assert(e); + + response = e->h.sbi.response; + ogs_assert(response); + rv = ogs_sbi_parse_header(&message, &response->h); + if (rv != OGS_OK) { + ogs_error("ogs_sbi_parse_header() failed"); + ogs_sbi_message_free(&message); + ogs_sbi_response_free(response); + break; + } + { + ogs_hash_index_t *hi; + for (hi = ogs_hash_first(response->http.headers); + hi; hi = ogs_hash_next(hi)) { + if (!ogs_strcasecmp(ogs_hash_this_key(hi), OGS_SBI_CONTENT_TYPE)) { + message.http.content_type = ogs_hash_this_val(hi); + } else if (!ogs_strcasecmp(ogs_hash_this_key(hi), OGS_SBI_LOCATION)) { + message.http.location = ogs_hash_this_val(hi); + } + } + } + + message.res_status = response->status; + + SWITCH(message.h.service.name) + CASE(OGS_SBI_SERVICE_NAME_NNRF_NFM) + + SWITCH(message.h.resource.component[0]) + CASE(OGS_SBI_RESOURCE_NAME_NF_INSTANCES) + nf_instance = e->h.sbi.data; + ogs_assert(nf_instance); + ogs_assert(OGS_FSM_STATE(&nf_instance->sm)); + + e->h.sbi.message = &message; + ogs_fsm_dispatch(&nf_instance->sm, e); + break; + + CASE(OGS_SBI_RESOURCE_NAME_SUBSCRIPTIONS) + subscription_data = e->h.sbi.data; + ogs_assert(subscription_data); + + SWITCH(message.h.method) + CASE(OGS_SBI_HTTP_METHOD_POST) + if (message.res_status == OGS_SBI_HTTP_STATUS_CREATED || + message.res_status == OGS_SBI_HTTP_STATUS_OK) { + ogs_nnrf_nfm_handle_nf_status_subscribe( + subscription_data, &message); + } else { + ogs_error("HTTP response error : %d", + message.res_status); + } + break; + + CASE(OGS_SBI_HTTP_METHOD_DELETE) + if (message.res_status == OGS_SBI_HTTP_STATUS_NO_CONTENT) { + ogs_sbi_subscription_data_remove(subscription_data); + } else { + ogs_error("HTTP response error : %d", + message.res_status); + } + break; + + DEFAULT + ogs_error("Invalid HTTP method [%s]", message.h.method); + ogs_assert_if_reached(); + END + break; + + DEFAULT + ogs_error("Invalid resource name [%s]", + message.h.resource.component[0]); + ogs_assert_if_reached(); + END + break; + + DEFAULT + ogs_error("Invalid service name [%s]", message.h.service.name); + ogs_assert_if_reached(); + END + + ogs_sbi_message_free(&message); + ogs_sbi_response_free(response); + break; + + case OGS_EVENT_SBI_TIMER: + ogs_assert(e); + + switch(e->h.timer_id) { + case OGS_TIMER_NF_INSTANCE_REGISTRATION_INTERVAL: + case OGS_TIMER_NF_INSTANCE_HEARTBEAT_INTERVAL: + case OGS_TIMER_NF_INSTANCE_NO_HEARTBEAT: + case OGS_TIMER_NF_INSTANCE_VALIDITY: + nf_instance = e->h.sbi.data; + ogs_assert(nf_instance); + ogs_assert(OGS_FSM_STATE(&nf_instance->sm)); + + ogs_fsm_dispatch(&nf_instance->sm, e); + if (OGS_FSM_CHECK(&nf_instance->sm, ogs_sbi_nf_state_exception)) + ogs_error("State machine exception [%d]", e->h.timer_id); + break; + + case OGS_TIMER_SUBSCRIPTION_VALIDITY: + subscription_data = e->h.sbi.data; + ogs_assert(subscription_data); + + ogs_assert(true == + ogs_nnrf_nfm_send_nf_status_subscribe(subscription_data)); + + ogs_debug("Subscription validity expired [%s]", + subscription_data->id); + ogs_sbi_subscription_data_remove(subscription_data); + break; + + case OGS_TIMER_SBI_CLIENT_WAIT: + sbi_xact = e->h.sbi.data; + ogs_assert(sbi_xact); + + stream = sbi_xact->assoc_stream; + + ogs_sbi_xact_remove(sbi_xact); + + ogs_error("Cannot receive SBI message"); + if (stream) { + ogs_assert(true == + ogs_sbi_server_send_error(stream, + OGS_SBI_HTTP_STATUS_GATEWAY_TIMEOUT, NULL, + "Cannot receive SBI message", NULL)); + } + break; + + default: + ogs_error("Unknown timer[%s:%d]", + ogs_timer_get_name(e->h.timer_id), e->h.timer_id); + } + break; +#endif + default: + ogs_error("No handler for event %s", msaf_event_get_name(e)); + break; + } + ogs_free(nf_name); +} + +/* vim:ts=8:sts=4:sw=4:expandtab: +*/ diff --git a/src/5gmsaf/msaf-mgmt-sm.c b/src/5gmsaf/msaf-mgmt-sm.c new file mode 100644 index 0000000..dd9c72e --- /dev/null +++ b/src/5gmsaf/msaf-mgmt-sm.c @@ -0,0 +1,232 @@ +/* + * License: 5G-MAG Public License (v1.0) + * Copyright: (C) 2022 British Broadcasting Corporation + * + * For full license terms please see the LICENSE file distributed with this + * program. If this file is missing then the license can be retrieved from + * https://drive.google.com/file/d/1cinCiA778IErENZ3JN52VFW-1ffHpx7Z/view + */ + +#include "ogs-sbi.h" +#include "sbi-path.h" +#include "context.h" +#include "certmgr.h" +#include "server.h" +#include "response-cache-control.h" +#include "msaf-version.h" +#include "msaf-sm.h" +#include "openapi/api/Maf_ManagementAPI-info.h" + +const nf_server_interface_metadata_t +maf_mgmt_api_metadata = { + MAF_MANAGEMENT_API_NAME, + MAF_MANAGEMENT_API_VERSION +}; + +void msaf_maf_mgmt_state_initial(ogs_fsm_t *s, msaf_event_t *e) +{ + msaf_sm_debug(e); + + ogs_assert(s); + + OGS_FSM_TRAN(s, &msaf_maf_mgmt_state_functional); +} + +void msaf_maf_mgmt_state_final(ogs_fsm_t *s, msaf_event_t *e) +{ + msaf_sm_debug(e); + + ogs_assert(s); +} + +void msaf_maf_mgmt_state_functional(ogs_fsm_t *s, msaf_event_t *e) +{ + ogs_sbi_stream_t *stream = NULL; + ogs_sbi_request_t *request = NULL; + ogs_sbi_message_t message; + + msaf_sm_debug(e); + + if (!msaf_self()->server_name[0]) msaf_context_server_name_set(); + char *nf_name = ogs_msprintf("5GMSdAF-%s", msaf_self()->server_name); + const nf_server_app_metadata_t app_metadata = { MSAF_NAME, MSAF_VERSION, nf_name}; + const nf_server_interface_metadata_t *maf_management_api = &maf_mgmt_api_metadata; + const nf_server_app_metadata_t *app_meta = &app_metadata; + + ogs_assert(s); + + switch (e->h.id) { + case OGS_FSM_ENTRY_SIG: + ogs_info("[%s] MSAF Management Interface Running", ogs_sbi_self()->nf_instance->id); + break; + + case OGS_FSM_EXIT_SIG: + break; + + case OGS_EVENT_SBI_SERVER: + request = e->h.sbi.request; + ogs_assert(request); + stream = e->h.sbi.data; + ogs_assert(stream); + message = *(e->message); + + SWITCH(message.h.service.name) + CASE("5gmag-rt-management") + ogs_fsm_dispatch(&msaf_self()->msaf_fsm.msaf_m1_sm, e); + break; + + DEFAULT + ogs_error("Resource [%s] not found.", message.h.service.name); + ogs_assert(true == nf_server_send_error(stream, OGS_SBI_HTTP_STATUS_NOT_FOUND, 0, &message, "Not Found.", ogs_strdup(message.h.service.name), NULL, maf_management_api, app_meta)); + + END + break; +#if 0 + case OGS_EVENT_SBI_CLIENT: + ogs_assert(e); + + response = e->h.sbi.response; + ogs_assert(response); + rv = ogs_sbi_parse_header(&message, &response->h); + if (rv != OGS_OK) { + ogs_error("ogs_sbi_parse_header() failed"); + ogs_sbi_message_free(&message); + ogs_sbi_response_free(response); + break; + } + { + ogs_hash_index_t *hi; + for (hi = ogs_hash_first(response->http.headers); + hi; hi = ogs_hash_next(hi)) { + if (!ogs_strcasecmp(ogs_hash_this_key(hi), OGS_SBI_CONTENT_TYPE)) { + message.http.content_type = ogs_hash_this_val(hi); + } else if (!ogs_strcasecmp(ogs_hash_this_key(hi), OGS_SBI_LOCATION)) { + message.http.location = ogs_hash_this_val(hi); + } + } + } + + message.res_status = response->status; + + SWITCH(message.h.service.name) + + CASE(OGS_SBI_SERVICE_NAME_NNRF_NFM) + + SWITCH(message.h.resource.component[0]) + CASE(OGS_SBI_RESOURCE_NAME_NF_INSTANCES) + nf_instance = e->h.sbi.data; + ogs_assert(nf_instance); + ogs_assert(OGS_FSM_STATE(&nf_instance->sm)); + + e->h.sbi.message = &message; + ogs_fsm_dispatch(&nf_instance->sm, e); + break; + + CASE(OGS_SBI_RESOURCE_NAME_SUBSCRIPTIONS) + subscription_data = e->h.sbi.data; + ogs_assert(subscription_data); + + SWITCH(message.h.method) + CASE(OGS_SBI_HTTP_METHOD_POST) + if (message.res_status == OGS_SBI_HTTP_STATUS_CREATED || + message.res_status == OGS_SBI_HTTP_STATUS_OK) { + ogs_nnrf_nfm_handle_nf_status_subscribe( + subscription_data, &message); + } else { + ogs_error("HTTP response error : %d", + message.res_status); + } + break; + + CASE(OGS_SBI_HTTP_METHOD_DELETE) + if (message.res_status == OGS_SBI_HTTP_STATUS_NO_CONTENT) { + ogs_sbi_subscription_data_remove(subscription_data); + } else { + ogs_error("HTTP response error : %d", + message.res_status); + } + break; + + DEFAULT + ogs_error("Invalid HTTP method [%s]", message.h.method); + ogs_assert_if_reached(); + END + break; + + DEFAULT + ogs_error("Invalid resource name [%s]", + message.h.resource.component[0]); + ogs_assert_if_reached(); + END + break; + + DEFAULT + ogs_error("Invalid service name [%s]", message.h.service.name); + ogs_assert_if_reached(); + END + + ogs_sbi_message_free(&message); + ogs_sbi_response_free(response); + break; + + case OGS_EVENT_SBI_TIMER: + ogs_assert(e); + + switch(e->h.timer_id) { + case OGS_TIMER_NF_INSTANCE_REGISTRATION_INTERVAL: + case OGS_TIMER_NF_INSTANCE_HEARTBEAT_INTERVAL: + case OGS_TIMER_NF_INSTANCE_NO_HEARTBEAT: + case OGS_TIMER_NF_INSTANCE_VALIDITY: + nf_instance = e->h.sbi.data; + ogs_assert(nf_instance); + ogs_assert(OGS_FSM_STATE(&nf_instance->sm)); + + ogs_fsm_dispatch(&nf_instance->sm, e); + if (OGS_FSM_CHECK(&nf_instance->sm, ogs_sbi_nf_state_exception)) + ogs_error("State machine exception [%d]", e->h.timer_id); + break; + + case OGS_TIMER_SUBSCRIPTION_VALIDITY: + subscription_data = e->h.sbi.data; + ogs_assert(subscription_data); + + ogs_assert(true == + ogs_nnrf_nfm_send_nf_status_subscribe(subscription_data)); + + ogs_debug("Subscription validity expired [%s]", + subscription_data->id); + ogs_sbi_subscription_data_remove(subscription_data); + break; + + case OGS_TIMER_SBI_CLIENT_WAIT: + sbi_xact = e->h.sbi.data; + ogs_assert(sbi_xact); + + stream = sbi_xact->assoc_stream; + + ogs_sbi_xact_remove(sbi_xact); + + ogs_error("Cannot receive SBI message"); + if (stream) { + ogs_assert(true == + ogs_sbi_server_send_error(stream, + OGS_SBI_HTTP_STATUS_GATEWAY_TIMEOUT, NULL, + "Cannot receive SBI message", NULL)); + } + break; + + default: + ogs_error("Unknown timer[%s:%d]", + ogs_timer_get_name(e->h.timer_id), e->h.timer_id); + } + break; +#endif + default: + ogs_error("No handler for event %s", msaf_event_get_name(e)); + break; + } + ogs_free(nf_name); +} + +/* vim:ts=8:sts=4:sw=4:expandtab: +*/ diff --git a/src/5gmsaf/msaf-sm.c b/src/5gmsaf/msaf-sm.c index 84c61f5..ef8ab51 100644 --- a/src/5gmsaf/msaf-sm.c +++ b/src/5gmsaf/msaf-sm.c @@ -12,8 +12,25 @@ #include "ogs-sbi.h" #include "sbi-path.h" #include "context.h" - -static void report_purging_error(msaf_application_server_state_node_t *as_state, ogs_sbi_message_t *message, int status_code, const char *status_msg); +#include "certmgr.h" +#include "server.h" +#include "response-cache-control.h" +#include "msaf-version.h" +#include "msaf-sm.h" +#include "openapi/api/TS26512_M1_ContentHostingProvisioningAPI-info.h" +#include "openapi/api/M3_ContentHostingProvisioningAPI-info.h" + +static const nf_server_interface_metadata_t +m1_contenthostingprovisioning_api_metadata = { + M1_CONTENTHOSTINGPROVISIONING_API_NAME, + M1_CONTENTHOSTINGPROVISIONING_API_VERSION +}; + +static const nf_server_interface_metadata_t +m3_contenthostingprovisioning_api_metatdata = { + M3_CONTENTHOSTINGPROVISIONING_API_NAME, + M3_CONTENTHOSTINGPROVISIONING_API_VERSION +}; void msaf_state_initial(ogs_fsm_t *s, msaf_event_t *e) { @@ -27,6 +44,7 @@ void msaf_state_initial(ogs_fsm_t *s, msaf_event_t *e) void msaf_state_final(ogs_fsm_t *s, msaf_event_t *e) { msaf_sm_debug(e); + msaf_fsm_fini(); ogs_assert(s); } @@ -46,18 +64,19 @@ void msaf_state_functional(ogs_fsm_t *s, msaf_event_t *e) msaf_sm_debug(e); + msaf_context_server_name_set(); + char *nf_name = ogs_msprintf("5GMSdAF-%s", msaf_self()->server_name); + const nf_server_app_metadata_t app_metadata = { MSAF_NAME, MSAF_VERSION, nf_name}; + static const nf_server_interface_metadata_t *m3_contenthostingprovisioning_api = &m3_contenthostingprovisioning_api_metatdata; + static const nf_server_interface_metadata_t *m1_contenthostingprovisioning_api = &m1_contenthostingprovisioning_api_metadata; + const nf_server_app_metadata_t *app_meta = &app_metadata; + ogs_assert(s); switch (e->h.id) { case OGS_FSM_ENTRY_SIG: + msaf_fsm_init(); ogs_info("[%s] MSAF Running", ogs_sbi_self()->nf_instance->id); - { /* TODO: Remove this when M1 is active */ - msaf_provisioning_session_t *ps; - ps = msaf_provisioning_session_create("DOWNLINK", NULL, "5GMS-AF internal"); - ogs_info("Provisioning session = %s", ps->provisioningSessionId); - ogs_assert(msaf_self()->config.contentHostingConfiguration); - } - break; case OGS_FSM_EXIT_SIG: @@ -69,1155 +88,808 @@ void msaf_state_functional(ogs_fsm_t *s, msaf_event_t *e) stream = e->h.sbi.data; ogs_assert(stream); + if (!strcmp(request->h.method, OGS_SBI_HTTP_METHOD_OPTIONS) && !strcmp(request->h.uri, "*")){ + char *methods = NULL; + ogs_sbi_response_t *response; + methods = ogs_msprintf("%s, %s, %s, %s, %s",OGS_SBI_HTTP_METHOD_POST, OGS_SBI_HTTP_METHOD_GET, OGS_SBI_HTTP_METHOD_PUT, OGS_SBI_HTTP_METHOD_DELETE, OGS_SBI_HTTP_METHOD_OPTIONS); + response = nf_server_new_response(NULL, NULL, 0, NULL, 0, methods, NULL, app_meta); + nf_server_populate_response(response, 0, NULL, 204); + ogs_assert(response); + ogs_assert(true == ogs_sbi_server_send_response(stream, response)); + ogs_free(methods); + break; + } + rv = ogs_sbi_parse_header(&message, &request->h); if (rv != OGS_OK) { ogs_error("ogs_sbi_parse_header() failed"); - ogs_assert(true == - ogs_sbi_server_send_error( - stream, OGS_SBI_HTTP_STATUS_BAD_REQUEST, - NULL, "cannot parse HTTP message", NULL)); + ogs_assert(true == nf_server_send_error(stream, OGS_SBI_HTTP_STATUS_BAD_REQUEST, 1, NULL, "cannot parse HTTP message", NULL, NULL, NULL, app_meta)); + break; } SWITCH(message.h.service.name) - CASE(OGS_SBI_SERVICE_NAME_NNRF_NFM) - if (strcmp(message.h.api.version, OGS_SBI_API_V1) != 0) { - ogs_error("Not supported version [%s]", message.h.api.version); - ogs_assert(true == ogs_sbi_server_send_error( - stream, OGS_SBI_HTTP_STATUS_BAD_REQUEST, - &message, "Not supported version", NULL)); - ogs_sbi_message_free(&message); + CASE(OGS_SBI_SERVICE_NAME_NNRF_NFM) + if (strcmp(message.h.api.version, OGS_SBI_API_V1) != 0) { + ogs_error("Not supported version [%s]", message.h.api.version); + ogs_assert(true == ogs_sbi_server_send_error( + stream, OGS_SBI_HTTP_STATUS_BAD_REQUEST, + &message, "Not supported version", NULL)); + ogs_sbi_message_free(&message); + break; + } + SWITCH(message.h.resource.component[0]) + CASE(OGS_SBI_RESOURCE_NAME_NF_STATUS_NOTIFY) + SWITCH(message.h.method) + CASE(OGS_SBI_HTTP_METHOD_POST) + ogs_nnrf_nfm_handle_nf_status_notify(stream, &message); break; - } - SWITCH(message.h.resource.component[0]) - CASE(OGS_SBI_RESOURCE_NAME_NF_STATUS_NOTIFY) - SWITCH(message.h.method) - CASE(OGS_SBI_HTTP_METHOD_POST) - ogs_nnrf_nfm_handle_nf_status_notify(stream, &message); - break; - - DEFAULT - ogs_error("Invalid HTTP method [%s]", message.h.method); - ogs_assert(true == - ogs_sbi_server_send_error(stream, - OGS_SBI_HTTP_STATUS_FORBIDDEN, &message, - "Invalid HTTP method", message.h.method)); - END - break; - DEFAULT - ogs_error("Invalid resource name [%s]", - message.h.resource.component[0]); - ogs_assert(true == - ogs_sbi_server_send_error(stream, - OGS_SBI_HTTP_STATUS_BAD_REQUEST, &message, - "Invalid resource name", - message.h.resource.component[0])); + DEFAULT + ogs_error("Invalid HTTP method [%s]", message.h.method); + ogs_assert(true == + ogs_sbi_server_send_error(stream, + OGS_SBI_HTTP_STATUS_FORBIDDEN, &message, + "Invalid HTTP method", message.h.method)); END - ogs_sbi_message_free(&message); break; - CASE("3gpp-m1") - if (strcmp(message.h.api.version, "v2") != 0) { - ogs_error("Not supported version [%s]", message.h.api.version); - ogs_assert(true == - ogs_sbi_server_send_error( - stream, OGS_SBI_HTTP_STATUS_BAD_REQUEST, - &message, "Not supported version", NULL)); - ogs_sbi_message_free(&message); - break; + DEFAULT + ogs_error("Invalid resource name [%s]", + message.h.resource.component[0]); + ogs_assert(true == + ogs_sbi_server_send_error(stream, + OGS_SBI_HTTP_STATUS_BAD_REQUEST, &message, + "Invalid resource name", + message.h.resource.component[0])); + END + ogs_sbi_message_free(&message); + break; + + CASE("3gpp-m1") + if(check_event_addresses(e, msaf_self()->config.m1_server_sockaddr, msaf_self()->config.m1_server_sockaddr_v6)){ + e->message = &message; + ogs_fsm_dispatch(&msaf_self()->msaf_fsm.msaf_m1_sm, e); + + + } else { + char *error; + error = ogs_msprintf("Resource [%s] not found.", request->h.uri); + ogs_assert(true == nf_server_send_error(stream, OGS_SBI_HTTP_STATUS_NOT_FOUND, 1, NULL, "Not Found.", error, NULL, NULL, app_meta)); + ogs_free(error); + } + ogs_sbi_message_free(&message); + break; + + CASE("5gmag-rt-management") + if(!msaf_self()->config.maf_mgmt_server_sockaddr && !msaf_self()->config.maf_mgmt_server_sockaddr_v6) { + if(check_event_addresses(e, msaf_self()->config.m1_server_sockaddr, msaf_self()->config.m1_server_sockaddr_v6)){ + e->message = &message; + ogs_fsm_dispatch(&msaf_self()->msaf_fsm.msaf_m1_sm, e); } - SWITCH(message.h.resource.component[0]) - CASE("provisioning-sessions") - SWITCH(message.h.method) - CASE(OGS_SBI_HTTP_METHOD_POST) - - if (message.h.resource.component[1] && message.h.resource.component[2] && message.h.resource.component[3]) { - msaf_provisioning_session_t *msaf_provisioning_session; - if (!strcmp(message.h.resource.component[2],"content-hosting-configuration") && !strcmp(message.h.resource.component[3],"purge")) { - msaf_provisioning_session = msaf_provisioning_session_find_by_provisioningSessionId(message.h.resource.component[1]); - if (msaf_provisioning_session) { - // process the POST body - purge_resource_id_node_t *purge_cache; - msaf_application_server_state_node_t *as_state; - assigned_provisioning_sessions_node_t *assigned_provisioning_sessions_resource; - ogs_list_for_each(&msaf_provisioning_session->msaf_application_server_state_nodes, as_state) { - if (as_state->application_server && as_state->application_server->canonicalHostname) { - ogs_list_for_each(&as_state->assigned_provisioning_sessions,assigned_provisioning_sessions_resource) { - if (!strcmp(assigned_provisioning_sessions_resource->assigned_provisioning_session->provisioningSessionId, msaf_provisioning_session->provisioningSessionId)) { - - purge_cache = ogs_calloc(1, sizeof(purge_resource_id_node_t)); - ogs_assert(purge_cache); - purge_cache->state = ogs_strdup(assigned_provisioning_sessions_resource->assigned_provisioning_session->provisioningSessionId); - if (request->http.content) - purge_cache->purge_regex = ogs_strdup(request->http.content); - if (ogs_list_first(&as_state->purge_content_hosting_cache) == NULL) - ogs_list_init(&as_state->purge_content_hosting_cache); - - ogs_list_add(&as_state->purge_content_hosting_cache, purge_cache); - } - } - } + } else + if(check_event_addresses(e, msaf_self()->config.maf_mgmt_server_sockaddr, msaf_self()->config.maf_mgmt_server_sockaddr_v6)){ + e->message = &message; + ogs_fsm_dispatch(&msaf_self()->msaf_fsm.msaf_maf_mgmt_sm, e); + + } else { + char *error; + error = ogs_msprintf("Resource [%s] not found.", request->h.uri); + ogs_assert(true == nf_server_send_error(stream, OGS_SBI_HTTP_STATUS_NOT_FOUND, 1, NULL, "Not Found.", error, NULL, NULL, app_meta)); + ogs_free(error); + } + ogs_sbi_message_free(&message); + break; + + CASE("3gpp-m5") + if(check_event_addresses(e, msaf_self()->config.m5_server_sockaddr, msaf_self()->config.m5_server_sockaddr_v6)){ + e->message = &message; + ogs_fsm_dispatch(&msaf_self()->msaf_fsm.msaf_m5_sm, e); + + } else { + char *error; + error = ogs_msprintf("Resource [%s] not found.", request->h.uri); + ogs_assert(true == nf_server_send_error(stream, OGS_SBI_HTTP_STATUS_NOT_FOUND, 1, NULL, "Not Found.", error, NULL, NULL, app_meta)); + ogs_free(error); + } + ogs_sbi_message_free(&message); + break; + DEFAULT + ogs_error("Invalid API name [%s]", message.h.service.name); + ogs_assert(true == nf_server_send_error(stream, OGS_SBI_HTTP_STATUS_BAD_REQUEST, 0, &message, "Invalid API name.", message.h.service.name, NULL, NULL, app_meta)); - as_state->stream = stream; + END + break; - next_action_for_application_server(as_state); - } - } else { - ogs_error("Unable to retrieve the Provisioning Session"); - } + case OGS_EVENT_SBI_CLIENT: + ogs_assert(e); - } - } else if (message.h.resource.component[1] && message.h.resource.component[2]) { - msaf_provisioning_session_t *msaf_provisioning_session; - if (!strcmp(message.h.resource.component[2],"content-hosting-configuration")) { - msaf_provisioning_session = msaf_provisioning_session_find_by_provisioningSessionId(message.h.resource.component[1]); - if (msaf_provisioning_session) { - // process the POST body - cJSON *entry; - int rv; - cJSON *chc; - cJSON *content_hosting_config = cJSON_Parse(request->http.content); - char *txt = cJSON_Print(content_hosting_config); - ogs_debug("txt:%s", txt); - - cJSON_ArrayForEach(entry, content_hosting_config) { - if (!strcmp(entry->string, "entryPointPath")){ - if (!uri_relative_check(entry->valuestring)) { - char *err = NULL; - asprintf(&err,"Entry Point Path does not match the regular expression."); - ogs_error("Entry Point Path does not match the regular expression."); - ogs_assert(true == ogs_sbi_server_send_error(stream, - 404, &message, - "Entry Point Path does not match the regular expression.", - err)); - cJSON_Delete(content_hosting_config); - break; - } - } - } + response = e->h.sbi.response; + ogs_assert(response); + rv = ogs_sbi_parse_header(&message, &response->h); + if (rv != OGS_OK) { + ogs_error("ogs_sbi_parse_header() failed"); + ogs_sbi_message_free(&message); + ogs_sbi_response_free(response); + break; + } + message.res_status = response->status; - if (msaf_provisioning_session->contentHostingConfiguration) { - OpenAPI_content_hosting_configuration_free(msaf_provisioning_session->contentHostingConfiguration); - msaf_provisioning_session->contentHostingConfiguration = NULL; + SWITCH(message.h.service.name) + + CASE("3gpp-m3") + SWITCH(message.h.resource.component[0]) + CASE("content-hosting-configurations") - } + msaf_application_server_state_node_t *as_state; + as_state = e->application_server_state; + ogs_assert(as_state); - if (msaf_provisioning_session->serviceAccessInformation) { - OpenAPI_service_access_information_resource_free(msaf_provisioning_session->serviceAccessInformation); - msaf_provisioning_session->serviceAccessInformation = NULL; - } + if (message.h.resource.component[1] && message.h.resource.component[2]) { - rv = msaf_distribution_create(content_hosting_config, msaf_provisioning_session); - - if (rv) { - ogs_debug("Content Hosting Configuration created successfully"); - msaf_application_server_state_set_on_post(msaf_provisioning_session); - chc = msaf_get_content_hosting_configuration_by_provisioning_session_id(message.h.resource.component[1]); - if (chc != NULL) { - ogs_sbi_response_t *response; - char *text; - response = ogs_sbi_response_new(); - text = cJSON_Print(chc); - response->http.content_length = strlen(text); - response->http.content = text; - response->status = 201; - ogs_sbi_header_set(response->http.headers, "Content-Type", "application/json"); - ogs_sbi_header_set(response->http.headers, "Location", request->h.uri); - ogs_assert(response); - ogs_assert(true == ogs_sbi_server_send_response(stream, response)); - cJSON_Delete(chc); - cJSON_Delete(content_hosting_config); - } else { - char *err = NULL; - ogs_error("Unable to retrieve the Content Hosting Configuration"); - asprintf(&err,"Unable to retrieve the Content Hosting Configuration"); - ogs_error("Unable to retrieve the Content Hosting Configuration"); - ogs_assert(true == ogs_sbi_server_send_error(stream, - 404, &message, - "Unable to retrieve the Content Hosting Configuration", - err)); + if (!strcmp(message.h.resource.component[2],"purge")) { + SWITCH(message.h.method) + CASE(OGS_SBI_HTTP_METHOD_POST) + purge_resource_id_node_t *purge_node = e->purge_node; + + if (response->status == 204 || response->status == 200) { + + purge_resource_id_node_t *content_hosting_cache, *next = NULL; + + if (response->status == 200) { + //parse the int in response body + //Add the integer to purge_node->m1_purge_info->purged_entries_total; + { + ogs_hash_index_t *hi; + for (hi = ogs_hash_first(request->http.headers); + hi; hi = ogs_hash_next(hi)) { + if (!ogs_strcasecmp(ogs_hash_this_key(hi), OGS_SBI_CONTENT_TYPE)) { + if (ogs_strcasecmp(ogs_hash_this_val(hi), "application/json")) { + char *err = NULL; + char *type = NULL; + type = (char *)ogs_hash_this_val(hi); + ogs_error("Unsupported Media Type: received type: %s, should have been application/x-www-form-urlencoded", type); + asprintf(&err, "Unsupported Media Type: received type: %s, should have been application/x-www-form-urlencoded", type); + + ogs_assert(true == nf_server_send_error(stream, 415, 2, &message, "Provisioning session does not exist.", err, NULL, m3_contenthostingprovisioning_api, app_meta)); + ogs_sbi_message_free(&message); + return; } - } else { - char *err = NULL; - ogs_error("Failed to populate Content Hosting Configuration"); - asprintf(&err,"Creation of the Content Hosting Configuration failed."); - ogs_error("Creation of the Content Hosting Configuration failed."); - ogs_assert(true == ogs_sbi_server_send_error(stream, - 404, &message, - "Creation of the Content Hosting Configuration failed.", - err)); - } - - } else { - char *err = NULL; - asprintf(&err,"Provisioning session does not exists."); - ogs_error("Provisioning session does not exists."); - ogs_assert(true == ogs_sbi_server_send_error(stream, - 404, &message, - "Provisioning session does not exists.", - err)); - } - } - } else { + int purged_items_from_as = 0; cJSON *entry; - cJSON *prov_sess = cJSON_Parse(request->http.content); - cJSON *provisioning_session; - char *provisioning_session_type, *external_app_id, *asp_id = NULL; - msaf_provisioning_session_t *msaf_provisioning_session; - - cJSON_ArrayForEach(entry, prov_sess) { - if (!strcmp(entry->string, "provisioningSessionType")){ - provisioning_session_type = entry->valuestring; - } - if (!strcmp(entry->string, "aspId")){ - asp_id = entry->valuestring; - } - if (!strcmp(entry->string, "externalApplicationId")){ - external_app_id = entry->valuestring; - } + cJSON *number_of_cache_entries = cJSON_Parse(response->http.content); + cJSON_ArrayForEach(entry, number_of_cache_entries) { + ogs_debug("Purged entries return %d\n", entry->valueint); + purged_items_from_as = entry->valueint; } - msaf_provisioning_session = msaf_provisioning_session_create(provisioning_session_type, asp_id, external_app_id); - provisioning_session = msaf_provisioning_session_get_json(msaf_provisioning_session->provisioningSessionId); - if (provisioning_session != NULL) { - ogs_sbi_response_t *response; - char *text; - char *location; - response = ogs_sbi_response_new(); - text = cJSON_Print(provisioning_session); - response->http.content_length = strlen(text); - response->http.content = text; - response->status = 201; - ogs_sbi_header_set(response->http.headers, "Content-Type", "application/json"); + purge_node->m1_purge_info->purged_entries_total += purged_items_from_as; - if (request->h.uri[strlen(request->h.uri)-1] != '/') { - location = ogs_msprintf("%s/%s", request->h.uri,msaf_provisioning_session->provisioningSessionId); - } else { - location = ogs_msprintf("%s%s", request->h.uri,msaf_provisioning_session->provisioningSessionId); - } - ogs_sbi_header_set(response->http.headers, "Location", location); - ogs_assert(response); - ogs_assert(true == ogs_sbi_server_send_response(stream, response)); - ogs_free(location); - cJSON_Delete(provisioning_session); - cJSON_Delete(prov_sess); - } else { - char *err = NULL; - asprintf(&err,"Creation of the Provisioning session failed."); - ogs_error("Creation of the Provisioning session failed."); - ogs_assert(true == ogs_sbi_server_send_error(stream, - 404, &message, - "Creation of the Provisioning session failed.", - err)); - } } - break; - - CASE(OGS_SBI_HTTP_METHOD_GET) - if (message.h.resource.component[1]) { - msaf_provisioning_session_t *msaf_provisioning_session = NULL; - cJSON *provisioning_session = NULL; - - msaf_provisioning_session = msaf_provisioning_session_find_by_provisioningSessionId(message.h.resource.component[1]); - provisioning_session = msaf_provisioning_session_get_json(message.h.resource.component[1]); - - if (provisioning_session && msaf_provisioning_session && !msaf_provisioning_session->marked_for_deletion) { + ogs_list_for_each_safe(&as_state->purge_content_hosting_cache, next, content_hosting_cache){ + if (purge_node->purge_regex) { + if(!strcmp(content_hosting_cache->provisioning_session_id, purge_node->provisioning_session_id) && !strcmp(content_hosting_cache->purge_regex, purge_node->purge_regex)) + break; + } else if(!strcmp(content_hosting_cache->provisioning_session_id, purge_node->provisioning_session_id)) { + break; + } + } + if(content_hosting_cache){ + ogs_list_remove(&as_state->purge_content_hosting_cache, content_hosting_cache); + ogs_debug("M1 List Purge refs: %d, Event Purge node refs: %d ", content_hosting_cache->m1_purge_info->refs, purge_node->m1_purge_info->refs); + + purge_node->m1_purge_info->refs--; + ogs_debug(" After decrement, M1 List Purge refs: %d, Event Purge node refs: %d ", content_hosting_cache->m1_purge_info->refs, purge_node->m1_purge_info->refs); + if(!purge_node->m1_purge_info->refs){ + // send M1 response with total from purge_node->m1_purge_info->purged_entries_total + // ogs_free(purge_node->m1_purge_info); ogs_sbi_response_t *response; - char *text; + cJSON *purged_entries_total_json = cJSON_CreateNumber(purge_node->m1_purge_info->purged_entries_total); + char *purged_entries_total = cJSON_Print(purged_entries_total_json); response = ogs_sbi_response_new(); - text = cJSON_Print(provisioning_session); - response->http.content_length = strlen(text); - response->http.content = text; + response->http.content_length = strlen(purged_entries_total); + response->http.content = purged_entries_total; response->status = 200; ogs_sbi_header_set(response->http.headers, "Content-Type", "application/json"); - ogs_assert(response); - ogs_assert(true == ogs_sbi_server_send_response(stream, response)); - cJSON_Delete(provisioning_session); - - } else { - char *err = NULL; - asprintf(&err,"Provisioning session is not available."); - ogs_error("Provisioning session is not available."); - ogs_assert(true == ogs_sbi_server_send_error(stream, - 404, &message, - "Provisioning session is not available.", - err)); + ogs_assert(true == ogs_sbi_server_send_response(purge_node->m1_purge_info->m1_stream, response)); + + if(content_hosting_cache->m1_purge_info) ogs_free(content_hosting_cache->m1_purge_info); + if (content_hosting_cache->provisioning_session_id) ogs_free(content_hosting_cache->provisioning_session_id); + if(content_hosting_cache->purge_regex) ogs_free(content_hosting_cache->purge_regex); + ogs_free(content_hosting_cache); } - } - break; - - CASE(OGS_SBI_HTTP_METHOD_PUT) - if (message.h.resource.component[1] && message.h.resource.component[2]) { - msaf_provisioning_session_t *msaf_provisioning_session; - if (!strcmp(message.h.resource.component[2],"content-hosting-configuration")) { - msaf_provisioning_session = msaf_provisioning_session_find_by_provisioningSessionId(message.h.resource.component[1]); - if (msaf_provisioning_session) { - // process the POST body - cJSON *entry; - int rv; - cJSON *content_hosting_config = cJSON_Parse(request->http.content); - char *txt = cJSON_Print(content_hosting_config); - ogs_debug("txt:%s", txt); - - cJSON_ArrayForEach(entry, content_hosting_config) { - if (!strcmp(entry->string, "entryPointPath")){ - if (!uri_relative_check(entry->valuestring)) { - char *err = NULL; - asprintf(&err,"Entry Point Path does not match the regular expression."); - ogs_error("Entry Point Path does not match the regular expression."); - ogs_assert(true == ogs_sbi_server_send_error(stream, - 404, &message, - "Entry Point Path does not match the regular expression.", - err)); - cJSON_Delete(content_hosting_config); - break; - } + msaf_application_server_state_log(&as_state->purge_content_hosting_cache, "Purge Content Hosting Cache list"); - } - } - if (msaf_provisioning_session->contentHostingConfiguration) { - OpenAPI_content_hosting_configuration_free(msaf_provisioning_session->contentHostingConfiguration); - msaf_provisioning_session->contentHostingConfiguration = NULL; + } + } - } - if (msaf_provisioning_session->serviceAccessInformation) { - OpenAPI_service_access_information_resource_free(msaf_provisioning_session->serviceAccessInformation); - msaf_provisioning_session->serviceAccessInformation = NULL; - } + if((response->status == 404) || (response->status == 413) || (response->status == 414) || (response->status == 415) || (response->status == 422) || (response->status == 500) || (response->status == 503)) { + char *error; + purge_resource_id_node_t *content_hosting_cache, *next = NULL; + cJSON *purge_cache_err = NULL; + if(response->http.content){ + purge_cache_err = cJSON_Parse(response->http.content); + char *txt = cJSON_Print(purge_cache_err); + ogs_debug("txt:%s", txt); + } - rv = msaf_distribution_create(content_hosting_config, msaf_provisioning_session); + if(response->status == 404) { + + ogs_error("Error message from the Application Server [%s] with response code [%d]: Cache not found\n", as_state->application_server->canonicalHostname, response->status); + } else if(response->status == 413) { + ogs_error("Error message from the Application Server [%s] with response code [%d]: Pay load too large\n", as_state->application_server->canonicalHostname, response->status); + } else if(response->status == 414) { + ogs_error("Error message from the Application Server [%s] with response code [%d]: URI too long\n", as_state->application_server->canonicalHostname, response->status); + } else if(response->status == 415) { + ogs_error("Error message from the Application Server [%s] with response code [%d]: Unsupported media type\n", as_state->application_server->canonicalHostname, response->status); + } else if(response->status == 422) { + ogs_error("Error message from the Application Server [%s] with response code [%d]: Unprocessable Entity\n", as_state->application_server->canonicalHostname, response->status); + } else if(response->status == 500) { + ogs_error("Error message from the Application Server [%s] with response code [%d]: Internal server error\n", as_state->application_server->canonicalHostname, response->status); + } else if(response->status == 503) { + ogs_error("Error message from the Application Server [%s] with response code [%d]: Service Unavailable\n", as_state->application_server->canonicalHostname, response->status); + } else { - if (rv) { - msaf_application_server_state_update(msaf_provisioning_session); + ogs_error("Application Server [%s] sent unrecognised response code [%d]", as_state->application_server->canonicalHostname, response->status); + } - ogs_debug("Content Hosting Configuration updated successfully"); + if (purge_node->purge_regex) { + error = ogs_msprintf("Application Server possibly encountered problem with regex %s", purge_node->purge_regex); + } else { + error = ogs_msprintf("Application Server unable to process the contained instructions"); + } - ogs_sbi_response_t *response; - response = ogs_sbi_response_new(); - response->status = 204; - ogs_sbi_header_set(response->http.headers, "Content-Type", "application/json"); - ogs_sbi_header_set(response->http.headers, "Location", request->h.uri); - ogs_assert(response); - ogs_assert(true == ogs_sbi_server_send_response(stream, response)); - cJSON_Delete(content_hosting_config); - } else { - char *err = NULL; - ogs_error("Failed to update Content Hosting Configuration"); - asprintf(&err,"Update to Content Hosting Configuration failed."); - ogs_error("Update of the Content Hosting Configuration failed."); - ogs_assert(true == ogs_sbi_server_send_error(stream, - 404, &message, - "Creation of the Content Hosting Configuration failed.", - err)); + ogs_assert(true == nf_server_send_error( purge_node->m1_purge_info->m1_stream, + response->status, 3, &purge_node->m1_purge_info->m1_message, "Problem occured during cache purge", error, purge_cache_err, m1_contenthostingprovisioning_api, app_meta)); - } + ogs_list_for_each_safe(&as_state->purge_content_hosting_cache, next, content_hosting_cache){ + if (purge_node->purge_regex) { + if(!strcmp(content_hosting_cache->provisioning_session_id, purge_node->provisioning_session_id) && !strcmp(content_hosting_cache->purge_regex, purge_node->purge_regex)) { - } else { - char *err = NULL; - asprintf(&err,"Provisioning session does not exists."); - ogs_error("Provisioning session does not exists."); - ogs_assert(true == ogs_sbi_server_send_error(stream, - 404, &message, - "Provisioning session does not exists.", - err)); + ogs_list_remove(&as_state->purge_content_hosting_cache, content_hosting_cache); + ogs_debug("M1 List Purge refs: %d, Event Purge node refs: %d ", content_hosting_cache->m1_purge_info->refs, purge_node->m1_purge_info->refs); + if(content_hosting_cache->m1_purge_info) ogs_free(content_hosting_cache->m1_purge_info); + if (content_hosting_cache->provisioning_session_id) ogs_free(content_hosting_cache->provisioning_session_id); + if(content_hosting_cache->purge_regex) ogs_free(content_hosting_cache->purge_regex); + ogs_free(content_hosting_cache); } + } else if(!strcmp(content_hosting_cache->provisioning_session_id, purge_node->provisioning_session_id)) { - } - } - break; - - CASE(OGS_SBI_HTTP_METHOD_DELETE) - if (message.h.resource.component[1]) { - ogs_sbi_response_t *response; - msaf_provisioning_session_t *provisioning_session = NULL; - provisioning_session = msaf_provisioning_session_find_by_provisioningSessionId(message.h.resource.component[1]); - if (!provisioning_session || provisioning_session->marked_for_deletion){ - char *err = NULL; - ogs_error("Provisioning session either not found or already marked for deletion."); - ogs_assert(true == ogs_sbi_server_send_error(stream, - 404, &message, - "Unable to find the Provisioning session or it is marked for deletion already.", - err)); - } else { - provisioning_session->marked_for_deletion = 1; - - response = ogs_sbi_response_new(); - ogs_assert(response); - response->status = 202; - ogs_assert(true == ogs_sbi_server_send_response(stream, response)); - - msaf_delete_content_hosting_configuration(message.h.resource.component[1]); - msaf_delete_certificate(message.h.resource.component[1]); - msaf_context_provisioning_session_free(provisioning_session); - msaf_provisioning_session_hash_remove(message.h.resource.component[1]); + ogs_list_remove(&as_state->purge_content_hosting_cache, content_hosting_cache); + ogs_debug("M1 List Purge refs: %d, Event Purge node refs: %d ", content_hosting_cache->m1_purge_info->refs, purge_node->m1_purge_info->refs); + if(content_hosting_cache->m1_purge_info) ogs_free(content_hosting_cache->m1_purge_info); + if (content_hosting_cache->provisioning_session_id) ogs_free(content_hosting_cache->provisioning_session_id); + if(content_hosting_cache->purge_regex) ogs_free(content_hosting_cache->purge_regex); + ogs_free(content_hosting_cache); } } - break; - - DEFAULT - ogs_error("Invalid HTTP method [%s]", message.h.method); - ogs_assert(true == ogs_sbi_server_send_error(stream, - OGS_SBI_HTTP_STATUS_FORBIDDEN, - &message, "Invalid HTTP method", - message.h.method)); - END - break; + ogs_free(error); + cJSON_Delete(purge_cache_err); - DEFAULT - ogs_error("Invalid resource name [%s]", - message.h.resource.component[0]); - ogs_assert(true == - ogs_sbi_server_send_error(stream, - OGS_SBI_HTTP_STATUS_BAD_REQUEST, &message, - "Invalid resource name", - message.h.resource.component[0])); - END - ogs_sbi_message_free(&message); - break; + } - CASE("3gpp-m5") - if (strcmp(message.h.api.version, "v2") != 0) { - ogs_error("Not supported version [%s]", message.h.api.version); - ogs_assert(true == - ogs_sbi_server_send_error( - stream, OGS_SBI_HTTP_STATUS_BAD_REQUEST, - &message, "Not supported version", NULL)); - ogs_sbi_message_free(&message); - break; - } - SWITCH(message.h.resource.component[0]) - CASE("service-access-information") - SWITCH(message.h.method) - CASE(OGS_SBI_HTTP_METHOD_GET) - cJSON *service_access_information; - - msaf_provisioning_session_t *msaf_provisioning_session = NULL; - msaf_provisioning_session = msaf_provisioning_session_find_by_provisioningSessionId(message.h.resource.component[1]); - - if (!msaf_provisioning_session) { - char *err = NULL; - asprintf(&err,"Provisioning Session [%s] not found.", message.h.resource.component[1]); - ogs_error("Client requested invalid Provisioning Session [%s]", message.h.resource.component[1]); - ogs_assert(true == ogs_sbi_server_send_error(stream, - 404, &message, - "Provisioning Session not found", - err)); - ogs_sbi_message_free(&message); - break; - } - - if (msaf_provisioning_session->serviceAccessInformation) { - service_access_information = msaf_context_retrieve_service_access_information(message.h.resource.component[1]); - if (service_access_information != NULL) { - ogs_sbi_response_t *response; - char *text; - response = ogs_sbi_response_new(); - text = cJSON_Print(service_access_information); - response->http.content_length = strlen(text); - response->http.content = text; - response->status = 200; - ogs_sbi_header_set(response->http.headers, "Content-Type", "application/json"); - ogs_assert(response); - ogs_assert(true == ogs_sbi_server_send_response(stream, response)); - cJSON_Delete(service_access_information); - } else { - char *err = NULL; - asprintf(&err,"Service Access Information for the Provisioning Session [%s] not found.", message.h.resource.component[1]); - ogs_error("Client requested invalid Service Access Information for the Provisioning Session [%s]", message.h.resource.component[1]); - ogs_assert(true == ogs_sbi_server_send_error(stream, - 404, &message, - "Service Access Information not found", - err)); - } - } else { - char *err = NULL; - asprintf(&err,"Provisioning Session [%s] has no Service Access Information associated with it.", message.h.resource.component[1]); - ogs_error("Provisioning Session [%s] has no Service Access Information associated with it", message.h.resource.component[1]); - ogs_assert(true == ogs_sbi_server_send_error(stream, - 404, &message, - "Service Access Information not found", - err)); - } - break; - DEFAULT - ogs_error("Invalid HTTP method [%s]", message.h.method); - ogs_assert(true == ogs_sbi_server_send_error(stream, - OGS_SBI_HTTP_STATUS_FORBIDDEN, - &message, "Invalid HTTP method", - message.h.method)); + next_action_for_application_server(as_state); + break; END break; - DEFAULT - ogs_error("Invalid resource name [%s]", - message.h.resource.component[0]); - ogs_assert(true == - ogs_sbi_server_send_error(stream, - OGS_SBI_HTTP_STATUS_BAD_REQUEST, &message, - "Invalid resource name", - message.h.resource.component[0])); - END - ogs_sbi_message_free(&message); - break; - DEFAULT - ogs_error("Invalid API name [%s]", message.h.service.name); - ogs_assert(true == - ogs_sbi_server_send_error(stream, - OGS_SBI_HTTP_STATUS_BAD_REQUEST, &message, - "Invalid API name", message.h.service.name)); - ogs_sbi_message_free(&message); - END - break; - - case OGS_EVENT_SBI_CLIENT: - ogs_assert(e); - - response = e->h.sbi.response; - ogs_assert(response); - rv = ogs_sbi_parse_header(&message, &response->h); - if (rv != OGS_OK) { - ogs_error("ogs_sbi_parse_header() failed"); - ogs_sbi_message_free(&message); - ogs_sbi_response_free(response); - break; - } - { - ogs_hash_index_t *hi; - for (hi = ogs_hash_first(response->http.headers); - hi; hi = ogs_hash_next(hi)) { - if (!ogs_strcasecmp(ogs_hash_this_key(hi), OGS_SBI_CONTENT_TYPE)) { - message.http.content_type = ogs_hash_this_val(hi); - } else if (!ogs_strcasecmp(ogs_hash_this_key(hi), OGS_SBI_LOCATION)) { - message.http.location = ogs_hash_this_val(hi); - } - } - } - message.res_status = response->status; + } + } else if (message.h.resource.component[1]) { - SWITCH(message.h.service.name) - CASE("3gpp-m3") - SWITCH(message.h.resource.component[0]) - CASE("content-hosting-configurations") + SWITCH(message.h.method) + CASE(OGS_SBI_HTTP_METHOD_POST) - msaf_application_server_state_node_t *as_state; - as_state = e->application_server_state; - ogs_assert(as_state); - - if (message.h.resource.component[1] && message.h.resource.component[2]) { - if (!strcmp(message.h.resource.component[2],"purge")) { - - SWITCH(message.h.method) - CASE(OGS_SBI_HTTP_METHOD_POST) - - if (response->status == 204) { - - int elements_in_purge_list = 0; - purge_resource_id_node_t *content_hosting_cache, *next = NULL; - - ogs_list_for_each_safe(&as_state->purge_content_hosting_cache, next, content_hosting_cache){ - if (!strcmp(content_hosting_cache->state, message.h.resource.component[1])) - break; - } + if (response->status == 201) { - if (content_hosting_cache) { + ogs_debug("[%s] Method [%s] with Response [%d] recieved for Content Hosting Configuration [%s]", message.h.resource.component[0], message.h.method, response->status, message.h.resource.component[1]); - msaf_application_server_state_log(&as_state->purge_content_hosting_cache, "Purge Content Hosting Cache list"); - - if (content_hosting_cache->purge_regex) { - ogs_debug("Application Server [%s]: Purged cache pertainining to resource [%s] matching %s", as_state->application_server->canonicalHostname, content_hosting_cache->state, content_hosting_cache->purge_regex); - - } else { - ogs_debug("Application Server [%s]: Purged cache pertainining to resource [%s]", as_state->application_server->canonicalHostname, content_hosting_cache->state); - - } - if (content_hosting_cache->state) ogs_free(content_hosting_cache->state); - if (content_hosting_cache->purge_regex) ogs_free(content_hosting_cache->purge_regex); - ogs_list_remove(&as_state->purge_content_hosting_cache, content_hosting_cache); - ogs_free(content_hosting_cache); - msaf_application_server_state_log(&as_state->purge_content_hosting_cache, "Purge Content Hosting Cache list"); - - } - - ogs_list_for_each(&as_state->purge_content_hosting_cache, content_hosting_cache) { - if (!strcmp(content_hosting_cache->state, message.h.resource.component[1])) - elements_in_purge_list++; - } - - if (!elements_in_purge_list) { - ogs_sbi_response_t *response; - response = ogs_sbi_response_new(); - response->status = 200; - ogs_assert(response); - ogs_assert(true == ogs_sbi_server_send_response(as_state->stream, response)); - } - - } else if (response->status == 200) { - - int elements_in_purge_list = 0; - purge_resource_id_node_t *content_hosting_cache, *next = NULL; + resource_id_node_t *content_hosting_configuration; + ogs_list_for_each(&as_state->upload_content_hosting_configurations,content_hosting_configuration) { + if(!strcmp(content_hosting_configuration->state, message.h.resource.component[1])) + break; + } + if(content_hosting_configuration) { - ogs_list_for_each_safe(&as_state->purge_content_hosting_cache, next, content_hosting_cache){ + ogs_debug("Removing %s from upload_content_hosting_configurations", content_hosting_configuration->state); + ogs_list_remove(&as_state->upload_content_hosting_configurations, content_hosting_configuration); + ogs_debug("Adding %s to current_content_hosting_configurations",content_hosting_configuration->state); + ogs_list_add(as_state->current_content_hosting_configurations, content_hosting_configuration); + } - if (!strcmp(content_hosting_cache->state, message.h.resource.component[1])) - break; - } + } + if(response->status == 405){ + ogs_error("Content Hosting Configuration resource already exist at the specified path\n"); + } + if(response->status == 413){ + ogs_error("Payload too large\n"); + } + if(response->status == 414){ + ogs_error("URI too long\n"); + } + if(response->status == 415){ + ogs_error("Unsupported media type\n"); + } + if(response->status == 500){ + ogs_error("Internal server error\n"); + } + if(response->status == 503){ + ogs_error("Service unavailable\n"); + } + next_action_for_application_server(as_state); + break; + CASE(OGS_SBI_HTTP_METHOD_PUT) + if(response->status == 200 || response->status == 204) { - if (content_hosting_cache) { + ogs_debug("[%s] Method [%s] with Response [%d] recieved for Content Hosting Configuration [%s]", message.h.resource.component[0], message.h.method, response->status, message.h.resource.component[1]); + resource_id_node_t *content_hosting_configuration; + ogs_list_for_each(&as_state->upload_content_hosting_configurations,content_hosting_configuration){ + if(!strcmp(content_hosting_configuration->state, message.h.resource.component[1])) + break; + } + if(content_hosting_configuration) { - msaf_application_server_state_log(&as_state->purge_content_hosting_cache, "Purge Content Hosting Cache list"); + ogs_debug("Removing %s from upload_content_hosting_configurations", content_hosting_configuration->state); + ogs_free(content_hosting_configuration->state); + ogs_list_remove(&as_state->upload_content_hosting_configurations, content_hosting_configuration); + ogs_free(content_hosting_configuration); + } - if (content_hosting_cache->purge_regex) { - ogs_debug("Application Server [%s] encountered a problem when purging its cache pertainining to resource [%s] for %s", as_state->application_server->canonicalHostname, content_hosting_cache->state, content_hosting_cache->purge_regex); + } + if(response->status == 404){ + ogs_error("Not Found\n"); + } + if(response->status == 413){ + ogs_error("Payload too large\n"); + } + if(response->status == 414){ + ogs_error("URI too long\n"); + } + if(response->status == 415){ + ogs_error("Unsupported Media Type\n"); + } + if(response->status == 500){ + ogs_error("Internal Server Error\n"); + } + if(response->status == 503){ + ogs_error("Service Unavailable\n"); + } + next_action_for_application_server(as_state); + break; + CASE(OGS_SBI_HTTP_METHOD_DELETE) + if(response->status == 204) { - } else { - ogs_debug("Application Server [%s]: Failed to purge cache pertainining to resource [%s]", as_state->application_server->canonicalHostname, content_hosting_cache->state); + ogs_debug("[%s] Method [%s] with Response [%d] recieved for Content Hosting Configuration [%s]", message.h.resource.component[0], message.h.method, response->status,message.h.resource.component[1]); - } - if (content_hosting_cache->state) ogs_free(content_hosting_cache->state); - if (content_hosting_cache->purge_regex) ogs_free(content_hosting_cache->purge_regex); - ogs_list_remove(&as_state->purge_content_hosting_cache, content_hosting_cache); - ogs_free(content_hosting_cache); - msaf_application_server_state_log(&as_state->purge_content_hosting_cache, "Purge Content Hosting Cache list"); - } + resource_id_node_t *content_hosting_configuration, *next = NULL; + resource_id_node_t *delete_content_hosting_configuration, *node = NULL; - ogs_list_for_each(&as_state->purge_content_hosting_cache, content_hosting_cache) { - if (!strcmp(content_hosting_cache->state, message.h.resource.component[1])) - elements_in_purge_list++; - } + if(as_state->current_content_hosting_configurations) { - if (!elements_in_purge_list) { - ogs_sbi_response_t *response; - response = ogs_sbi_response_new(); - response->status = 200; - ogs_assert(response); - ogs_assert(true == ogs_sbi_server_send_response(as_state->stream, response)); - } + ogs_list_for_each_safe(as_state->current_content_hosting_configurations, next, content_hosting_configuration){ - } else if (response->status == 404) { - report_purging_error(as_state, &message, response->status, "Cache not found"); - } else if (response->status == 413) { - report_purging_error(as_state, &message, response->status, "Payload too large"); - } else if (response->status == 414){ - report_purging_error(as_state, &message, response->status, "URI too long"); - } else if (response->status == 415){ - report_purging_error(as_state, &message, response->status, "Unsupported media type"); - } else if (response->status == 422) { - report_purging_error(as_state, &message, response->status, "Unprocessable Entity"); - } else if (response->status == 500) { - report_purging_error(as_state, &message, response->status, "Internal server error"); - } else if (response->status == 503) { - report_purging_error(as_state, &message, response->status, "Service unavailable"); - } - next_action_for_application_server(as_state); + if(!strcmp(content_hosting_configuration->state, message.h.resource.component[1])) break; - END - break; + } } - } else if (message.h.resource.component[1]) { - SWITCH(message.h.method) - CASE(OGS_SBI_HTTP_METHOD_POST) - if (response->status == 201) { + if(content_hosting_configuration) { - ogs_debug("[%s] Method [%s] with Response [%d] recieved for Content Hosting Configuration [%s]", message.h.resource.component[0], message.h.method, response->status, message.h.resource.component[1]); + msaf_application_server_state_log(as_state->current_content_hosting_configurations, "Current Content Hosting Configurations"); - resource_id_node_t *content_hosting_configuration; - ogs_list_for_each(&as_state->upload_content_hosting_configurations,content_hosting_configuration) { - if (!strcmp(content_hosting_configuration->state, message.h.resource.component[1])) - break; - } - if (content_hosting_configuration) { + ogs_debug("Removing %s from current_content_hosting_configurations", content_hosting_configuration->state); + ogs_free(content_hosting_configuration->state); + ogs_list_remove(as_state->current_content_hosting_configurations, content_hosting_configuration); + ogs_free(content_hosting_configuration); + msaf_application_server_state_log(as_state->current_content_hosting_configurations, "Current Content Hosting Configurations"); + } - ogs_debug("Removing %s from upload_content_hosting_configurations", content_hosting_configuration->state); - ogs_list_remove(&as_state->upload_content_hosting_configurations, content_hosting_configuration); - ogs_debug("Adding %s to current_content_hosting_configurations",content_hosting_configuration->state); - ogs_list_add(as_state->current_content_hosting_configurations, content_hosting_configuration); - } + ogs_list_for_each_safe(&as_state->delete_content_hosting_configurations, node, delete_content_hosting_configuration) { - } - if (response->status == 405){ - ogs_error("Content Hosting Configuration resource already exists at the specified path\n"); - } - if (response->status == 413){ - ogs_error("Payload too large\n"); - } - if (response->status == 414){ - ogs_error("URI too long\n"); - } - if (response->status == 415){ - ogs_error("Unsupported media type\n"); - } - if (response->status == 500){ - ogs_error("Internal server error\n"); - } - if (response->status == 503){ - ogs_error("Service unavailable\n"); - } - next_action_for_application_server(as_state); - break; - CASE(OGS_SBI_HTTP_METHOD_PUT) - if (response->status == 200 || response->status == 204) { - - ogs_debug("[%s] Method [%s] with Response [%d] recieved for Content Hosting Configuration [%s]", message.h.resource.component[0], message.h.method, response->status, message.h.resource.component[1]); - resource_id_node_t *content_hosting_configuration; - ogs_list_for_each(&as_state->upload_content_hosting_configurations,content_hosting_configuration){ - if (!strcmp(content_hosting_configuration->state, message.h.resource.component[1])) - break; - } - if (content_hosting_configuration) { + if (!strcmp(delete_content_hosting_configuration->state, message.h.resource.component[1])) { - ogs_debug("Removing %s from upload_content_hosting_configurations", content_hosting_configuration->state); - ogs_free(content_hosting_configuration->state); - ogs_list_remove(&as_state->upload_content_hosting_configurations, content_hosting_configuration); - ogs_free(content_hosting_configuration); - } + msaf_application_server_state_log(&as_state->delete_content_hosting_configurations, "Delete Content Hosting Configurations"); - } - if (response->status == 404){ - ogs_error("Not Found\n"); - } - if (response->status == 413){ - ogs_error("Payload too large\n"); - } - if (response->status == 414){ - ogs_error("URI too long\n"); - } - if (response->status == 415){ - ogs_error("Unsupported Media Type\n"); - } - if (response->status == 500){ - ogs_error("Internal Server Error\n"); - } - if (response->status == 503){ - ogs_error("Service Unavailable\n"); - } - next_action_for_application_server(as_state); - break; - CASE(OGS_SBI_HTTP_METHOD_DELETE) - if (response->status == 204) { + ogs_debug("Destroying Content Hosting Configuration: %s", delete_content_hosting_configuration->state); + ogs_free(delete_content_hosting_configuration->state); + ogs_list_remove(&as_state->delete_content_hosting_configurations, delete_content_hosting_configuration); + ogs_free(delete_content_hosting_configuration); - ogs_debug("[%s] Method [%s] with Response [%d] recieved for Content Hosting Configuration [%s]", message.h.resource.component[0], message.h.method, response->status,message.h.resource.component[1]); + msaf_application_server_state_log(&as_state->delete_content_hosting_configurations, "Delete Content Hosting Configurations"); + } + } - resource_id_node_t *content_hosting_configuration, *next = NULL; - resource_id_node_t *delete_content_hosting_configuration, *node = NULL; + } + if(response->status == 404){ + ogs_error("Not Found\n"); + } + if(response->status == 413){ + ogs_error("Payload too large\n"); + } + if(response->status == 414){ + ogs_error("URI too long\n"); + } + if(response->status == 415){ + ogs_error("Unsupported Media Type\n"); + } + if(response->status == 500){ + ogs_error("Internal Server Error\n"); + } + if(response->status == 503){ + ogs_error("Service Unavailable\n"); + } + next_action_for_application_server(as_state); + break; + DEFAULT + ogs_error("Unknown M3 Content Hosting Configuration operation [%s]", message.h.resource.component[1]); + ogs_assert(true == nf_server_send_error(stream, OGS_SBI_HTTP_STATUS_BAD_REQUEST, 0, &message, "Unknown M3 Content Hosting Configuration operation", message.h.resource.component[1], NULL, NULL, app_meta)); + break; + END + break; + } else { + cJSON *entry; + cJSON *chc_array = cJSON_Parse(response->http.content); + resource_id_node_t *current_chc; + SWITCH(message.h.method) + CASE(OGS_SBI_HTTP_METHOD_GET) + + if(response->status == 200) { + + ogs_debug("[%s] Method [%s] with Response [%d] for Content Hosting Configuration operation [%s]", + message.h.resource.component[0], message.h.method, response->status, message.h.resource.component[1]); + + if (as_state->current_content_hosting_configurations == NULL) { + as_state->current_content_hosting_configurations = ogs_calloc(1,sizeof(*as_state->current_content_hosting_configurations)); + ogs_assert(as_state->current_content_hosting_configurations); + ogs_list_init(as_state->current_content_hosting_configurations); + + } else { + resource_id_node_t *next, *node; + ogs_list_for_each_safe(as_state->current_content_hosting_configurations, next, node) { + ogs_free(node->state); + ogs_list_remove(as_state->current_content_hosting_configurations, node); + ogs_free(node); + } + } + cJSON_ArrayForEach(entry, chc_array) { + char *id = strrchr(entry->valuestring, '/'); + if (id == NULL) { + id = entry->valuestring; + } else { + id++; + } + current_chc = ogs_calloc(1, sizeof(*current_chc)); + current_chc->state = ogs_strdup(id); + ogs_debug("Adding [%s] to the current Content Hosting Configuration list",current_chc->state); + ogs_list_add(as_state->current_content_hosting_configurations, current_chc); + } - if (as_state->current_content_hosting_configurations) { + cJSON_Delete(chc_array); + } + if (response->status == 500){ + ogs_error("Received Internal Server error\n"); + } + if (response->status == 503) { + ogs_error("Service Unavailable\n"); + } + next_action_for_application_server(as_state); + break; + DEFAULT + char *err; + ogs_error("Unknown M3 Content Hosting Configuration operation [%s] with method [%s]", message.h.resource.component[1], message.h.method); + asprintf(&err, "Unknown M3 Content Hosting Configuration operation [%s] with method [%s]", message.h.resource.component[1], message.h.method); + ogs_assert(true == nf_server_send_error(stream, OGS_SBI_HTTP_STATUS_BAD_REQUEST, 0, &message, "Unknown M3 Content Hosting Configuration operation", err, NULL, NULL, app_meta)); - ogs_list_for_each_safe(as_state->current_content_hosting_configurations, next, content_hosting_configuration){ + break; + END + break; + } + next_action_for_application_server(as_state); - if (!strcmp(content_hosting_configuration->state, message.h.resource.component[1])) - break; - } - } + break; - if (content_hosting_configuration) { + CASE("certificates") - msaf_application_server_state_log(as_state->current_content_hosting_configurations, "Current Content Hosting Configurations"); + msaf_application_server_state_node_t *as_state; + as_state = e->application_server_state; + ogs_assert(as_state); + if (message.h.resource.component[1]) { + SWITCH(message.h.method) + CASE(OGS_SBI_HTTP_METHOD_POST) + if(response->status == 201) { - ogs_debug("Removing %s from current_content_hosting_configurations", content_hosting_configuration->state); - ogs_free(content_hosting_configuration->state); - ogs_list_remove(as_state->current_content_hosting_configurations, content_hosting_configuration); - ogs_free(content_hosting_configuration); - msaf_application_server_state_log(as_state->current_content_hosting_configurations, "Current Content Hosting Configurations"); - } + ogs_debug("[%s] Method [%s] with Response [%d] recieved for certificate [%s]", message.h.resource.component[0], message.h.method, response->status, message.h.resource.component[1]); - ogs_list_for_each_safe(&as_state->delete_content_hosting_configurations, node, delete_content_hosting_configuration) { + resource_id_node_t *certificate; - if (!strcmp(delete_content_hosting_configuration->state, message.h.resource.component[1])) { + //Iterate upload_certs and find match strcmp resource component 0 + ogs_list_for_each(&as_state->upload_certificates,certificate){ + if(!strcmp(certificate->state, message.h.resource.component[1])) + break; + } + if(certificate) { - msaf_application_server_state_log(&as_state->delete_content_hosting_configurations, "Delete Content Hosting Configurations"); + ogs_debug("Removing certificate [%s] from upload_certificates", certificate->state); - ogs_debug("Destroying Content Hosting Configuration: %s", delete_content_hosting_configuration->state); - ogs_free(delete_content_hosting_configuration->state); - ogs_list_remove(&as_state->delete_content_hosting_configurations, delete_content_hosting_configuration); - ogs_free(delete_content_hosting_configuration); + ogs_list_remove(&as_state->upload_certificates, certificate); - msaf_application_server_state_log(&as_state->delete_content_hosting_configurations, "Delete Content Hosting Configurations"); - } - } + ogs_debug("Adding certificate [%s] to current_certificates", certificate->state); - } - if (response->status == 404){ - ogs_error("Not Found\n"); - } - if (response->status == 413){ - ogs_error("Payload too large\n"); - } - if (response->status == 414){ - ogs_error("URI too long\n"); - } - if (response->status == 415){ - ogs_error("Unsupported Media Type\n"); - } - if (response->status == 500){ - ogs_error("Internal Server Error\n"); - } - if (response->status == 503){ - ogs_error("Service Unavailable\n"); - } - next_action_for_application_server(as_state); - break; - DEFAULT - ogs_error("Unknown M3 Content Hosting Configuration operation [%s]", message.h.resource.component[1]); - break; - END - break; - } else { - cJSON *entry; - cJSON *chc_array = cJSON_Parse(response->http.content); - resource_id_node_t *current_chc; - SWITCH(message.h.method) - CASE(OGS_SBI_HTTP_METHOD_GET) - - if (response->status == 200) { - - ogs_debug("[%s] Method [%s] with Response [%d] for Content Hosting Configuration operation [%s]", - message.h.resource.component[0], message.h.method, response->status, message.h.resource.component[1]); - - if (as_state->current_content_hosting_configurations == NULL) { - as_state->current_content_hosting_configurations = ogs_calloc(1,sizeof(*as_state->current_content_hosting_configurations)); - ogs_assert(as_state->current_content_hosting_configurations); - ogs_list_init(as_state->current_content_hosting_configurations); - - } else { - resource_id_node_t *next, *node; - ogs_list_for_each_safe(as_state->current_content_hosting_configurations, next, node) { - ogs_free(node->state); - ogs_list_remove(as_state->current_content_hosting_configurations, node); - ogs_free(node); - } - } - cJSON_ArrayForEach(entry, chc_array) { - char *id = strrchr(entry->valuestring, '/'); - if (id == NULL) { - id = entry->valuestring; - } else { - id++; - } - current_chc = ogs_calloc(1, sizeof(*current_chc)); - current_chc->state = ogs_strdup(id); - ogs_debug("Adding [%s] to the current Content Hosting Configuration list",current_chc->state); - ogs_list_add(as_state->current_content_hosting_configurations, current_chc); - } - - cJSON_Delete(chc_array); - } - if (response->status == 500){ - ogs_error("Received Internal Server error\n"); - } - if (response->status == 503) { - ogs_error("Service Unavailable\n"); - } - next_action_for_application_server(as_state); - break; - DEFAULT - ogs_error("Unknown M3 Content Hosting Configuratiobn GET operation [%s]", message.h.resource.component[1]); - break; - END - break; + ogs_list_add(as_state->current_certificates, certificate); + // ogs_free(upload_cert_id); + } + } + if(response->status == 405){ + ogs_error("Server Certificate resource already exist at the specified path\n"); + } + if(response->status == 413){ + ogs_error("Payload too large\n"); + } + if(response->status == 414){ + ogs_error("URI too long\n"); + } + if(response->status == 415){ + ogs_error("Unsupported media type\n"); + } + if(response->status == 500){ + ogs_error("Internal server error\n"); + } + if(response->status == 503){ + ogs_error("Service unavailable\n"); } next_action_for_application_server(as_state); - break; + CASE(OGS_SBI_HTTP_METHOD_PUT) + if(response->status == 200 || response->status == 204) { - CASE("certificates") + ogs_debug("[%s] Method [%s] with Response [%d] recieved for certificate [%s]", message.h.resource.component[0], message.h.method, response->status,message.h.resource.component[1]); - msaf_application_server_state_node_t *as_state; - as_state = e->application_server_state; - ogs_assert(as_state); - if (message.h.resource.component[1]) { - SWITCH(message.h.method) - CASE(OGS_SBI_HTTP_METHOD_POST) - if (response->status == 201) { + resource_id_node_t *certificate; - ogs_debug("[%s] Method [%s] with Response [%d] recieved for certificate [%s]", message.h.resource.component[0], message.h.method, response->status, message.h.resource.component[1]); + msaf_application_server_state_log(&as_state->upload_certificates, "Upload Certificates"); - resource_id_node_t *certificate; + //Iterate upload_certs and find match strcmp resource component 0 + ogs_list_for_each(&as_state->upload_certificates,certificate){ - //Iterate upload_certs and find match strcmp resource component 0 - ogs_list_for_each(&as_state->upload_certificates,certificate){ - if (!strcmp(certificate->state, message.h.resource.component[1])) - break; - } - if (certificate) { - - ogs_debug("Removing certificate [%s] from upload_certificates", certificate->state); - - ogs_list_remove(&as_state->upload_certificates, certificate); - - ogs_debug("Adding certificate [%s] to current_certificates", certificate->state); - - ogs_list_add(as_state->current_certificates, certificate); - // ogs_free(upload_cert_id); - } - } - if (response->status == 405){ - ogs_error("Server Certificate resource already exists at the specified path\n"); - } - if (response->status == 413){ - ogs_error("Payload too large\n"); - } - if (response->status == 414){ - ogs_error("URI too long\n"); - } - if (response->status == 415){ - ogs_error("Unsupported media type\n"); - } - if (response->status == 500){ - ogs_error("Internal server error\n"); - } - if (response->status == 503){ - ogs_error("Service unavailable\n"); - } - next_action_for_application_server(as_state); + if(!strcmp(certificate->state, message.h.resource.component[1])) break; - CASE(OGS_SBI_HTTP_METHOD_PUT) - if (response->status == 200 || response->status == 204) { - - ogs_debug("[%s] Method [%s] with Response [%d] recieved for certificate [%s]", message.h.resource.component[0], message.h.method, response->status,message.h.resource.component[1]); - - resource_id_node_t *certificate; - - msaf_application_server_state_log(&as_state->upload_certificates, "Upload Certificates"); - - //Iterate upload_certs and find match strcmp resource component 0 - ogs_list_for_each(&as_state->upload_certificates,certificate){ - - if (!strcmp(certificate->state, message.h.resource.component[1])) - break; - } + } - if (!certificate){ - ogs_debug("Certificate %s not found in upload certificates", message.h.resource.component[1]); - } else { - ogs_debug("Removing certificate [%s] from upload_certificates", certificate->state); - ogs_free(certificate->state); + if(!certificate){ + ogs_debug("Certificate %s not found in upload certificates", message.h.resource.component[1]); + } else { + ogs_debug("Removing certificate [%s] from upload_certificates", certificate->state); + ogs_free(certificate->state); - ogs_list_remove(&as_state->upload_certificates, certificate); - ogs_free(certificate); - } - } - if (response->status == 404){ - ogs_error("Not Found\n"); - } - if (response->status == 413){ - ogs_error("Payload too large\n"); - } - if (response->status == 414){ - ogs_error("URI too long\n"); - } - if (response->status == 415){ - ogs_error("Unsupported Media Type\n"); - } - if (response->status == 500){ - ogs_error("Internal Server Error\n"); - } - if (response->status == 503){ - ogs_error("Service Unavailable\n"); - } - next_action_for_application_server(as_state); - break; - CASE(OGS_SBI_HTTP_METHOD_DELETE) - if (response->status == 204) { + ogs_list_remove(&as_state->upload_certificates, certificate); + ogs_free(certificate); + } + } + if(response->status == 404){ + ogs_error("Not Found\n"); + } + if(response->status == 413){ + ogs_error("Payload too large\n"); + } + if(response->status == 414){ + ogs_error("URI too long\n"); + } + if(response->status == 415){ + ogs_error("Unsupported Media Type\n"); + } + if(response->status == 500){ + ogs_error("Internal Server Error\n"); + } + if(response->status == 503){ + ogs_error("Service Unavailable\n"); + } + next_action_for_application_server(as_state); + break; + CASE(OGS_SBI_HTTP_METHOD_DELETE) + if(response->status == 204) { - ogs_debug("[%s] Method [%s] with Response [%d] recieved for Certificate [%s]", message.h.resource.component[0], message.h.method, response->status,message.h.resource.component[1]); + ogs_debug("[%s] Method [%s] with Response [%d] recieved for Certificate [%s]", message.h.resource.component[0], message.h.method, response->status,message.h.resource.component[1]); - resource_id_node_t *certificate, *next = NULL; - resource_id_node_t *delete_certificate, *node = NULL; + resource_id_node_t *certificate, *next = NULL; + resource_id_node_t *delete_certificate, *node = NULL; - if (as_state->current_certificates) { - ogs_list_for_each_safe(as_state->current_certificates, next, certificate){ + if(as_state->current_certificates) { + ogs_list_for_each_safe(as_state->current_certificates, next, certificate){ - if (!strcmp(certificate->state, message.h.resource.component[1])) - break; - } - } + if(!strcmp(certificate->state, message.h.resource.component[1])) + break; + } + } - if (certificate) { + if(certificate) { - msaf_application_server_state_log(as_state->current_certificates, "Current Certificates"); + msaf_application_server_state_log(as_state->current_certificates, "Current Certificates"); - ogs_debug("Removing certificate [%s] from current_certificates", certificate->state); - ogs_free(certificate->state); + ogs_debug("Removing certificate [%s] from current_certificates", certificate->state); + ogs_free(certificate->state); - ogs_list_remove(as_state->current_certificates, certificate); - ogs_free(certificate); - msaf_application_server_state_log(as_state->current_certificates, "Current Certificates"); - } + ogs_list_remove(as_state->current_certificates, certificate); + ogs_free(certificate); + msaf_application_server_state_log(as_state->current_certificates, "Current Certificates"); + } - ogs_list_for_each_safe(&as_state->delete_certificates, node, delete_certificate){ + ogs_list_for_each_safe(&as_state->delete_certificates, node, delete_certificate){ - if (!strcmp(delete_certificate->state, message.h.resource.component[1])) { - msaf_application_server_state_log(&as_state->delete_certificates, "Delete Certificates"); + if(!strcmp(delete_certificate->state, message.h.resource.component[1])) { + msaf_application_server_state_log(&as_state->delete_certificates, "Delete Certificates"); - ogs_debug("Destroying Certificate: %s", delete_certificate->state); - ogs_free(delete_certificate->state); - ogs_list_remove(&as_state->delete_certificates, delete_certificate); - ogs_free(delete_certificate); - msaf_application_server_state_log(&as_state->delete_certificates, "Delete Certificates"); + ogs_debug("Destroying Certificate: %s", delete_certificate->state); + ogs_free(delete_certificate->state); + ogs_list_remove(&as_state->delete_certificates, delete_certificate); + ogs_free(delete_certificate); + msaf_application_server_state_log(&as_state->delete_certificates, "Delete Certificates"); - } - } - } - if (response->status == 404){ - ogs_error("Not Found\n"); - } - if (response->status == 413){ - ogs_error("Payload too large\n"); - } - if (response->status == 414){ - ogs_error("URI too long\n"); - } - if (response->status == 415){ - ogs_error("Unsupported Media Type\n"); - } - if (response->status == 500){ - ogs_error("Internal Server Error\n"); - } - if (response->status == 503){ - ogs_error("Service Unavailable\n"); - } - next_action_for_application_server(as_state); - break; - DEFAULT - ogs_error("Unknown M3 certificate operation [%s]", message.h.resource.component[1]); - break; - END - break; - } else { - cJSON *entry; - cJSON *cert_array = cJSON_Parse(response->http.content); - resource_id_node_t *current_cert; - SWITCH(message.h.method) - CASE(OGS_SBI_HTTP_METHOD_GET) + } + } + } + if(response->status == 404){ + ogs_error("Not Found\n"); + } + if(response->status == 413){ + ogs_error("Payload too large\n"); + } + if(response->status == 414){ + ogs_error("URI too long\n"); + } + if(response->status == 415){ + ogs_error("Unsupported Media Type\n"); + } + if(response->status == 500){ + ogs_error("Internal Server Error\n"); + } + if(response->status == 503){ + ogs_error("Service Unavailable\n"); + } + next_action_for_application_server(as_state); + break; + DEFAULT + ogs_error("Unknown M3 certificate operation [%s]", message.h.resource.component[1]); + ogs_assert(true == nf_server_send_error(stream, OGS_SBI_HTTP_STATUS_BAD_REQUEST, 0, &message, "Unknown M3 certificate operation.", message.h.resource.component[1], NULL, NULL, app_meta)); + break; + END + break; + } else { + cJSON *entry; + cJSON *cert_array = cJSON_Parse(response->http.content); + resource_id_node_t *current_cert; + SWITCH(message.h.method) + CASE(OGS_SBI_HTTP_METHOD_GET) - if (response->status == 200) { + if(response->status == 200) { - ogs_debug("[%s] Method [%s] with Response [%d] received", - message.h.resource.component[0], message.h.method, response->status); + ogs_debug("[%s] Method [%s] with Response [%d] received", + message.h.resource.component[0], message.h.method, response->status); - if (as_state->current_certificates == NULL) { - as_state->current_certificates = ogs_calloc(1,sizeof(*as_state->current_certificates)); - ogs_assert(as_state->current_certificates); - ogs_list_init(as_state->current_certificates); + if (as_state->current_certificates == NULL) { + as_state->current_certificates = ogs_calloc(1,sizeof(*as_state->current_certificates)); + ogs_assert(as_state->current_certificates); + ogs_list_init(as_state->current_certificates); - } else { - resource_id_node_t *next, *node; - ogs_list_for_each_safe(as_state->current_certificates, next, node) { + } else { + resource_id_node_t *next, *node; + ogs_list_for_each_safe(as_state->current_certificates, next, node) { - ogs_debug("Removing certificate [%s] from current_certificates", node->state); + ogs_debug("Removing certificate [%s] from current_certificates", node->state); - ogs_free(node->state); - ogs_list_remove(as_state->current_certificates, node); - ogs_free(node); - } - } - cJSON_ArrayForEach(entry, cert_array) { - char *id = strrchr(entry->valuestring, '/'); - if (id == NULL) { - id = entry->valuestring; - } else { - id++; - } - current_cert = ogs_calloc(1, sizeof(*current_cert)); - current_cert->state = ogs_strdup(id); - ogs_debug("Adding certificate [%s] to Current certificates", current_cert->state); - ogs_list_add(as_state->current_certificates, current_cert); - } + ogs_free(node->state); + ogs_list_remove(as_state->current_certificates, node); + ogs_free(node); + } + } + cJSON_ArrayForEach(entry, cert_array) { + char *id = strrchr(entry->valuestring, '/'); + if (id == NULL) { + id = entry->valuestring; + } else { + id++; + } + current_cert = ogs_calloc(1, sizeof(*current_cert)); + current_cert->state = ogs_strdup(id); + ogs_debug("Adding certificate [%s] to Current certificates", current_cert->state); + ogs_list_add(as_state->current_certificates, current_cert); + } - cJSON_Delete(cert_array); - } - if (response->status == 500) { - ogs_error("Received Internal Server error"); - } - if (response->status == 503) { - ogs_error("Service Unavailable"); - } - next_action_for_application_server(as_state); - break; - DEFAULT - ogs_error("Unknown M3 certificate GET operation [%s]", message.h.resource.component[1]); - break; - END - break; + cJSON_Delete(cert_array); + } + if (response->status == 500){ + ogs_error("Received Internal Server error"); + } + if (response->status == 503) { + ogs_error("Service Unavailable"); } next_action_for_application_server(as_state); - break; - DEFAULT - ogs_error("Unknown M3 operation [%s]", message.h.resource.component[0]); + char *err; + ogs_error("Unknown M3 certificate operation [%s] with method [%s]", message.h.resource.component[1], message.h.method); + asprintf(&err, "Unsupported M3 Certificate operation [%s] with method [%s]", message.h.resource.component[1], message.h.method); + ogs_assert(true == nf_server_send_error(stream, OGS_SBI_HTTP_STATUS_BAD_REQUEST, 0, &message, "Unknown M3 Certificate operation", err, NULL, NULL, app_meta)); + break; - END + END + break; + } + next_action_for_application_server(as_state); + break; - CASE(OGS_SBI_SERVICE_NAME_NNRF_NFM) + DEFAULT + ogs_error("Unknown M3 operation [%s]", message.h.resource.component[0]); + ogs_assert(true == nf_server_send_error(stream, OGS_SBI_HTTP_STATUS_BAD_REQUEST, 0, &message, "Unsupported M3 operation", message.h.resource.component[0], NULL, NULL, app_meta)); + break; + END + break; - SWITCH(message.h.resource.component[0]) - CASE(OGS_SBI_RESOURCE_NAME_NF_INSTANCES) - nf_instance = e->h.sbi.data; - ogs_assert(nf_instance); - ogs_assert(OGS_FSM_STATE(&nf_instance->sm)); + CASE(OGS_SBI_SERVICE_NAME_NNRF_NFM) - e->h.sbi.message = &message; - ogs_fsm_dispatch(&nf_instance->sm, e); - break; + SWITCH(message.h.resource.component[0]) + CASE(OGS_SBI_RESOURCE_NAME_NF_INSTANCES) + nf_instance = e->h.sbi.data; + ogs_assert(nf_instance); + ogs_assert(OGS_FSM_STATE(&nf_instance->sm)); - CASE(OGS_SBI_RESOURCE_NAME_SUBSCRIPTIONS) - subscription_data = e->h.sbi.data; - ogs_assert(subscription_data); + e->h.sbi.message = &message; + ogs_fsm_dispatch(&nf_instance->sm, e); + break; - SWITCH(message.h.method) - CASE(OGS_SBI_HTTP_METHOD_POST) - if (message.res_status == OGS_SBI_HTTP_STATUS_CREATED || - message.res_status == OGS_SBI_HTTP_STATUS_OK) { - ogs_nnrf_nfm_handle_nf_status_subscribe( - subscription_data, &message); - } else { - ogs_error("HTTP response error : %d", - message.res_status); - } - break; + CASE(OGS_SBI_RESOURCE_NAME_SUBSCRIPTIONS) + subscription_data = e->h.sbi.data; + ogs_assert(subscription_data); - CASE(OGS_SBI_HTTP_METHOD_DELETE) - if (message.res_status == OGS_SBI_HTTP_STATUS_NO_CONTENT) { - ogs_sbi_subscription_data_remove(subscription_data); - } else { - ogs_error("HTTP response error : %d", - message.res_status); - } - break; + SWITCH(message.h.method) + CASE(OGS_SBI_HTTP_METHOD_POST) + if (message.res_status == OGS_SBI_HTTP_STATUS_CREATED || + message.res_status == OGS_SBI_HTTP_STATUS_OK) { + ogs_nnrf_nfm_handle_nf_status_subscribe( + subscription_data, &message); + } else { + ogs_error("HTTP response error : %d", + message.res_status); + } + break; - DEFAULT - ogs_error("Invalid HTTP method [%s]", message.h.method); - ogs_assert_if_reached(); - END - break; + CASE(OGS_SBI_HTTP_METHOD_DELETE) + if (message.res_status == OGS_SBI_HTTP_STATUS_NO_CONTENT) { + ogs_sbi_subscription_data_remove(subscription_data); + } else { + ogs_error("HTTP response error : %d", + message.res_status); + } + break; - DEFAULT - ogs_error("Invalid resource name [%s]", - message.h.resource.component[0]); - ogs_assert_if_reached(); + DEFAULT + ogs_error("Invalid HTTP method [%s]", message.h.method); + ogs_assert_if_reached(); END break; DEFAULT - ogs_error("Invalid service name [%s]", message.h.service.name); + ogs_error("Invalid resource name [%s]", + message.h.resource.component[0]); ogs_assert_if_reached(); + END + break; + + DEFAULT + ogs_error("Invalid service name [%s]", message.h.service.name); + ogs_assert_if_reached(); END ogs_sbi_message_free(&message); @@ -1280,37 +952,7 @@ void msaf_state_functional(ogs_fsm_t *s, msaf_event_t *e) ogs_error("No handler for event %s", msaf_event_get_name(e)); break; } -} - -static void report_purging_error(msaf_application_server_state_node_t *as_state, ogs_sbi_message_t *message, int status_code, const char *status_msg) -{ - char *regex_purge = NULL; - char *error; - purge_resource_id_node_t *content_hosting_cache, *next = NULL; - - ogs_error("Error message from the Application Server [%s]: %s\n", as_state->application_server->canonicalHostname, status_msg); - - ogs_list_for_each_safe(&as_state->purge_content_hosting_cache, next, content_hosting_cache) { - if (!strcmp(content_hosting_cache->state, message->h.resource.component[1])) { - if (content_hosting_cache->state) ogs_free(content_hosting_cache->state); - if (content_hosting_cache->purge_regex) { - regex_purge = content_hosting_cache->purge_regex; - content_hosting_cache->purge_regex = NULL; - } - - ogs_list_remove(&as_state->purge_content_hosting_cache, content_hosting_cache); - ogs_free(content_hosting_cache); - } - } - if (regex_purge) { - error = ogs_msprintf("Application Server encountered a problem when purging its cache for the %s", regex_purge); - ogs_free(regex_purge); - } else { - error = ogs_msprintf("Application Server encountered a problem when purging its cache"); - } - - ogs_assert(true == ogs_sbi_server_send_error(as_state->stream, - status_code, message, "Application Server encountered a problem when purging its cache", error)); + ogs_free(nf_name); } /* vim:ts=8:sts=4:sw=4:expandtab: diff --git a/src/5gmsaf/msaf-sm.h b/src/5gmsaf/msaf-sm.h index a4d06a3..14fae67 100644 --- a/src/5gmsaf/msaf-sm.h +++ b/src/5gmsaf/msaf-sm.h @@ -12,6 +12,7 @@ program. If this file is missing then the license can be retrieved from #define MSAF_SM_H #include "event.h" +#include "ContentProtocolsDiscovery_body.h" #ifdef __cplusplus extern "C" { @@ -22,6 +23,21 @@ void msaf_state_final(ogs_fsm_t *s, msaf_event_t *e); void msaf_state_functional(ogs_fsm_t *s, msaf_event_t *e); void msaf_state_exception(ogs_fsm_t *s, msaf_event_t *e); +void msaf_m1_state_initial(ogs_fsm_t *s, msaf_event_t *e); +void msaf_m1_state_final(ogs_fsm_t *s, msaf_event_t *e); +void msaf_m1_state_functional(ogs_fsm_t *s, msaf_event_t *e); +void msaf_m1_state_exception(ogs_fsm_t *s, msaf_event_t *e); + +void msaf_m5_state_initial(ogs_fsm_t *s, msaf_event_t *e); +void msaf_m5_state_final(ogs_fsm_t *s, msaf_event_t *e); +void msaf_m5_state_functional(ogs_fsm_t *s, msaf_event_t *e); +void msaf_m5_state_exception(ogs_fsm_t *s, msaf_event_t *e); + +void msaf_maf_mgmt_state_initial(ogs_fsm_t *s, msaf_event_t *e); +void msaf_maf_mgmt_state_final(ogs_fsm_t *s, msaf_event_t *e); +void msaf_maf_mgmt_state_functional(ogs_fsm_t *s, msaf_event_t *e); +void msaf_maf_mgmt_state_exception(ogs_fsm_t *s, msaf_event_t *e); + #define msaf_sm_debug(__pe) \ ogs_debug("%s(): %s", __func__, msaf_event_get_name(__pe)) diff --git a/src/5gmsaf/msaf.yaml b/src/5gmsaf/msaf.yaml.in similarity index 90% rename from src/5gmsaf/msaf.yaml rename to src/5gmsaf/msaf.yaml.in index dc5635f..054a607 100644 --- a/src/5gmsaf/msaf.yaml +++ b/src/5gmsaf/msaf.yaml.in @@ -8,7 +8,8 @@ # #=========================================================================== # -# logger: +logger: + level: debug # # o Set OGS_LOG_INFO to all domain level # - If `level` is omitted, the default level is OGS_LOG_INFO) @@ -130,17 +131,31 @@ logger: # * That is, 'no_service_names' has no effect. # msaf: - open5gsIntegration: false + open5gsIntegration: false sbi: - - addr: 0.0.0.0 - port: 7778 + - addr: 127.0.0.22 + port: 7777 + m1: + - addr: 127.0.0.23 + port: 7777 + m5: + - addr: 127.0.0.24 + port: 7777 + maf: + - addr: 127.0.0.25 + port: 7777 applicationServers: - canonicalHostname: localhost urlPathPrefixFormat: /m4d/provisioning-session-{provisioningSessionId}/ m3Port: 7777 - # The following parameters are only needed until M1 is implemented. - certificate: ../../examples/Certificates.json - contentHostingConfiguration: ../../examples/ContentHostingConfiguration_Big-Buck-Bunny_pull-ingest.json + certificateManager: @default-certmgr@ + serverResponseCacheControl: + - maxAge: 60 + m1ProvisioningSessions: 60 + m1ContentHostingConfigurations: 60 + m1ServerCertificates: 60 + m1ContentProtocols: 86400 + m5ServiceAccessInformation: 60 # # nrf: # diff --git a/src/5gmsaf/provisioning-session.c b/src/5gmsaf/provisioning-session.c index 40ea89b..2a25e99 100644 --- a/src/5gmsaf/provisioning-session.c +++ b/src/5gmsaf/provisioning-session.c @@ -8,23 +8,35 @@ program. If this file is missing then the license can be retrieved from https://drive.google.com/file/d/1cinCiA778IErENZ3JN52VFW-1ffHpx7Z/view */ -#include "provisioning-session.h" +#include #include "application-server-context.h" #include "media-player-entry.h" +#include "certmgr.h" #include "context.h" #include "utilities.h" +#include "hash.h" + +#include "provisioning-session.h" typedef struct free_ogs_hash_provisioning_session_s { - char *provisioning_session; + const char *provisioning_session; ogs_hash_t *hash; } free_ogs_hash_provisioning_session_t; +typedef struct free_ogs_hash_provisioning_session_certificate_s { + const char *certificate; + ogs_hash_t *hash; +} free_ogs_hash_provisioning_session_certificate_t; + static regex_t *relative_path_re = NULL; static int ogs_hash_do_cert_check(void *rec, const void *key, int klen, const void *value); static int free_ogs_hash_provisioning_session(void *rec, const void *key, int klen, const void *value); +static int free_ogs_hash_provisioning_session_certificate(void *rec, const void *key, int klen, const void *value); static char* url_path_create(const char* macro, const char* session_id, const msaf_application_server_node_t *msaf_as); static void tidy_relative_path_re(void); +static const char *calculate_provisioning_session_hash(OpenAPI_provisioning_session_t *provisioning_session); +static const char *calculate_service_access_information_hash(OpenAPI_service_access_information_resource_t *service_access_information); /***** Public functions *****/ @@ -55,34 +67,33 @@ msaf_content_hosting_configuration_with_af_unique_cert_id(msaf_provisioning_sess } msaf_provisioning_session_t * -msaf_provisioning_session_create(char *provisioning_session_type, char *asp_id, char *external_app_id) +msaf_provisioning_session_create(const char *provisioning_session_type, const char *asp_id, const char *external_app_id) { msaf_provisioning_session_t *msaf_provisioning_session; - char *media_player_entry; - ogs_uuid_t uuid; char id[OGS_UUID_FORMATTED_LENGTH + 1]; - OpenAPI_provisioning_session_t *provisioning_session; + char *prov_sess_type; + prov_sess_type = ogs_strdup(provisioning_session_type); ogs_uuid_get(&uuid); ogs_uuid_format(id, &uuid); - - provisioning_session = OpenAPI_provisioning_session_create(ogs_strdup(id), OpenAPI_provisioning_session_type_FromString(provisioning_session_type), (asp_id)?ogs_strdup(asp_id):NULL, ogs_strdup(external_app_id), NULL, NULL, NULL, NULL, NULL, NULL); + provisioning_session = OpenAPI_provisioning_session_create(ogs_strdup(id), OpenAPI_provisioning_session_type_FromString(prov_sess_type), (asp_id)?ogs_strdup(asp_id):NULL, ogs_strdup(external_app_id), NULL, NULL, NULL, NULL, NULL, NULL); + ogs_free(prov_sess_type); msaf_provisioning_session = ogs_calloc(1, sizeof(msaf_provisioning_session_t)); ogs_assert(msaf_provisioning_session); - msaf_provisioning_session->provisioningSessionId = ogs_strdup(provisioning_session->provisioning_session_id); - msaf_provisioning_session->provisioningSessionType = provisioning_session->provisioning_session_type; msaf_provisioning_session->aspId = (provisioning_session->asp_id)?ogs_strdup(provisioning_session->asp_id):NULL; msaf_provisioning_session->externalApplicationId = ogs_strdup(provisioning_session->external_application_id); + msaf_provisioning_session->provisioningSessionReceived = time(NULL); + msaf_provisioning_session->provisioningSessionHash = ogs_strdup(calculate_provisioning_session_hash(provisioning_session)); msaf_provisioning_session->certificate_map = msaf_certificate_map(); ogs_hash_set(msaf_self()->provisioningSessions_map, ogs_strdup(msaf_provisioning_session->provisioningSessionId), OGS_HASH_KEY_STRING, msaf_provisioning_session); -#if 1 /* TODO: Remove when content hosting configuration is available via M1 interface */ +#if 0 /* TODO: Remove when content hosting configuration is available via M1 interface */ msaf_provisioning_session->contentHostingConfiguration = msaf_content_hosting_configuration_create(msaf_provisioning_session); media_player_entry = media_player_entry_create(msaf_provisioning_session->provisioningSessionId, msaf_provisioning_session->contentHostingConfiguration); ogs_assert(media_player_entry); @@ -99,35 +110,35 @@ msaf_provisioning_session_get_json(const char *provisioning_session_id) { msaf_provisioning_session_t *msaf_provisioning_session; - cJSON *provisioning_session_json; - - OpenAPI_provisioning_session_t *provisioning_session = NULL; - provisioning_session = ogs_malloc(sizeof(OpenAPI_provisioning_session_t)); - ogs_assert(provisioning_session); + cJSON *provisioning_session_json = NULL; msaf_provisioning_session = msaf_provisioning_session_find_by_provisioningSessionId(provisioning_session_id); if (msaf_provisioning_session) { + OpenAPI_provisioning_session_t *provisioning_session; + ogs_hash_index_t *cert_node; + + provisioning_session = ogs_calloc(1,sizeof(OpenAPI_provisioning_session_t)); + ogs_assert(provisioning_session); provisioning_session->provisioning_session_id = msaf_provisioning_session->provisioningSessionId; provisioning_session->provisioning_session_type = msaf_provisioning_session->provisioningSessionType; - provisioning_session->asp_id = msaf_provisioning_session->aspId; + provisioning_session->asp_id = msaf_provisioning_session->aspId; provisioning_session->external_application_id = msaf_provisioning_session->externalApplicationId; - provisioning_session->server_certificate_ids = NULL; - provisioning_session->content_preparation_template_ids = NULL; - provisioning_session->metrics_reporting_configuration_ids = NULL; - provisioning_session->policy_template_ids = NULL; - provisioning_session->edge_resources_configuration_ids = NULL; - provisioning_session->event_data_processing_configuration_ids = NULL; + provisioning_session->server_certificate_ids = (OpenAPI_set_t*)OpenAPI_list_create(); + for (cert_node=ogs_hash_first(msaf_provisioning_session->certificate_map); cert_node; cert_node=ogs_hash_next(cert_node)) { + ogs_debug("msaf_provisioning_session_get_json: Add cert %s", (const char *)ogs_hash_this_key(cert_node)); + OpenAPI_list_add(provisioning_session->server_certificate_ids, (void*)ogs_hash_this_key(cert_node)); + } provisioning_session_json = OpenAPI_provisioning_session_convertToJSON(provisioning_session); + + OpenAPI_list_free(provisioning_session->server_certificate_ids); + ogs_free(provisioning_session); } else { ogs_error("Unable to retrieve Provisioning Session"); - ogs_free(provisioning_session); - return NULL; } - ogs_free(provisioning_session); return provisioning_session_json; } @@ -164,78 +175,56 @@ msaf_content_hosting_configuration_certificate_check(msaf_provisioning_session_t } void -msaf_delete_certificate(char *resource_id) +msaf_delete_certificates(const char *provisioning_session_id) { - msaf_application_server_state_node_t *as_state; + ogs_list_for_each(&msaf_self()->application_server_states, as_state) { - resource_id_node_t *certificate, *next = NULL; - resource_id_node_t *upload_certificate, *next_node = NULL; - resource_id_node_t *delete_cert = NULL; - ogs_list_init(&as_state->delete_certificates); + resource_id_node_t *upload_certificate, *next_node; + /* delete certificates already on the AS */ if (as_state->current_certificates) { - - char *current_cert_id; - char *provisioning_session; - char *cert_id; + resource_id_node_t *certificate, *next; ogs_list_for_each_safe(as_state->current_certificates, next, certificate){ + char *cert_id; + char *provisioning_session; + char *current_cert_id = ogs_strdup(certificate->state); - current_cert_id = ogs_strdup(certificate->state); provisioning_session = strtok_r(current_cert_id,":",&cert_id); - if(!strcmp(provisioning_session, resource_id)) - break; + if(!strcmp(provisioning_session, provisioning_session_id)) { + /* provisioning session matches */ + resource_id_node_t *delete_cert; + delete_cert = ogs_calloc(1, sizeof(resource_id_node_t)); + ogs_assert(delete_cert); + delete_cert->state = ogs_strdup(certificate->state); + ogs_list_add(&as_state->delete_certificates, delete_cert); + } if(current_cert_id) ogs_free(current_cert_id); - } - - if (certificate) { - delete_cert = ogs_calloc(1, sizeof(resource_id_node_t)); - ogs_assert(delete_cert); - delete_cert->state = ogs_strdup(certificate->state); - ogs_list_add(&as_state->delete_certificates, delete_cert); - - } - - if (current_cert_id) - ogs_free(current_cert_id); } - { - - char *upload_cert_id = NULL; - char *provisioning_session; + /* remove entries from upload queue and try to delete just to be safe */ + ogs_list_for_each_safe(&as_state->upload_certificates, next_node, upload_certificate) { char *cert_id; + char *upload_cert_id = ogs_strdup(upload_certificate->state); + char *provisioning_session = strtok_r(upload_cert_id,":",&cert_id); - ogs_list_for_each_safe(&as_state->upload_certificates, next_node, upload_certificate) { - - upload_cert_id = ogs_strdup(upload_certificate->state); - provisioning_session = strtok_r(upload_cert_id,":",&cert_id); - if(!strcmp(provisioning_session, resource_id)) - break; - } - - if (upload_certificate) { - + if (!strcmp(provisioning_session, provisioning_session_id)) { ogs_list_remove(&as_state->upload_certificates, upload_certificate); - ogs_list_add(&as_state->delete_certificates, upload_certificate); - } - - if(upload_cert_id) + if (upload_cert_id) ogs_free(upload_cert_id); } - } } void -msaf_delete_content_hosting_configuration(char *resource_id) +msaf_delete_content_hosting_configuration(const char *provisioning_session_id) { msaf_application_server_state_node_t *as_state; @@ -251,7 +240,7 @@ msaf_delete_content_hosting_configuration(char *resource_id) ogs_list_for_each_safe(as_state->current_content_hosting_configurations, next, content_hosting_configuration){ - if (!strcmp(content_hosting_configuration->state, resource_id)) + if (!strcmp(content_hosting_configuration->state, provisioning_session_id)) break; } if (content_hosting_configuration) { @@ -264,7 +253,7 @@ msaf_delete_content_hosting_configuration(char *resource_id) } ogs_list_for_each_safe(&as_state->upload_content_hosting_configurations, next_node, upload_content_hosting_configuration){ - if (!strcmp(upload_content_hosting_configuration->state, resource_id)) + if (!strcmp(upload_content_hosting_configuration->state, provisioning_session_id)) break; } if (upload_content_hosting_configuration) { @@ -290,28 +279,7 @@ msaf_provisioning_session_find_by_provisioningSessionId(const char *provisioning ogs_hash_t * msaf_certificate_map(void) { - cJSON *entry; ogs_hash_t *certificate_map = ogs_hash_make(); - char *certificate = read_file(msaf_self()->config.certificate); - if(!certificate){ - ogs_error("The certificates JSON file [%s] cannot be opened or read.", msaf_self()->config.certificate); - } - cJSON *cert = cJSON_Parse(certificate); - if (!cert) { - ogs_error("The certificates JSON file [%s] does not parse as JSON.", msaf_self()->config.certificate); - } - cJSON_ArrayForEach(entry, cert) { - char *abs_path; - if (!entry->valuestring) { - ogs_error("Certificates JSON file configuration parameter has not been set"); - continue; - } - abs_path = rebase_path(msaf_self()->config.certificate, entry->valuestring); - ogs_hash_set(certificate_map, ogs_strdup(entry->string), OGS_HASH_KEY_STRING, abs_path); - } - cJSON_Delete(entry); - cJSON_Delete(cert); - free(certificate); return certificate_map; } @@ -354,14 +322,15 @@ msaf_retrieve_certificates_from_map(msaf_provisioning_session_t *provisioning_se certificate->state = provisioning_session_id_plus_cert_id; ogs_list_add(certs, certificate); } else { - ogs_error("Certificate id [%s] not found for Content Hosting Configuration [%s]", dist_config->certificate_id, provisioning_session->provisioningSessionId); + ogs_warn("Certificate id [%s] not found for Content Hosting Configuration [%s]", dist_config->certificate_id, provisioning_session->provisioningSessionId); resource_id_node_t *next; ogs_list_for_each_safe(certs, next, certificate) { ogs_list_remove(certs, certificate); if (certificate->state) ogs_free(certificate->state); ogs_free(certificate); } - certs = NULL; + ogs_free(certs); + certs = NULL; break; } } @@ -380,6 +349,7 @@ msaf_distribution_create(cJSON *content_hosting_config, msaf_provisioning_sessio char *media_player_entry; static const char macro[] = "{provisioningSessionId}"; msaf_application_server_node_t *msaf_as = NULL; + char *content_hosting_config_to_hash = NULL; msaf_as = ogs_list_first(&msaf_self()->config.applicationServers_list); @@ -424,24 +394,33 @@ msaf_distribution_create(cJSON *content_hosting_config, msaf_provisioning_sessio ogs_error("The Content Hosting Configuration has no Distribution Configuration"); } if (content_hosting_configuration->entry_point_path) { - + media_player_entry = ogs_msprintf("%s%s", dist_config->base_url, content_hosting_configuration->entry_point_path); } else { - ogs_debug("The contentHostingConfiguration has no entryPointPath"); + ogs_debug("The contentHostingConfiguration has no entryPointPath"); } if(media_player_entry) { provisioning_session->serviceAccessInformation = msaf_context_service_access_information_create(provisioning_session->provisioningSessionId, media_player_entry); + provisioning_session->serviceAccessInformationCreated = time(NULL); + provisioning_session->serviceAccessInformationHash = ogs_strdup(calculate_service_access_information_hash(provisioning_session->serviceAccessInformation)); } else { - ogs_debug("Couldn't formulate serviceAccessInformation as media Player Entry is not formulated"); + ogs_debug("Couldn't formulate serviceAccessInformation as media Player Entry is not formulated"); } provisioning_session->contentHostingConfiguration = content_hosting_configuration; + if(provisioning_session->contentHostingConfiguration) + { + content_hosting_config_to_hash = cJSON_Print(content_hosting_config); + provisioning_session->contentHostingConfigurationReceived = time(NULL); + + provisioning_session->contentHostingConfigurationHash = ogs_strdup(calculate_hash(content_hosting_config_to_hash)); + } ogs_free(url_path); return 1; } -cJSON *msaf_get_content_hosting_configuration_by_provisioning_session_id(char *provisioning_session_id) { +cJSON *msaf_get_content_hosting_configuration_by_provisioning_session_id(const char *provisioning_session_id) { msaf_provisioning_session_t *msaf_provisioning_session; cJSON *content_hosting_configuration_json; @@ -458,94 +437,8 @@ cJSON *msaf_get_content_hosting_configuration_by_provisioning_session_id(char *p return content_hosting_configuration_json; } - -OpenAPI_content_hosting_configuration_t * -msaf_content_hosting_configuration_create(msaf_provisioning_session_t *provisioning_session) -{ - OpenAPI_lnode_t *dist_config_node = NULL; - OpenAPI_distribution_configuration_t *dist_config = NULL; - char *url_path; - static const char macro[] = "{provisioningSessionId}"; - msaf_application_server_state_node_t *as_state; - char *domain_name; - - if(!msaf_self()->config.contentHostingConfiguration) { - ogs_error("contentHostingConfiguration not present in the MSAF configuration file"); - } - - ogs_assert(msaf_self()->config.contentHostingConfiguration); - - as_state = ogs_list_first(&msaf_self()->application_server_states); - - url_path = url_path_create(macro, provisioning_session->provisioningSessionId, as_state->application_server); - - char *content_host_config_data = read_file(msaf_self()->config.contentHostingConfiguration); - if (content_host_config_data == NULL){ - ogs_error("Reading contentHostingConfiguration failed"); - } - - ogs_assert(content_host_config_data); - - cJSON *content_host_config_json = cJSON_Parse(content_host_config_data); - - if (content_host_config_json == NULL){ - ogs_error("Parsing contentHostingConfiguration to JSON structure failed"); - } - - ogs_assert(content_host_config_json); - - OpenAPI_content_hosting_configuration_t *content_hosting_configuration - = OpenAPI_content_hosting_configuration_parseFromJSON(content_host_config_json); - if (!uri_relative_check(content_hosting_configuration->entry_point_path)) { - cJSON_Delete(content_host_config_json); - ogs_free(url_path); - ogs_debug("Check for relative URI for entryPointPath fails"); - if (content_hosting_configuration) - OpenAPI_content_hosting_configuration_free(content_hosting_configuration); - ogs_free(content_host_config_data); - return NULL; - } - - if (content_hosting_configuration->distribution_configurations) { - OpenAPI_list_for_each(content_hosting_configuration->distribution_configurations, dist_config_node) { - char *protocol = "http"; - dist_config = (OpenAPI_distribution_configuration_t*)dist_config_node->data; - if (dist_config->canonical_domain_name) - ogs_free(dist_config->canonical_domain_name); - dist_config->canonical_domain_name = ogs_strdup(as_state->application_server->canonicalHostname); - if (dist_config->certificate_id) { - protocol = "https"; - } - - if(dist_config->domain_name_alias){ - domain_name = dist_config->domain_name_alias; - } else { - domain_name = dist_config->canonical_domain_name; - } - - if (dist_config->base_url) - ogs_free(dist_config->base_url); - dist_config->base_url = ogs_msprintf("%s://%s%s", protocol, domain_name, url_path); - ogs_debug("Distribution URL: %s",dist_config->base_url); - } - } else { - ogs_error("The Content Hosting Configuration has no Distribution Configuration"); - if (content_hosting_configuration) - OpenAPI_content_hosting_configuration_free(content_hosting_configuration); - ogs_free(content_host_config_data); - return NULL; - } - - cJSON_Delete(content_host_config_json); - ogs_free(url_path); - free (content_host_config_data); - provisioning_session->contentHostingConfiguration = content_hosting_configuration; - msaf_application_server_state_set(as_state, provisioning_session); - return content_hosting_configuration; -} - void -msaf_provisioning_session_hash_remove(char *provisioning_session_id) +msaf_provisioning_session_hash_remove(const char *provisioning_session_id) { free_ogs_hash_provisioning_session_t fohps = { provisioning_session_id, @@ -554,7 +447,20 @@ msaf_provisioning_session_hash_remove(char *provisioning_session_id) ogs_hash_do(free_ogs_hash_provisioning_session, &fohps, msaf_self()->provisioningSessions_map); } -int uri_relative_check(char *entry_point_path) +void +msaf_provisioning_session_certificate_hash_remove(const char *provisioning_session_id, const char *certificate_id) +{ + msaf_provisioning_session_t *provisioning_session = NULL; + provisioning_session = msaf_provisioning_session_find_by_provisioningSessionId(provisioning_session_id); + + free_ogs_hash_provisioning_session_certificate_t fohpsc = { + certificate_id, + provisioning_session->certificate_map + }; + ogs_hash_do(free_ogs_hash_provisioning_session_certificate, &fohpsc, provisioning_session->certificate_map); +} + +int uri_relative_check(const char *entry_point_path) { int result; @@ -596,8 +502,61 @@ int uri_relative_check(char *entry_point_path) } } +char *enumerate_provisioning_sessions(void) +{ + ogs_hash_index_t *hi; + char *provisioning_sessions = "[]"; + int number_of_provisioning_sessions = ogs_hash_count(msaf_self()->provisioningSessions_map); + if (number_of_provisioning_sessions) + { + provisioning_sessions = ogs_calloc(1, (4 + (sizeof(char)*(OGS_UUID_FORMATTED_LENGTH + 1) *number_of_provisioning_sessions) +1)); + provisioning_sessions[0] = '['; + + for (hi = ogs_hash_first(msaf_self()->provisioningSessions_map); hi; hi = ogs_hash_next(hi)) { + const char *key = NULL; + const char *val = NULL; + char *provisioning_session = NULL; + key = ogs_hash_this_key(hi); + ogs_assert(key); + val = ogs_hash_this_val(hi); + ogs_assert(val); + provisioning_session = ogs_msprintf("\"%s\", ", key); + strcat(provisioning_sessions, provisioning_session); + ogs_free(provisioning_session); + + } + provisioning_sessions[strlen(provisioning_sessions) - 2] = ']'; + provisioning_sessions[strlen(provisioning_sessions) - 1] = '\0'; + } + return provisioning_sessions; -/***** Private functions *****/ +} + +static const char *calculate_provisioning_session_hash(OpenAPI_provisioning_session_t *provisioning_session) +{ + cJSON *provisioning_sess = NULL; + char *provisioning_session_to_hash; + const char *provisioning_session_hashed = NULL; + provisioning_sess = OpenAPI_provisioning_session_convertToJSON(provisioning_session); + provisioning_session_to_hash = cJSON_Print(provisioning_sess); + cJSON_Delete(provisioning_sess); + provisioning_session_hashed = calculate_hash(provisioning_session_to_hash); + ogs_free(provisioning_session_to_hash); + return provisioning_session_hashed; +} + +static const char *calculate_service_access_information_hash(OpenAPI_service_access_information_resource_t *service_access_information) +{ + cJSON *service_access_info = NULL; + char *service_access_information_to_hash; + const char *service_access_information_hashed = NULL; + service_access_info = OpenAPI_service_access_information_resource_convertToJSON(service_access_information); + service_access_information_to_hash = cJSON_Print(service_access_info); + cJSON_Delete(service_access_info); + service_access_information_hashed = calculate_hash(service_access_information_to_hash); + ogs_free(service_access_information_to_hash); + return service_access_information_hashed; +} static int ogs_hash_do_cert_check(void *rec, const void *key, int klen, const void *value) @@ -619,6 +578,19 @@ free_ogs_hash_provisioning_session(void *rec, const void *key, int klen, const v return 1; } +static int +free_ogs_hash_provisioning_session_certificate(void *rec, const void *key, int klen, const void *value) +{ + free_ogs_hash_provisioning_session_certificate_t *fohpsc = (free_ogs_hash_provisioning_session_certificate_t *)rec; + if (!strcmp(fohpsc->certificate, (char *)key)) { + + ogs_hash_set(fohpsc->hash, key, klen, NULL); + ogs_free((void*)key); + + } + return 1; +} + static char* url_path_create(const char* macro, const char* session_id, const msaf_application_server_node_t *msaf_as) { @@ -667,3 +639,5 @@ tidy_relative_path_re(void) } } +/* vim:ts=8:sts=4:sw=4:expandtab: + */ diff --git a/src/5gmsaf/provisioning-session.h b/src/5gmsaf/provisioning-session.h index c960ba3..ad4a9ec 100644 --- a/src/5gmsaf/provisioning-session.h +++ b/src/5gmsaf/provisioning-session.h @@ -28,12 +28,24 @@ typedef struct msaf_provisioning_session_s { char *externalApplicationId; OpenAPI_content_hosting_configuration_t *contentHostingConfiguration; OpenAPI_service_access_information_resource_t *serviceAccessInformation; - ogs_hash_t *certificate_map; - ogs_list_t msaf_application_server_state_nodes; //Nodes for this list are msaf_application_server_state_node_t * + time_t provisioningSessionReceived; + char *provisioningSessionHash; + time_t contentHostingConfigurationReceived; + char *contentHostingConfigurationHash; + time_t serviceAccessInformationCreated; + char *serviceAccessInformationHash; + ogs_hash_t *certificate_map; + ogs_list_t application_server_states; //Type: msaf_application_server_state_ref_node_t * int marked_for_deletion; } msaf_provisioning_session_t; -extern msaf_provisioning_session_t *msaf_provisioning_session_create(char *provisioning_session_type, char *asp_id, char *external_app_id); +typedef struct msaf_application_server_state_node_s msaf_application_server_state_node_t; +typedef struct msaf_application_server_state_ref_node_s { + ogs_lnode_t node; + msaf_application_server_state_node_t *as_state; +} msaf_application_server_state_ref_node_t; + +extern msaf_provisioning_session_t *msaf_provisioning_session_create(const char *provisioning_session_type, const char *asp_id, const char *external_app_id); extern msaf_provisioning_session_t *msaf_provisioning_session_find_by_provisioningSessionId(const char *provisioningSessionId); extern cJSON *msaf_provisioning_session_get_json(const char *provisioning_session_id); @@ -48,18 +60,22 @@ extern ogs_list_t *msaf_retrieve_certificates_from_map(msaf_provisioning_session extern OpenAPI_content_hosting_configuration_t *msaf_content_hosting_configuration_with_af_unique_cert_id(msaf_provisioning_session_t *provisioning_session); -extern void msaf_delete_content_hosting_configuration(char * resource_id); +extern void msaf_delete_content_hosting_configuration(const char *provisioning_session_id); + +extern void msaf_delete_certificates(const char *provisioning_session_id); -extern void msaf_delete_certificate(char *resource_id); +extern void msaf_provisioning_session_hash_remove(const char *provisioning_session_id); -extern void msaf_provisioning_session_hash_remove(char *provisioning_session_id); +extern void msaf_provisioning_session_certificate_hash_remove(const char *provisioning_session_id, const char *certificate_id); -extern int uri_relative_check(char *entry_point_path); +extern int uri_relative_check(const char *entry_point_path); extern int msaf_distribution_create(cJSON *content_hosting_config, msaf_provisioning_session_t *provisioning_session); -extern cJSON *msaf_get_content_hosting_configuration_by_provisioning_session_id(char *provisioning_session_id); +extern cJSON *msaf_get_content_hosting_configuration_by_provisioning_session_id(const char *provisioning_session_id); + +extern char *enumerate_provisioning_sessions(void); #ifdef __cplusplus } diff --git a/src/5gmsaf/response-cache-control.c b/src/5gmsaf/response-cache-control.c new file mode 100644 index 0000000..42703fc --- /dev/null +++ b/src/5gmsaf/response-cache-control.c @@ -0,0 +1,35 @@ +/* +License: 5G-MAG Public License (v1.0) +Author: Dev Audsin +Copyright: (C) 2022 British Broadcasting Corporation + +For full license terms please see the LICENSE file distributed with this +program. If this file is missing then the license can be retrieved from +https://drive.google.com/file/d/1cinCiA778IErENZ3JN52VFW-1ffHpx7Z/view +*/ + +#include "context.h" +#include "response-cache-control.h" + +void msaf_server_response_cache_control_set(void) +{ + msaf_server_response_cache_control_t *server_response_cache_control = NULL; + server_response_cache_control = ogs_calloc(1, sizeof(msaf_server_response_cache_control_t)); + ogs_assert(server_response_cache_control); + server_response_cache_control->m1_provisioning_session_response_max_age = SERVER_RESPONSE_MAX_AGE; + server_response_cache_control->m1_content_hosting_configurations_response_max_age = SERVER_RESPONSE_MAX_AGE; + server_response_cache_control->m1_server_certificates_response_max_age = SERVER_RESPONSE_MAX_AGE; + server_response_cache_control->m1_content_protocols_response_max_age = M1_CONTENT_PROTOCOLS_RESPONSE_MAX_AGE; + server_response_cache_control->m5_service_access_information_response_max_age = SERVER_RESPONSE_MAX_AGE; + msaf_self()->config.server_response_cache_control = server_response_cache_control; +} + +void msaf_server_response_cache_control_set_from_config(int m1_provisioning_session_response_max_age, int m1_content_hosting_configurations_response_max_age, int m1_server_certificates_response_max_age, int m1_content_protocols_response_max_age, int m5_service_access_information_response_max_age) +{ + msaf_self()->config.server_response_cache_control->m1_provisioning_session_response_max_age = m1_provisioning_session_response_max_age; + msaf_self()->config.server_response_cache_control->m1_content_hosting_configurations_response_max_age = m1_content_hosting_configurations_response_max_age; + msaf_self()->config.server_response_cache_control->m1_server_certificates_response_max_age = m1_server_certificates_response_max_age; + msaf_self()->config.server_response_cache_control->m1_content_protocols_response_max_age = m1_content_protocols_response_max_age; + msaf_self()->config.server_response_cache_control->m5_service_access_information_response_max_age = m5_service_access_information_response_max_age; +} + diff --git a/src/5gmsaf/response-cache-control.h b/src/5gmsaf/response-cache-control.h new file mode 100644 index 0000000..ab680c1 --- /dev/null +++ b/src/5gmsaf/response-cache-control.h @@ -0,0 +1,39 @@ +/* +License: 5G-MAG Public License (v1.0) +Author: Dev Audsin +Copyright: (C) 2022 British Broadcasting Corporation + +For full license terms please see the LICENSE file distributed with this +program. If this file is missing then the license can be retrieved from +https://drive.google.com/file/d/1cinCiA778IErENZ3JN52VFW-1ffHpx7Z/view +*/ + +#ifndef MSAF_RESPONSE_CACHE_CONTROL_H +#define MSAF_RESPONSE_CACHE_CONTROL_H + +#include "ogs-app.h" + +#define SERVER_RESPONSE_MAX_AGE 60 +#define M1_CONTENT_PROTOCOLS_RESPONSE_MAX_AGE 86400 + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct msaf_server_response_cache_control_s { + int m1_provisioning_session_response_max_age; + int m1_content_hosting_configurations_response_max_age; + int m1_server_certificates_response_max_age; + int m1_content_protocols_response_max_age; + int m5_service_access_information_response_max_age; +}msaf_server_response_cache_control_t; + +extern void msaf_server_response_cache_control_set(void); +extern void msaf_server_response_cache_control_set_from_config(int m1_provisioning_session_response_max_age, int m1_content_hosting_configurations_response_max_age, int m1_server_certificates_response_max_age, int m1_content_protocols_response_max_age, int m5_service_access_information_response_max_age); + + +#ifdef __cplusplus +} +#endif + +#endif /* MSAF_RESPONSE_CACHE_CONTROL_H */ diff --git a/src/5gmsaf/sbi-path.c b/src/5gmsaf/sbi-path.c index 2c9ad9f..14e41dc 100644 --- a/src/5gmsaf/sbi-path.c +++ b/src/5gmsaf/sbi-path.c @@ -59,4 +59,4 @@ void msaf_sbi_close(void) { ogs_sbi_client_stop_all(); ogs_sbi_server_stop_all(); -} +} \ No newline at end of file diff --git a/src/5gmsaf/server.c b/src/5gmsaf/server.c new file mode 100644 index 0000000..19232db --- /dev/null +++ b/src/5gmsaf/server.c @@ -0,0 +1,243 @@ +/* +License: 5G-MAG Public License (v1.0) +Author: Dev Audsin +Copyright: (C) 2022 British Broadcasting Corporation + +For full license terms please see the LICENSE file distributed with this +program. If this file is missing then the license can be retrieved from +https://drive.google.com/file/d/1cinCiA778IErENZ3JN52VFW-1ffHpx7Z/view +*/ + +#include "ogs-sbi.h" +#include "server.h" +#include "utilities.h" +#include "msaf-version.h" + +static bool nf_server_send_problem( + ogs_sbi_stream_t *stream, OpenAPI_problem_details_t *problem, const nf_server_interface_metadata_t *interface, const nf_server_app_metadata_t *app); + +static ogs_sbi_response_t *nf_build_response( + ogs_sbi_message_t *message, int status, const nf_server_interface_metadata_t *interface, const nf_server_app_metadata_t *app); + +static bool nf_build_content( + ogs_sbi_http_message_t *http, ogs_sbi_message_t *message); + +static char *nf_build_json(ogs_sbi_message_t *message); + +ogs_sbi_response_t *nf_server_new_response(char *location, char *content_type, time_t last_modified, char *etag, int cache_control, char *allow_methods, const nf_server_interface_metadata_t *interface, const nf_server_app_metadata_t *app) +{ + ogs_sbi_response_t *response = NULL; + char *server_api_info = ((char*)""); + char *server = NULL; + + response = ogs_sbi_response_new(); + ogs_expect_or_return_val(response, NULL); + + if(content_type) + { + ogs_sbi_header_set(response->http.headers, "Content-Type", content_type); + } + + if(location) + { + ogs_sbi_header_set(response->http.headers, "Location", location); + } + + if(last_modified) + { + + ogs_sbi_header_set(response->http.headers, "Last-Modified", get_time(last_modified)); + } + + if(etag) + { + + ogs_sbi_header_set(response->http.headers, "ETag", etag); + } + + if(cache_control) + { + char *response_cache_control; + response_cache_control = ogs_msprintf("max-age=%d", cache_control); + ogs_sbi_header_set(response->http.headers, "Cache-Control", response_cache_control); + ogs_free(response_cache_control); + } + + if(allow_methods) + { + + ogs_sbi_header_set(response->http.headers, "Allow", allow_methods); + } + + + if (interface) { + server_api_info = ogs_msprintf(" (info.title=%s; info.version=%s)", interface->api_title, interface->api_version); + } + server = ogs_msprintf("%s/%s%s %s/%s",app->server_name, FIVEG_API_RELEASE, server_api_info, app->app_name, app->app_version); + if (interface) { + ogs_free(server_api_info); + } + ogs_sbi_header_set(response->http.headers, "Server", server); + ogs_free(server); + return response; + + +} + +ogs_sbi_response_t *nf_server_populate_response(ogs_sbi_response_t *response, int content_length, char *content, int status) +{ + response->http.content_length = content_length; + response->http.content = content; + response->status = status; + return response; + +} + +static bool nf_server_send_problem( + ogs_sbi_stream_t *stream, OpenAPI_problem_details_t *problem, const nf_server_interface_metadata_t *interface, const nf_server_app_metadata_t *app) +{ + ogs_sbi_message_t message; + ogs_sbi_response_t *response = NULL; + + ogs_assert(stream); + ogs_assert(problem); + + memset(&message, 0, sizeof(message)); + + message.http.content_type = (char*)"application/problem+json"; + message.ProblemDetails = problem; + + response = nf_build_response(&message, problem->status, interface, app); + ogs_assert(response); + + ogs_sbi_server_send_response(stream, response); + + return true; +} + + +bool nf_server_send_error(ogs_sbi_stream_t *stream, + int status, int number_of_components, ogs_sbi_message_t *message, + const char *title, const char *detail, cJSON * problem_detail, const nf_server_interface_metadata_t *interface, const nf_server_app_metadata_t *app) +{ + OpenAPI_problem_details_t problem; + OpenAPI_problem_details_t *problem_details = NULL; + + ogs_assert(stream); + + memset(&problem, 0, sizeof(problem)); + + if(problem_detail) { + problem_details = OpenAPI_problem_details_parseFromJSON(problem_detail); + problem.invalid_params = problem_details->invalid_params; + + } + + + if (message) { + int i; + problem.type = ogs_msprintf("/%s/%s", + message->h.service.name, message->h.api.version); + ogs_expect_or_return_val(problem.type, false); + + problem.instance = ogs_msprintf("/%s", message->h.resource.component[0]); + + for (i = 1; i <= number_of_components; i++) + { + ogs_free(problem.instance); + problem.instance = ogs_msprintf("%s/%s", problem.instance, message->h.resource.component[i]); + + } + ogs_expect_or_return_val(problem.instance, NULL); + } + if (status) { + problem.is_status = true; + problem.status = status; + } + problem.title = (char*)title; + problem.detail = (char*)detail; + + nf_server_send_problem(stream, &problem, interface, app); + + if (problem.type) + ogs_free(problem.type); + if (problem.instance) + ogs_free(problem.instance); + if (problem.invalid_params) + OpenAPI_problem_details_free(problem_details); + + return true; +} + +ogs_sbi_response_t *nf_build_response( + ogs_sbi_message_t *message, int status, const nf_server_interface_metadata_t *interface, const nf_server_app_metadata_t *app) +{ + ogs_sbi_response_t *response = NULL; + + ogs_assert(message); + + response = nf_server_new_response(NULL, NULL, 0, NULL, 0, NULL, interface, app); + + //response = ogs_sbi_response_new(); + ogs_expect_or_return_val(response, NULL); + + response->status = status; + + if (response->status != OGS_SBI_HTTP_STATUS_NO_CONTENT) { + ogs_expect_or_return_val(true == + nf_build_content(&response->http, message), NULL); + } + + if (message->http.location) { + ogs_sbi_header_set(response->http.headers, "Location", + message->http.location); + } + if (message->http.cache_control) + ogs_sbi_header_set(response->http.headers, "Cache-Control", + message->http.cache_control); + + return response; +} + +static bool nf_build_content( + ogs_sbi_http_message_t *http, ogs_sbi_message_t *message) +{ + ogs_assert(message); + ogs_assert(http); + + http->content = nf_build_json(message); + if (http->content) { + http->content_length = strlen(http->content); + if (message->http.content_type) { + ogs_sbi_header_set(http->headers, + OGS_SBI_CONTENT_TYPE, message->http.content_type); + } else { + ogs_sbi_header_set(http->headers, + OGS_SBI_CONTENT_TYPE, OGS_SBI_CONTENT_JSON_TYPE); + } + } + + return true; +} + +static char *nf_build_json(ogs_sbi_message_t *message) +{ + char *content = NULL; + cJSON *item = NULL; + + ogs_assert(message); + + if (message->ProblemDetails) { + item = OpenAPI_problem_details_convertToJSON(message->ProblemDetails); + ogs_assert(item); + } + if (item) { + content = cJSON_Print(item); + ogs_assert(content); + ogs_log_print(OGS_LOG_TRACE, "%s", content); + cJSON_Delete(item); + } + + return content; +} + diff --git a/src/5gmsaf/server.h b/src/5gmsaf/server.h new file mode 100644 index 0000000..60da40c --- /dev/null +++ b/src/5gmsaf/server.h @@ -0,0 +1,43 @@ +/* +License: 5G-MAG Public License (v1.0) +Author: Dev Audsin +Copyright: (C) 2022 British Broadcasting Corporation + +For full license terms please see the LICENSE file distributed with this +program. If this file is missing then the license can be retrieved from +https://drive.google.com/file/d/1cinCiA778IErENZ3JN52VFW-1ffHpx7Z/view + */ + +#ifndef NF_SERVER_H +#define NF_SERVER_H + + #include "context.h" + + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct nf_server_interface_metadata_s { + const char *api_title; + const char *api_version; +} nf_server_interface_metadata_t; + +typedef struct nf_server_app_metadata_s { + const char *app_name; + const char *app_version; + const char *server_name; +} nf_server_app_metadata_t; + +extern bool nf_server_send_error(ogs_sbi_stream_t *stream, + int status, int number_of_components, ogs_sbi_message_t *message, + const char *title, const char *detail, cJSON * problem_detail, const nf_server_interface_metadata_t *interface, const nf_server_app_metadata_t *app); + +extern ogs_sbi_response_t *nf_server_new_response(char *location, char *content_type, time_t last_modified, char *etag, int cache_control, char *allow_methods, const nf_server_interface_metadata_t *interface, const nf_server_app_metadata_t *app); +extern ogs_sbi_response_t *nf_server_populate_response(ogs_sbi_response_t *response, int content_length, char *content, int status); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/5gmsaf/utilities.c b/src/5gmsaf/utilities.c index a2ad33c..b0831ac 100644 --- a/src/5gmsaf/utilities.c +++ b/src/5gmsaf/utilities.c @@ -8,13 +8,42 @@ program. If this file is missing then the license can be retrieved from https://drive.google.com/file/d/1cinCiA778IErENZ3JN52VFW-1ffHpx7Z/view */ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + #include #include #include #include #include +#include +#include +#include + #include "utilities.h" +time_t str_to_time(const char *str_time) +{ + static time_t time; + struct tm tm = {0}; + strptime(str_time, "%a, %d %b %Y %H:%M:%S %Z", &tm); + time = mktime(&tm); + return time; +} + +const char *get_time(time_t time_epoch) +{ + struct tm *ts; + static char buf[80]; + + /* Format and print the time, "ddd yyyy-mm-dd hh:mm:ss zzz" */ + ts = localtime(&time_epoch); + strftime(buf, sizeof(buf), "%a, %d %b %Y %H:%M:%S %Z", ts); + + return buf; +} + char *read_file(const char *filename) { FILE *f = NULL; @@ -40,6 +69,22 @@ char *read_file(const char *filename) } +int str_match(const char *line, const char *word_to_find) { + + char* p = strstr(line,word_to_find); + if ((p==line) || (p!=NULL && !isalnum((unsigned char)p[-1]))) + { + p += strlen(word_to_find); + if (!isalnum((unsigned char)*p)) + { + return 1; + } else { + return 0; + } + } + return 0; +} + char *get_path(const char *file) { char *path = NULL; @@ -82,5 +127,17 @@ long int ascii_to_long(const char *str) return ret; } +uint16_t ascii_to_uint16(const char *str) +{ + long int ret; + ret = ascii_to_long(str); + if (ret > UINT16_MAX) + { + ogs_error("[%s] cannot be greater than [%d]", str, UINT16_MAX); + ret = 0; + } + return ret; +} + /* vim:ts=8:sts=4:sw=4:expandtab: */ diff --git a/src/5gmsaf/utilities.h b/src/5gmsaf/utilities.h index ccd45e9..eae8457 100644 --- a/src/5gmsaf/utilities.h +++ b/src/5gmsaf/utilities.h @@ -1,20 +1,31 @@ /* -License: 5G-MAG Public License (v1.0) -Author: Dev Audsin -Copyright: (C) 2022 British Broadcasting Corporation - -For full license terms please see the LICENSE file distributed with this -program. If this file is missing then the license can be retrieved from -https://drive.google.com/file/d/1cinCiA778IErENZ3JN52VFW-1ffHpx7Z/view -*/ + * License: 5G-MAG Public License (v1.0) + * Author: Dev Audsin + * Copyright: (C) 2022 British Broadcasting Corporation + * + * For full license terms please see the LICENSE file distributed with this + * program. If this file is missing then the license can be retrieved from + * https://drive.google.com/file/d/1cinCiA778IErENZ3JN52VFW-1ffHpx7Z/view + */ #ifndef MSAF_UTILITIES_H #define MSAF_UTILITIES_H +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif +#include + +#ifndef _XOPEN_SOURCE +#define _XOPEN_SOURCE +#endif + #include #include #include +#include #include "ogs-app.h" +#include "context.h" #ifdef __cplusplus extern "C" { @@ -24,6 +35,10 @@ extern char *read_file(const char *filename); extern char *get_path(const char *file); extern char *rebase_path(const char *base, const char *file); extern long int ascii_to_long(const char *str); +extern uint16_t ascii_to_uint16(const char *str); +extern int str_match(const char *line, const char *word_to_find); +extern const char *get_time(time_t time_epoch); +extern time_t str_to_time(const char *str_time); #ifdef __cplusplus } diff --git a/subprojects/patch_open5gs.sh b/subprojects/patch_open5gs.sh index d5012d5..5f90519 100755 --- a/subprojects/patch_open5gs.sh +++ b/subprojects/patch_open5gs.sh @@ -18,9 +18,11 @@ patch_cmd=`which patch` if ! grep -q '/\* rt-5gms-applicatiopn-function patch applied \*/' "$open5gs_src/lib/sbi/server.c"; then (cd "$open5gs_src"; "$patch_cmd" -p1 < +# David Waring +# Copyright: ©2023 British Broadcasting Corporation +# +# For full license terms please see the LICENSE file distributed with this +# program. If this file is missing then the license can be retrieved from +# https://drive.google.com/file/d/1cinCiA778IErENZ3JN52VFW-1ffHpx7Z/view +# +# This is an example script that manages locally generated self-signed +# certificates on behalf of the 5GMS Application Function. +# + +# Save location of this script and the name it was called by +scriptname=`basename "$0"` +scriptdir=`dirname "$0"` +scriptdir=`cd "$scriptdir"; pwd` + +# Constants +default_cert_op= +cert_store="@prefix@/@localstatedir@/cache/rt-5gms/af/certificates" + +# Variables +cert_operation= +common_name= +server_certificate_resource_id= +cert_status= +cert_subject= +cache_control_max_age=70 + +print_syntax() { + echo "Syntax: $scriptname -h" + echo " $scriptname -c [...]" +} + +# Parse command line arguments +ARGS=`getopt -n "$scriptname" -o 'c:h' -l 'cert-operation:,help' -s sh -- "$@"` + +if [ $? -ne 0 ]; then + print_syntax >&2 + exit 1 +fi + +debug() { + #echo "$@" >&2 + true; +} + +error() { + echo "$@" >&2 +} + +print_help() { + cat < + + certificate-id The certificate ID to create a new key and certificate + signing request. + +newcert parameters: + syntax: newcert + + certificate-id The certificate ID to create a new key and public + certificate. + +publiccert parameters: + syntax: publiccert + + certificate-id The certificate ID to fetch the public certificate for. + +servercert parameters: + syntax: servercert + + certificate-id The certificate ID to fetch the private key and + certificate for. + +setcert parameters: + syntax: setcert < + + certificate-id The certificate ID for which the public certificate is + being uploaded. + +revoke parameters: + syntax: revoke + + certificate-id The certificate ID to revoke. + +delete parameters: + syntax: delete + + certificate-id The certificate ID to delete. + +list parameters: + syntax: list [...] +EOF +} + +eval set -- "$ARGS" +unset ARGS + +CERTOPS="$default_cert_op" + +while true; do + case "$1" in + -c|--cert-operation) + CERTOPS="$2" + shift 2 + continue + ;; + -h|--help) + print_help + exit 0 + ;; + --) + shift + break + ;; + *) + echo "Error: Command line argument \"$1\" unexpected" >&2 + print_syntax >&2 + exit 1 + ;; + esac +done + +if [ -z "$CERTOPS" ]; then + error 'Error: Required command line parameter are missing' + print_syntax >&2 + exit 1 +fi + +error_exit() { + exit_code="$1" + shift + error "$@" + exit "$exit_code" +} + +cert_store_create() { + if [ ! -d "$cert_store/csrs" ]; then + mkdir -p "$cert_store/csrs" + fi + + if [ ! -d "$cert_store/private" ]; then + mkdir -p "$cert_store/private" + fi + + if [ ! -d "$cert_store/public" ]; then + mkdir -p "$cert_store/public" + fi +} + +cert_store_check() { + cert_id="$1" + if [ -f "$cert_store/csrs/$cert_id.pem" ]; then + error_exit 3 "CSR for Server Certificate Resource $cert_id exists already" + fi + + if [ -f "$cert_store/public/$cert_id.pem" ]; then + error_exit 3 "Certificate for Server Certificate Resource $cert_id exists already" + fi +} + +new_csr() { + cert_id="$1" + common_name="$2" + cert_store_check "$cert_id" + + openssl req -new -newkey rsa:2048 -batch -nodes -keyout "$cert_store/private/$cert_id.pem" -out "$cert_store/csrs/$cert_id.pem" -subj "/C=GB/L=London/CN=$common_name" -addext "subjectAltName=DNS:$common_name" > /dev/null 2>&1 + + ts=`stat -c '%Y' "$cert_store/csrs/$cert_id.pem"` + timestamp=`TZ=GMT date --date=@$ts +'%a, %d %b %Y %H:%M:%S %Z'` + hashsum=$(sha256sum "$cert_store/csrs/$cert_id.pem"|sed 's/ .*//') + + echo "Last-Modified: $timestamp" + echo "ETag: $hashsum" + echo "Cache-Control: max-age=$cache_control_max_age" + cat "$cert_store/csrs/$cert_id.pem" + + exit 0 + +} + +new_cert() { + cert_id="$1" + common_name="$2" + + cert_store_check "$cert_id" + + # Generate server cert to be signed + openssl req -new -nodes -x509 -days 90 -newkey rsa:2048 -keyout "$cert_store/private/$cert_id.pem" -out "$cert_store/public/$cert_id.pem" -subj "/C=GB/L=London/CN=$common_name" -addext "subjectAltName=DNS:$common_name"> /dev/null 2>&1 + + ts=`stat -c '%Y' "$cert_store/public/$cert_id.pem"` + timestamp=`TZ=GMT date --date=@"$ts" +'%a, %d %b %Y %H:%M:%S %Z'` + hashsum=$(sha256sum "$cert_store/public/$cert_id.pem" | sed 's/ .*//') + + echo "Last-Modified: $timestamp" + echo "ETag: $hashsum" + echo "Cache-Control: max-age=$cache_control_max_age" + cat "$cert_store/public/$cert_id.pem" + + exit 0 +} + +public_cert_get() { + cert_id="$1" + if ([ -f "$cert_store/csrs/$cert_id.pem" ] && ! [ -f "$cert_store/public/$cert_id.pem" ]); then + error_exit 8 "Public Certificate for $cert_id not yet available." + fi + + if [ ! -f "$cert_store/public/$cert_id.pem" ]; then + error_exit 4 "Certificate for $cert_id not found" + fi + + ts=`stat -c '%Y' "$cert_store/public/$cert_id.pem"` + timestamp=`TZ=GMT date --date=@$ts +'%a, %d %b %Y %H:%M:%S %Z'` + hashsum=$(sha256sum "$cert_store/public/$cert_id.pem"|sed 's/ .*//') + + echo "Last-Modified: $timestamp" + echo "ETag: $hashsum" + echo "Cache-Control: max-age=$cache_control_max_age" + cat "$cert_store/public/$cert_id.pem" + + exit 0 +} + +server_cert_get() { + cert_id="$1" + if [[ ! -f "$cert_store/public/$cert_id.pem" || ! -f "$cert_store/private/$cert_id.pem" ]]; then + error_exit 8 "Credentials for $cert_id not yet available." + fi + + ts=$(stat -c '%Y' "$cert_store/public/$cert_id.pem") + timestamp=$(TZ=GMT date --date=@$ts +'%a, %d %b %Y %H:%M:%S %Z') + hashsum=$(sha256sum "$cert_store/public/$cert_id.pem"|sed 's/ .*//') + + echo "Last-Modified: $timestamp" + echo "ETag: $hashsum" + echo "Cache-Control: max-age=$cache_control_max_age" + cat "$cert_store/public/$cert_id.pem" + echo "" + cat "$cert_store/private/$cert_id.pem" + + exit 0 +} + +cert_set() { + cert_id="$1" + if [[ ! -f "$cert_store/csrs/$cert_id.pem" ]]; then + error_exit 4 "No CSR issued: $cert_id." + fi + + if [[ -f "$cert_store/public/$cert_id.pem" ]]; then + error_exit 3 "Certificate already exists for $cert_id." + fi + + cat > "$cert_store/public/$cert_id.pem" + exit 0 +} + +cert_expiry_check() { + cert_id="$1" + if openssl x509 -checkend 86400 -noout -in "$cert_store/public/$cert_id.pem" >/dev/null 2>&1; then + cert_status= + else + cert_status="Expired or will expire within 24 hours" + fi +} + +cert_list_entry() { + cert_id="$1" + if [[ -f "$cert_store/private/$cert_id.pem" && ! -f "$cert_store/public/$cert_id.pem" ]]; then + cert_status="Awaiting" + cert_subject= + elif [[ -f "$cert_store/public/$cert_id.pem" ]]; then + cert_expiry_check "$cert_id" >&2 + cert_subject=$(openssl x509 -noout -subject -in "$cert_store/public/$cert_id.pem") + fi + echo -e "${cert_id}\t${cert_subject}\t${cert_status}" +} + +cert_list() { + if [[ $# -eq 0 ]]; then + eval set -- $(cd "$cert_store/private"; ls *.pem 2>/dev/null | sed 's/\.pem$//') + fi + for cert in "$@"; do + cert_list_entry "$cert" + done +} + +check_cert_revoke() { + cert_id="$1" + if [[ -f "$cert_store/csr/$cert_id.pem" ]]; then + can_be_revoked=0 + error "Cannot revoke as $cert_id.pem is an externally signed certificate" + else + can_be_revoked=0 + error "Cannot revoke as $cert_id.pem is a self-signed certificate" + fi + #issuer=$(openssl x509 -in "$cert_store/public/$cert_id.pem" -inform PEM -noout -issuer | sed 's/issuer=//') + #subject=$(openssl x509 -in "$cert_store/public/$cert_id.pem" -inform PEM -noout -subject | sed 's/subject=//') + #if [ "$issuer" = "$subject" ]; then + # can_be_revoked=0 + # error "Cannot revoke as $cert_id.pem is a self-signed certificate" + #else + # can_be_revoked=1 + #fi +} + +cert_revoke() { + cert_id="$1" + check_cert_revoke "$cert_id" + debug "in cert_revoke: $can_be_revoked" + if [ "$can_be_revoked" -eq "0" ] ; then + + debug "in cert_revoke:exit 2 $can_be_revoked" + exit 2 + fi + + #openssl ca -revoke "$cert_store/public/$cert_id.pem" -keyfile "$cert_store/private/ca.key" -cert "$cert_store/public/ca.crt" + exit 0 +} + +cert_delete() { + cert_id="$1" + rm -f "$cert_store/private/$cert_id.pem" + rm -f "$cert_store/public/$cert_id.pem" + rm -f "$cert_store/csrs/$cert_id.pem" +} + +certificate_delete() { + cert_id="$1" + check_cert_revoke "$cert_id" + if [ "$can_be_revoked" -ne 0 ] ; then + cert_revoke "$cert_id" + fi + cert_delete "$cert_id" + exit 0 +} + +parse_opts() { + if [ "$CERTOPS" == "newcsr" ]; then + if [ $# -ne 2 ]; then + echo "$CERTOPS: Wrong parameters to create a new csr" + exit 1 + fi + new_csr "$@" + exit 1 + fi + + if [ "$CERTOPS" == "newcert" ]; then + if [ $# -ne 2 ]; then + echo "$CERTOPS: Wrong parameters to create a new certificate" + exit 1 + fi + new_cert "$@" + exit 1 + fi + + if [ "$CERTOPS" == "publiccert" ]; then + if [[ $# -ne 1 ]]; then + error_exit 1 "$CERTOPS: has invalid options" + fi + public_cert_get "$@" + exit 1 + fi + + if [ "$CERTOPS" == "servercert" ]; then + if [ $# -ne 1 ]; then + error_exit 1 "$CERTOPS: has invalid options" + fi + server_cert_get "$@" + exit 1 + fi + + if [ "$CERTOPS" == "setcert" ]; then + if [ $# -ne 1 ]; then + error_exit 1 "$CERTOPS: has invalid options" + fi + cert_set "$@" + exit 1 + fi + + if [ "$CERTOPS" == "revoke" ]; then + if [ $# -ne 1 ]; then + error_exit 1 "$CERTOPS: has invalid options" + fi + cert_revoke "$@" + exit 1 + fi + + if [ "$CERTOPS" == "delete" ]; then + if [ $# -ne 1 ]; then + error_exit 1 "$CERTOPS: has invalid options" + fi + certificate_delete "$@" + exit 1 + fi + + if [ "$CERTOPS" == "list" ]; then + cert_list "$@" + fi +} + +cert_store_create +parse_opts "$@" + +exit 0 diff --git a/tools/meson.build b/tools/meson.build new file mode 100644 index 0000000..f52fe3d --- /dev/null +++ b/tools/meson.build @@ -0,0 +1,45 @@ +pymod = import('python') +fs = import('fs') + +support_scripts_dir = get_option('libexecdir') / 'rt-5gms' + +scripts = { +#'python3/m1_client_cli.py': 'm1-client', +'python3/m1_session_cli.py': 'm1-session' +} + +support_scripts = { +'bash/certmgr': 'self-signed-certmgr' +} + +self_signed_certmgr_runtime = support_scripts_dir / 'self-signed-certmgr' + +python3_modules = [ + 'python3/lib/rt_m1_client', +] + +scripts_conf_data = configuration_data() +script_conf_options = [ + 'prefix', 'bindir', 'libdir', 'libexecdir', 'localstatedir', 'sbindir', + 'sysconfdir', + ] +foreach opt : script_conf_options + scripts_conf_data.set(opt, get_option(opt)) +endforeach + +foreach src, dst : scripts + scriptfile = configure_file(input: src, configuration: scripts_conf_data, output: dst) + install_data(scriptfile, install_dir: get_option('bindir'), install_mode: 'rwxr-xr-x') +endforeach + +foreach src, dst : support_scripts + scriptfile = configure_file(input: src, configuration: scripts_conf_data, output: dst) + install_data(scriptfile, install_dir: support_scripts_dir, install_mode: 'rwxr-xr-x') +endforeach + +python3 = pymod.find_installation('python3') +sh = find_program('sh') +foreach pm : python3_modules + mod_files = files(run_command(sh, '-c', 'cd "$MESON_SOURCE_ROOT/$MESON_SUBDIR"; find "' + pm + '" -type f -name "*.py" -print').stdout().strip().split('\n')) + python3.install_sources(mod_files, subdir: fs.name(pm)) +endforeach diff --git a/tools/python3/lib/rt_m1_client/certificates.py b/tools/python3/lib/rt_m1_client/certificates.py new file mode 100644 index 0000000..cf566fd --- /dev/null +++ b/tools/python3/lib/rt_m1_client/certificates.py @@ -0,0 +1,216 @@ +#!/usr/bin/python3 +#============================================================================== +# 5G-MAG Reference Tools: M1 Client Certificate Signing +#============================================================================== +# +# File: rt_m1_client/certificates.py +# License: 5G-MAG Public License (v1.0) +# Author: David Waring +# Copyright: (C) 2023 British Broadcasting Corporation +# +# For full license terms please see the LICENSE file distributed with this +# program. If this file is missing then the license can be retrieved from +# https://drive.google.com/file/d/1cinCiA778IErENZ3JN52VFW-1ffHpx7Z/view +# +#============================================================================== +# +# M1 Session Certificate Signing +# ============================= +# +# This module defines classes used by the M1 session classes to sign certificates +# +# CertificateSigner - Base class for certificate signing. +# +# LocalCACertificateSigner - A CertificateSigner that uses a locally generated CA to generate X509 certificates from CSRs +# +# DefaultCertificateSigner - The default CertificateSigner used by the M1Session class, presently LocalCACertificateSigner. +# +''' +====================================================== +5G-MAG Reference Tools: M1 Session Certificate Signing +====================================================== + +This module defines some classes that can be used by the `M1Session` class to provide certificate signing services. + +''' +from typing import Optional, Tuple + +import OpenSSL + +from .data_store import DataStore + +class CertificateSigner: + '''Base class for all CertificateSigner classes + ''' + def __init__(self, *args, data_store: Optional[DataStore] = None, **kwargs): + self.data_store = data_store + + def __await__(self): + '''Await method + + This allows the class to be instantiated with asynchronous initialisation, e.g.:: + cert_signer = await MyCertificateSigner() + + This will call the async method `asyncInit` to perform the asynchronous initialisation operations. + ''' + return self.asyncInit().__await__() + + async def asyncInit(self): + '''Asynchronous object initialisation + + Derived classes should override this if they have object initialisation to do that requires async operations. + + This async method must return self. + ''' + return self + + async def signCertificate(self, csr: str, *args, domain_name_alias: Optional[str] = None, **kwargs) -> str: + '''Sign a CSR in PEM format and return the public X509 Certificate in PEM format + + :param str csr: A CSR in PEM format. + :param str domain_name_alias: Optional domain name to add to the subjectAltNames in the final certificate. + + :return: a public X509 certificate in PEM format. + ''' + raise NotImplementedError('Class derived from CertificateSigner must implement this method') + +class LocalCACertificateSigner(CertificateSigner): + '''CertificateSigner that uses a locally generated CA kept in the data store + ''' + + def __init__(self, *args, data_store: Optional[DataStore] = None, local_ca_days: int = 365, temp_ca_days: int = 1, local_cert_days: int = 30, **kwargs): + '''Constructor + + Create a CertificateSigner that uses a locally generated CA to sign certificates. + + :param DataStore data_store: The DataStore to use to persist the CA key and certificate. + :param int local_ca_days: The number of days before expiry of the local CA in the data store. + :param int temp_ca_days: The number of days for the local CA if no DataStore is provided for persistence. + :param int local_cert_days: The number of days before expiry of signed certificates. + ''' + super().__init__(self, data_store=data_store) + self.__ca_key: Optional[OpenSSL.crypto.PKey] = None + self.__ca: Optional[OpenSSL.crypto.X509] = None + self.__local_ca_days: int = local_ca_days + self.__temp_ca_days: int = temp_ca_days + self.__local_cert_days: int = local_cert_days + + async def signCertificate(self, csr: str, *args, domain_name_alias: Optional[str] = None, **kwargs) -> str: + '''Sign a CSR in PEM format and return the public X509 Certificate in PEM format + + This will generate a public certificate from the *csr*, which is signed by the locally generated CA. + The certificate will have subjectAltNames defined for the SANs in the *csr*, the commonName and optionally the + *domain_name_alias*. The certificate will expire in the number of days indicated by the *local_cert_days* parameter + when an instance of this class was created. + + :param str csr: A CSR in PEM format. + :param str domain_name_alias: Optional domain name to add to the subjectAltNames in the final certificate. + + :return: a public X509 certificate in PEM format. + ''' + x509req: OpenSSL.crypto.X509Req = OpenSSL.crypto.load_certificate_request(OpenSSL.crypto.FILETYPE_PEM, csr.encode('utf-8')) + # Adjust SANs + sans: List[bytes] = [] + if domain_name_alias is not None: + sans += [b'DNS:'+domain_name_alias.encode('utf-8')] + sans += [b'DNS:'+x509req.get_subject().commonName.encode('utf-8')] + # Get local CA + ca_key, ca = await self.__getLocalCA() + # Convert CSR to X509 certificate + x509 = OpenSSL.crypto.X509() + if domain_name_alias is not None: + new_subj = x509.get_subject() + new_subj.commonName = domain_name_alias + new_subj.organizationName = '5G-MAG' + else: + x509.set_subject(x509req.get_subject()) + x509.set_serial_number(1) + x509.gmtime_adj_notBefore(0) + x509.gmtime_adj_notAfter(self.__local_cert_days * 24 * 60 * 60) + x509.set_issuer(ca.get_subject()) + x509.set_pubkey(x509req.get_pubkey()) + # Copy any extensions we aren't replacing + for ext in x509req.get_extensions(): + if ext.get_short_name() not in [b'subjectKeyIdentifier', b'authorityKeyIdentifier', b'basicConstraints', b'subjectAltName']: + x509.add_extensions([ext]) + x509.add_extensions([ + OpenSSL.crypto.X509Extension(b'subjectKeyIdentifier', False, b'hash', subject=x509), + OpenSSL.crypto.X509Extension(b'authorityKeyIdentifier', False, b'keyid, issuer', issuer=ca), + OpenSSL.crypto.X509Extension(b'basicConstraints', True, b'CA:FALSE'), + OpenSSL.crypto.X509Extension(b'subjectAltName', False, b','.join(sans)) + ]) + x509.sign(ca_key, "sha256") + return OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, x509).decode('utf-8') + + async def __makeCACert(self, key: OpenSSL.crypto.PKey, cn: str, days: int = 365) -> OpenSSL.crypto.X509: + '''Make a CA certificate + + The CA certificate will use the provided *key* for its public key (if a private key is provided the pubilc key will be + extracted). The *cn* parameter defines the common name for the CA certificate. The *days* parameter is used to set the + expiry date on the CA certificate. + + :meta private: + :param OpenSSL.crypto.PKey key: A public or private key to use for the public key of the CA certificate. + :param str cn: The commonName for the certificate subject and issuer. + :param int days: The number of days the CA certificate will be valid for. + + :return: a self signed X509 CA certificate. + :rtype: OpenSSL.crypto.X509 + ''' + ca = OpenSSL.crypto.X509() + ca_name = ca.get_subject() + # TODO: Get these values from configured values + ca_name.organizationName = '5G-MAG' + ca_name.commonName = cn + ca.set_issuer(ca_name) + # TODO: increment serial number from data-store + ca.set_serial_number(1) + ca.gmtime_adj_notBefore(0) + ca.gmtime_adj_notAfter(days*24*60*60) + ca.set_pubkey(key) + ca.add_extensions([ + OpenSSL.crypto.X509Extension(b'basicConstraints', True, b'CA:TRUE,pathlen:1'), + OpenSSL.crypto.X509Extension(b'subjectKeyIdentifier', False, b'hash', subject=ca), + OpenSSL.crypto.X509Extension(b'authorityKeyIdentifier', False, b'keyid, issuer:always', issuer=ca), + ]) + ca.sign(key, 'sha256') + return ca + + async def __getLocalCA(self) -> Tuple[OpenSSL.crypto.PKey, OpenSSL.crypto.X509]: + '''Get the locally generated CA + + This will create the locally generated CA if it doesn't already exist. + + :meta private: + :return: the CA key and CA public certificate. + :rtype: Tuple[OpenSSL.crypto.PKey, OpenSSL.crypto.X509] + ''' + if self.__ca_key is None or self.__ca is None: + if self.data_store: + ca_key_pem = await self.data_store.get('ca-private') + if ca_key_pem is not None: + self.__ca_key = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, ca_key_pem) + else: + self.__ca_key = OpenSSL.crypto.PKey() + self.__ca_key.generate_key(OpenSSL.crypto.TYPE_RSA, 4096) + await self.data_store.set('ca-private', OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, self.__ca_key).decode('utf-8')) + ca_pem = await self.data_store.get('ca-public') + if ca_pem is not None: + self.__ca = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, ca_pem) + else: + self.__ca = await self.__makeCACert(self.__ca_key, '5G-MAG Reference Tools Local CA', days=self.__local_ca_days) + await self.data_store.set('ca-public', OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, self.__ca).decode('utf-8')) + else: + self.__ca_key = OpenSSL.crypto.PKey() + self.__ca_key.generate_key(OpenSSL.crypto.TYPE_RSA, 2048) + self.__ca = await self.__makeCACert(self.__ca_key, 'Temporary Demo CA', days=self.__temp_ca_days) + + return self.__ca_key, self.__ca + +DefaultCertificateSigner = LocalCACertificateSigner #: The default CertificateSigner + +__all__ = [ + "CertificateSigner", + "LocalCACertificateSigner", + "DefaultCertificateSigner", + ] diff --git a/tools/python3/lib/rt_m1_client/client.py b/tools/python3/lib/rt_m1_client/client.py new file mode 100644 index 0000000..36117de --- /dev/null +++ b/tools/python3/lib/rt_m1_client/client.py @@ -0,0 +1,665 @@ +#!/usr/bin/python3 +#============================================================================== +# 5G-MAG Reference Tools: M1 Client +#============================================================================== +# +# File: m1_client/client.py +# License: 5G-MAG Public License (v1.0) +# Author: David Waring +# Copyright: (C) 2022 British Broadcasting Corporation +# +# For full license terms please see the LICENSE file distributed with this +# program. If this file is missing then the license can be retrieved from +# https://drive.google.com/file/d/1cinCiA778IErENZ3JN52VFW-1ffHpx7Z/view +# +#============================================================================== +# +# M1 Client class +# =============== +# +# This module contains an M1 Client written as a Python 3 class using asyncio. +# +'''5G-MAG Reference Tools: M1 Client class +======================================= + +This class provides a simple interface for maintaining a connection to an M1 +server, converting Python types to the various M1 requests, parsing the +responses and conversion back to Python types or Exceptions when errors have +occurred. This class will ensure that the out going request headers are +formatted according to 3GPP TS 26.512. Message bodies are passed through as +is and therefore it is the responsibility of the application using this class +to format the body correctly. + +This class is not intended to maintain client state for an M1 session, that +should be performed outside of this class. +''' +import datetime +import json +import logging +from typing import Optional, Union, Tuple, Dict, Any, TypedDict + +import httpx + +from .exceptions import (M1ClientError, M1ServerError) +from .types import (ApplicationId, ContentHostingConfiguration, ContentProtocols, + ProvisioningSessionType, ProvisioningSession, ResourceId) + +class TagAndDateResponse(TypedDict, total=False): + '''Response containing ETag and Last-Modified headers + ''' + ETag: str + LastModified: datetime.datetime + +class ProvisioningSessionResponse(TagAndDateResponse, total=False): + '''Response containing a provisioning session object + ''' + ProvisioningSessionId: ResourceId + ProvisioningSession: ProvisioningSession + +class ContentHostingConfigurationResponse(TagAndDateResponse, total=False): + '''Response containing a content hosting configuration + ''' + ProvisioningSessionId: ResourceId + ContentHostingConfiguration: ContentHostingConfiguration + +class ServerCertificateResponse(TagAndDateResponse, total=False): + '''Response containing a server certificate + ''' + ProvisioningSessionId: ResourceId + ServerCertificateId: ResourceId + ServerCertificate: str + +class ServerCertificateSigningRequestResponse(TagAndDateResponse, total=False): + '''Response containing a CSR for a reserved certificate + ''' + ProvisioningSessionId: ResourceId + ServerCertificateId: ResourceId + CertificateSigningRequestPEM: str + +class ContentProtocolsResponse(TagAndDateResponse, total=False): + '''Response containing a ContentProtocols object + ''' + ProvisioningSessionId: ResourceId + ContentProtocols: ContentProtocols + +class M1Client: + '''5G-MAG Reference Tools: M1 Client + ''' + + def __init__(self, host_address: Tuple[str,int]): + ''' + Constructor + + :param Tuple[str,int] host_address: 5GMS Application Function to connect to as a tuple of hostname/ip-addr and TCP port + number. + ''' + self.__host_address = host_address + self.__connection = None + self.__log = logging.getLogger(__name__ + '.' + self.__class__.__name__) + + # TS26512_M1_ProvisioningSession + + async def createProvisioningSession(self, provisioning_session_type: ProvisioningSessionType, + external_application_id: ApplicationId, + asp_id: Optional[ApplicationId] = None + ) -> Optional[ProvisioningSessionResponse]: + ''' + Create a provisioning session on the 5GMS Application Function + + :param ProvisioningSessionType provisioning_session_type: The provisioning session type. + :param str external_application_id: The application ID of the external application requesting the new provisioning + session. + :param Optional[str] asp_id: The Application Server Provider ID. + + :return: the ResourceId of the allocated provisioning session or None if there was an error. + + :raises M1ClientError: if there was a problem with the request + :raises M1ServerError: if there was a server side issue preventing the creation of the provisioning session. + ''' + self.__debug('M1Client.createProvisioningSession(%r, %r, asp_id=%r)', + provisioning_session_type, external_application_id, asp_id) + send: ProvisioningSession = { + 'provisioningSessionType': provisioning_session_type, + 'externalApplicationId': external_application_id + } + if asp_id is not None: + send['aspId'] = asp_id + result = await self.__do_request('POST', '/provisioning-sessions', json.dumps(send), + 'application/json') + if result['status_code'] == 201: + ret: ProvisioningSessionResponse = {'ProvisioningSessionId': result['headers']['location'].split('/')[-1]} + if len(result['body']) > 0: + ret.update(self.__tag_and_date(result)) + ret['ProvisioningSession'] = ProvisioningSession.fromJSON(result['body']) + return ret + self.__default_response(result) + return None + + async def getProvisioningSessionById(self, + provisioning_session_id: ResourceId + ) -> Optional[ProvisioningSessionResponse]: + ''' + Get a provisioning session from the 5GMS Application Function + + :param ResourceId provisioning_session_id: The provisioning session to find. + + :return: a ProvisioningSessionResponse structure if the provisioning session was found, or None if the provisioning + session was not found. + + :raises M1ClientError: if there was a problem with the request + :raises M1ServerError: if there was a server side issue preventing the creation of the provisioning session. + ''' + result = await self.__do_request('GET', + '/provisioning-sessions/' + provisioning_session_id, '', + 'application/json') + if result['status_code'] == 200: + ret: ProvisioningSessionResponse = self.__tag_and_date(result) + ret.update({ + 'ProvisioningSessionId': provisioning_session_id, + 'ProvisioningSession': ProvisioningSession.fromJSON(result['body']) + }) + return ret + if result['status_code'] == 404: + return None + self.__default_response(result) + return None + + async def destroyProvisioningSession(self, provisioning_session_id: ResourceId) -> bool: + ''' + Destroy a provisioning session on the 5GMS Application Function + + :param ResourceId provisioning_session_id: The provisioning session to find. + + :return: True if a provisioning session was deleted (or pending deletion) or False if there was no action. + + :raise M1ClientError: if there was a problem with the request + :raise M1ServerError: if there was a server side issue preventing the creation of the provisioning session. + ''' + result = await self.__do_request('DELETE', + '/provisioning-sessions/' + provisioning_session_id, '', + 'application/json') + if result['status_code'] == 204 or result['status_code'] == 202: + return True + self.__default_response(result) + return False + + # TS26512_M1_ContentHostingProvisioning + + async def createContentHostingConfiguration(self, provisioning_session_id: ResourceId, + content_hosting_configuration: ContentHostingConfiguration + ) -> Union[bool,ContentHostingConfigurationResponse]: + ''' + Create a content hosting configuration for a provisioning session + + :param ResourceId provisioning_session_id: The provisioning session to create the content hosting configuration in. + :param ContentHostingConfiguration content_hosting_configuration: The content hosting configuration template to use. + + :return: True if the ContentHostingConfiguration was accepted but the response was empty, False if the + ContentHostingConfiguration was not accepted or a ContentHostingConfigurationResponse if the + ContentHostingConfiguration was accepted and the AF updated version returned. + :raise M1ClientError: if there was a problem with the request. + :raise M1ServerError: if there was a server side issue preventing the creation of the provisioning session. + ''' + result = await self.__do_request('POST', + f'/provisioning-sessions/{provisioning_session_id}/content-hosting-configuration', + json.dumps(content_hosting_configuration), 'application/json') + if result['status_code'] == 201: + if len(result['body']) > 0: + ret: ContentHostingConfigurationResponse = self.__tag_and_date(result) + ret.update({ + 'ProvisioningSessionId': provisioning_session_id, + }) + if len(result['body']) > 0: + ret.update({ + 'ContentHostingConfiguration': ContentHostingConfiguration.fromJSON( + result['body']) + }) + return ret + return True + self.__default_response(result) + return False + + async def retrieveContentHostingConfiguration(self, provisioning_session_id: ResourceId + ) -> Optional[ContentHostingConfigurationResponse]: + ''' + Fetch the content hosting configuration for a provisioning session + + :param ResourceId provisioning_session_id: The provisioning session to fetch the current content hosting configuration + for. + + :return: None if the provisioning session does not exist, also returns None if the + provisioning session exists but does not have a content hosting configuration, + otherwise returns a ContentHostingConfigurationResponse. + + :raise M1ClientError: if there was a problem with the request. + :raise M1ServerError: if there was a server side issue preventing the creation of the provisioning session. + ''' + result = await self.__do_request('GET', + f'/provisioning-sessions/{provisioning_session_id}/content-hosting-configuration', + '', 'application/json') + if result['status_code'] == 200: + ret: ContentHostingConfigurationResponse = self.__tag_and_date(result) + ret.update({ + 'ProvisioningSessionId': provisioning_session_id, + 'ContentHostingConfiguration': ContentHostingConfiguration.fromJSON(result['body']) + }) + return ret + if result['status_code'] == 404: + return None + self.__default_response(result) + return None + + async def updateContentHostingConfiguration(self, provisioning_session_id: ResourceId, + content_hosting_configuration: ContentHostingConfiguration + ) -> bool: + ''' + Update a content hosting configuration for a provisioning session + + :param ResourceId provisioning_session_id: The provisioning session to update the current content hosting configuration + for. + :param ContentHostingConfiguration content_hosting_configuration: The new content hosting configuration to apply. + + :return: ``True`` if the update succeeded or ``False`` if the update failed. + + :raise M1ClientError: if there was a problem with the request. + :raise M1ServerError: if there was a server side issue preventing the creation of the provisioning session. + ''' + result = await self.__do_request('PUT', f'/provisioning-sessions/{provisioning_session_id}/content-hosting-configuration', + json.dumps(content_hosting_configuration), 'application/json') + if result['status_code'] == 204: + return True + if result['status_code'] == 404: + return False + self.__default_response(result) + return False + + async def patchContentHostingConfiguration(self, provisioning_session_id: ResourceId, patch: str + ) -> Union[bool,ContentHostingConfigurationResponse]: + ''' + Patch a content hosting configuration for a provisioning session using a JSON patch + + :param ResourceId provisioning_session_id: The provisioning session to update the current content hosting configuration + for. + :param str patch: The patch information in JSON patch format. + + :return: a `ContentHostingConfigurationResponse` if the patch succeeded and the new ContentHostingConfiguration was + returned, or True if the patch succeeded but no ContentHostingConfiguration was returned, or False if the + patch failed. + + :raise M1ClientError: if there was a problem with the request. + :raise M1ServerError: if there was a server side issue preventing the creation of the provisioning session. + ''' + result = await self.__do_request('PATCH', + f'/provisioning-sessions/{provisioning_session_id}/content-hosting-configuration', + + patch, 'application/json-patch+json') + if result['status_code'] == 200: + if len(result['body']) > 0: + ret: ContentHostingConfigurationResponse = self.__tag_and_date(result) + ret.update({ + 'ProvisioningSessionId': provisioning_session_id, + 'ContentHostingConfiguration': ContentHostingConfiguration.fromJSON( + result['body']) + }) + return ret + return True + if result['status_code'] == 404: + return False + self.__default_response(result) + return False + + async def destroyContentHostingConfiguration(self, provisioning_session_id: ResourceId + ) -> bool: + ''' + Delete a content hosting configuration for a provisioning session + + :param ResourceId provisioning_session_id: The provisioning session to remove the content hosting configuration for. + + :return: True if the ContentHostingConfiguration was deleted or False if the ContentHostingConfiguration did not exist. + + :raise M1ClientError: if there was a problem with the request. + :raise M1ServerError: if there was a server side issue preventing the creation of the provisioning session. + ''' + result = await self.__do_request('DELETE', + f'/provisioning-sessions/{provisioning_session_id}/content-hosting-configuration', + '', 'application/json') + if result['status_code'] == 204: + return True + if result['status_code'] == 404: + return False + self.__default_response(result) + return False + + async def purgeContentHostingCache(self, provisioning_session_id: ResourceId, + filter_regex: Optional[str] = None) -> Optional[int]: + ''' + Purge cache entries for a provisioning session + + :param ResourceId provisioning_session_id: The provisioning session to purge cache entries for. + :param Optional[str] filter_regex: Optional regular expression to match the cache entries origin URL path. + + :return: the number of purged entries, or None if no purge took place. + :raise M1ClientError: if there was a problem with the request. + :raise M1ServerError: if there was a server side issue preventing the creation of the provisioning session. + ''' + body = '' + if filter_regex is not None: + body = f'pattern={filter_regex}' + result = await self.__do_request('POST', + f'/provisioning-sessions/{provisioning_session_id}/content-hosting-configuration/purge', + body, 'application/x-www-form-urlencoded') + if result['status_code'] == 200: + return int(result['body']) + if result['status_code'] == 204: + return None + self.__default_response(result) + return None + + # TS26512_M1_ServerCertificatesProvisioning + + async def createOrReserveServerCertificate(self, provisioning_session_id: ResourceId, csr=False) -> Optional[ServerCertificateSigningRequestResponse]: + '''Create or reserve a server certificate for a provisioing session + + :param ResourceId provisioning_session_id: The provisioning session to create the new certificate entry in. + :param bool csr: Whether to reserve a certificate and return the CSR PEM data. + + If *csr* is ``True`` then this will reserve the certificate and request the CSR PEM data be returned along side the Id + of the newly reserved certificate. + + If *csr* is ``False`` or not provided then create a new certificate and just return the new certificate Id. + + :return: a `ServerCertificateSigningRequestResponse` containing the certificate id and metadata optionally with CSR PEM + data if *csr* was ``True``. + :raise M1ClientError: if there was a problem with the request. + :raise M1ServerError: if there was a server side issue preventing the creation of the provisioning session. + ''' + + url = f'/provisioning-sessions/{provisioning_session_id}/certificates' + if csr: + url += '?csr=true' + result = await self.__do_request('POST', url, '', 'application/octet-stream') + if result['status_code'] == 200: + certificate_id = result['headers'].get('Location','').rsplit('/',1)[1] + ret: ServerCertificateSigningRequestResponse = self.__tag_and_date(result) + ret.update({ + 'ProvisioningSessionId': provisioning_session_id, + 'ServerCertificateId': certificate_id, + }) + if csr and len(result['body']) > 0: + ret.update({ + 'CertificateSigningRequestPEM': result['body'], + }) + return ret + self.__default_response(result) + return None + + async def createServerCertificate(self, provisioning_session_id: ResourceId) -> ServerCertificateResponse: + '''Create a new certificate for a provisioning session + + :param ResourceId provisioning_session_id: The provisioning session to create the new certificate entry in. + + :return: a ServerCertificateResponse for the newly created certificate. + :raise M1ClientError: if there was a problem with the request. + :raise M1ServerError: if there was a server side issue preventing the creation of the provisioning session. + ''' + result = await self.createOrReserveServerCertificate(provisioning_session_id, csr=False) + return result + + async def reserveServerCertificate(self, provisioning_session_id: ResourceId) -> ServerCertificateSigningRequestResponse: + '''Reserve a certificate for a provisioning session and get the CSR PEM + + :param ResourceId provisioning_session_id: The provisioning session to create the new certificate entry in. + + :return: a `ServerCertificateSigningRequestResponse` containing the CSR as a PEM string plus metadata for the reserved + certificate. + :raise M1ClientError: if there was a problem with the request. + :raise M1ServerError: if there was a server side issue preventing the creation of the provisioning session. + ''' + result = await self.createOrReserveServerCertificate(provisioning_session_id, csr=True) + if result is None or 'CertificateSigningRequestPEM' not in result: + raise M1ClientError(reason = f'Failed to retrieve CSR for session {provisioning_session_id}', status_code = 200) + return result + + async def uploadServerCertificate(self, provisioning_session_id: ResourceId, certificate_id: ResourceId, pem_data: str) -> bool: + '''Upload the signed public certificate for a reserved certificate + + :param ResourceId provisioning_session_id: The provisioning session the certificate was reserved for. + :param ResourceId certificate_id: The certificate Id of the reserved certificate. + :param str pem_data: A string containing the PEM data for the public certificate to upload. + + :return: ``True`` if successful or ``False`` if the certificate has already been uploaded. + :raise M1ClientError: if there was a problem with the request. + :raise M1ServerError: if there was a server side issue preventing the creation of the provisioning session. + ''' + result = await self.__do_request('PUT', + f'/provisioning-sessions/{provisioning_session_id}/certificates/{certificate_id}', + pem_data, 'application/x-pem-file') + if result['status_code'] == 204: + return True + self.__default_response(result) + return False + + async def retrieveServerCertificate(self, provisioning_session_id: ResourceId, certificate_id: ResourceId) -> Optional[ServerCertificateResponse]: + '''Retrieve the public certificate for a given certificate Id + + :param ResourceId provisioning_session_id: The provisioning session for the certificate. + :param ResourceId certificate_id: The certificate Id of the certificate. + + :return: a ServerCertificateResponse containing the PEM data for the public certificate and its metadata or ``None`` + if the certificate is reserved and awaiting upload. + + :raise M1ClientError: if there was a problem with the request or the certificate was not found. + :raise M1ServerError: if there was a server side issue preventing the creation of the provisioning session. + ''' + result = await self.__do_request('GET', + f'/provisioning-sessions/{provisioning_session_id}/certificates/{certificate_id}', + '', 'application/octet-stream') + if result['status_code'] == 200: + ret: ServerCertificateResponse = self.__tag_and_date(result) + ret['ProvisioningSessionId'] = provisioning_session_id + ret['ServerCertificateId'] = certificate_id + ret['ServerCertificate'] = result['body'] + return ret + if result['status_code'] == 204: + return None + if result['status_code'] == 404: + raise M1ClientError(reason="Certificate not found", status_code=404) + self.__default_response(result) + return None + + async def destroyServerCertificate(self, provisioning_session_id: ResourceId, certificate_id: ResourceId) -> bool: + '''Delete a certificate. + + :param ResourceId provisioning_session_id: The provisioning session for the certificate. + :param ResourceId certificate_id: The certificate Id of the certificate. + + :return: ``True`` if the certificate has been deleted. + :raise M1ClientError: if there was a problem with the request. + :raise M1ServerError: if there was a server side issue preventing the creation of the provisioning session. + ''' + result = await self.__do_request('DELETE', + f'/provisioning-sessions/{provisioning_session_id}/certificates/{certificate_id}', + '', 'application/octet-stream') + if result['status_code'] == 204: + return True + self.__default_response(result) + return False + + # TS26512_M1_ContentProtocolsDiscovery + async def retrieveContentProtocols(self, provisioning_session_id: ResourceId) -> Optional[ContentProtocolsResponse]: + '''Get the ContentProtocols information for the provisioning session + + :param ResourceId provisioning_session_id: The provisioning session to get the ContentProtocols for. + + :return: a `ContentProtocolsResponse` containing the ContentProtocols structure and metadata or None if the + provisioning session was not found. + :raise M1ClientError: if there was a problem with the request. + :raise M1ServerError: if there was a server side issue preventing the creation of the provisioning session. + ''' + result = await self.__do_request('GET', + f'/provisioning-sessions/{provisioning_session_id}/protocols', + '', 'application/octet-stream') + if result['status_code'] == 200: + ret: ContentProtocolsResponse = self.__tag_and_date(result) + ret['ContentProtocols'] = ContentProtocols.fromJSON(result['body']) + return ret + self.__default_response(result) + return None + + # TS26512_M1_ConsumptionReportingProvisioning + #async def activateConsumptionReporting(self, provisioning_session_id: ResourceId, consumption_reporting_config: ConsumptionReportingConfiguration) -> Optional[ResourceId]: + #async def retrieveConsumptionReportingConfiguration(self, provisioning_session_id: ResourceId, consumption_reporting_id: ResourceId) -> ConsumptionReportingConfigurationResponse: + #async def updateConsumptionReportingConfiguration(self, provisioning_session_id: ResourceId, consumption_reporting_config: ConsumptionReportingConfiguration) -> bool: + #async def patchConsumptionReportingConfiguration(self, provisioning_session_id: ResourceId, patch: str) -> ConsumptionReportingConfigurationResponse: + #async def destroyConsumptionReportingConfiguration(self, provisioning_session_id: ResourceId, consumption_reporting_id: ResourceId) -> bool: + + # TS26512_M1_ContentPreparationTemplatesProvisioning + #async def createContentPreparationTemplate(self, provisioning_session_id: ResourceId, content_preparation_template: Any) -> Optional[ResourceId]: + #async def retrieveContentPreparationTemplate(self, provisioning_session_id: ResourceId, content_preparation_template_id: ResourceId) -> ContentPreparationTemplateResponse: + #async def updateContentPreparationTemplate(self, provisioning_session_id: ResourceId, content_preparation_template_id: ResourceId, content_preparation_template: Any) -> bool: + #async def patchContentPreparationTemplate(self, provisioning_session_id: ResourceId, content_preparation_template_id: ResourceId, patch: str) -> ContentPreparationTemplateResponse: + #async def destroyContentPreparationTemplate(self, provisioning_session_id: ResourceId, content_preparation_template_id: ResourceId) -> bool: + + # TS26512_M1_EdgeResourcesProvisioning + #async def createEdgeResourcesConfiguration(self, provisioning_session_id: ResourceId, edge_resource_config: EdgeResourceConfiguration) -> Optional[ResourceId]: + #async def retrieveEdgeResourcesConfiguration(self, provisioning_session_id: ResourceId, edge_resource_config_id: ResourceId) -> EdgeResourceConfigurationResponse: + #async def updateEdgeResourcesConfiguration(self, provisioning_session_id: ResourceId, edge_resource_config_id: ResourceId, edge_resource_config: EdgeResourceConfiguration) -> bool: + #async def patchEdgeResourcesConfiguration(self, provisioning_session_id: ResourceId, edge_resource_config_id: ResourceId, patch: str) -> EdgeResourceConfigurationResponse: + #async def destroyEdgeResourcesConfiguration(self, provisioning_session_id: ResourceId, edge_resource_config_id: ResourceId) -> bool: + + # TS26512_M1_EventDataProcessingProvisioning + #async def createEventDataProcessingConfiguration(self, provisioning_session_id: ResourceId, event_data_processing_config: EventDataProcessingConfiguration) -> Optional[ResourceId]: + #async def retrieveEventDataProcessingConfiguration(self, provisioning_session_id: ResourceId, event_data_processing_config_id: ResourceId) -> EventDataProcessingConfigurationResponse: + #async def updateEventDataProcessingConfiguration(self, provisioning_session_id: ResourceId, event_data_processing_config_id: ResourceId, event_data_processing_config: EventDataProcessingConfiguration) -> bool: + #async def patchEventDataProcessingConfiguration(self, provisioning_session_id: ResourceId, event_data_processing_config_id: ResourceId, patch: str) -> EventDataProcessingConfigurationResponse: + #async def destroyEventDataProcessingConfiguration(self, provisioning_session_id: ResourceId, event_data_processing_config_id: ResourceId) -> bool: + + # TS26512_M1_MetricsReportingProvisioning + #async def activateMetricsReporting(self, provisioning_session_id: ResourceId, metrics_reporting_config: MetricsReportingConfiguration) -> ResourceId: + #async def retrieveMetricsReportingConfiguration(self, provisioning_session_id: ResourceId, metrics_reporting_config_id: ResourceId) -> MetricsReportingConfigurationResponse: + #async def updateMetricsReportingConfiguration(self, provisioning_session_id: ResourceId, metrics_reporting_config_id: ResourceId, metrics_reporting_config: MetricsReportingConfiguration) -> bool: + #async def patchMetricsReportingConfiguration(self, provisioning_session_id: ResourceId, metrics_reporting_config_id: ResourceId, patch: str) -> MetricsReportingConfigurationResponse: + #sync def destroyMetricsReportingConfiguration(self, provisioning_session_id: ResourceId, metrics_reporting_config_id: ResourceId) -> bool: + + # TS26512_M1_PolicyTemplatesProvisioning + #async def createPolicyTemplate(self, provisioning_session_id: ResourceId, policy_template: PolicyTemplate) -> Optional[ResourceId]: + #async def retrievePolicyTemplate(self, provisioning_session_id: ResourceId, policy_template_id: ResourceId) -> PolicyTemplateResponse: + #async def updatePolicyTemplate(self, provisioning_session_id: ResourceId, policy_template_id: ResourceId, policy_template: PolicyTemplate) -> bool: + #async def patchPolicyTemplate(self, provisioning_session_id: ResourceId, policy_template_id: ResourceId, patch: str) -> PolicyTemplateResponse: + #async def destroyPolicyTemplate(self, provisioning_session_id: ResourceId, policy_template_id: ResourceId) -> bool: + + # Private methods + + async def __do_request(self, method: str, url_suffix: str, body: Union[str,bytes], + content_type: str, headers: Optional[dict] = None) -> Dict[str,Any]: + '''Send a request to the 5GMS Application Function + + :meta private: + :param str method: The HTTP method for the request. + :param str url_suffix: The URL path suffix for the request after the protocol and version identifiers. + :param Union[str,bytes] body: The body of the request as a `str` or `bytes`. + :param str content_type: The content type to use in the ``Content-Type`` header of the request. + :param Optional[dict] headers: Extra headers to go along with the request. + :return: a `dict` with 3 entries ``status_code``, ``body`` and ``headers`` representing the HTTP response status code, + the response message body and the response headers. + :raise M1ServerError: if communication with the AF failed. + ''' + # pylint: disable=too-many-arguments + if isinstance(body, str): + body = bytes(body, 'utf-8') + req_headers = {'Content-Type': content_type} + if headers is not None: + req_headers.update(headers) + url = f'http://{self.__host_address[0]}:{self.__host_address[1]}/3gpp-m1/v2{url_suffix}' + if self.__connection is None: + self.__connection = httpx.AsyncClient(http1=True, http2=False, + headers={'User-Agent': '5GMS-AF/testing'}) + req = self.__connection.build_request(method, url, headers=req_headers, data=body) + try: + resp = await self.__connection.send(req) + except httpx.RemoteProtocolError as err: + raise M1ServerError(reason=f'Communication with the Application Function failed: {err}', status_code=500) + return {'status_code': resp.status_code, 'body': resp.text, 'headers': resp.headers} + + def __default_response(self, result: Dict[str,Any]) -> None: + '''Handle default actions for all responses from the 5GMS Application Function + + This will raise exceptions for 4XX and 5XX response codes. + + :meta private: + :param Dict[str,Any] result: The result as returned by `__do_request`. + + :raise M1ClientError: if there was a problem with the request. + :raise M1ServerError: if there was a server side issue preventing the creation of the provisioning session. + ''' + if result['status_code'] >= 400 and result['status_code'] < 500: + raise M1ClientError(reason='M1 operation failed: '+str(result['body']), + status_code=result['status_code']) + if result['status_code'] >= 500 and result['status_code'] < 600: + raise M1ServerError(reason='M1 operation failed: '+str(result['body']), + status_code=result['status_code']) + + @staticmethod + def __tag_and_date(result: Dict[str,Any]) -> TagAndDateResponse: + '''Get the response message standard metadata + + This will extract metadata from ``ETag``, ``Last-Modified`` and ``Cache-Control`` headers. + + :param Dict[str,Any] result: The result as returned by `__do_request`. + + :return: the base TagAndDateResponse structure for all response messages. + ''' + # Get ETag + ret = {'ETag': result['headers'].get('etag')} + # Get Last-Modified as a datetime.datetime + lm_dt = result['headers'].get('last-modified') + if lm_dt is not None: + try: + lm_dt = datetime.datetime.strptime(lm_dt, '%a, %d %b %Y %H:%M:%S %Z').replace( + tzinfo=datetime.timezone.utc) + except ValueError: + try: + lm_dt = datetime.datetime.strptime(lm_dt, '%A, %d-%b-%y %H:%M:%S %Z').replace( + tzinfo=datetime.timezone.utc) + except ValueError: + try: + lm_dt = datetime.datetime.strptime(lm_dt, '%a %b %d %H:%M:%S %Y').replace( + tzinfo=datetime.timezone.utc) + except ValueError: + lm_dt = None + ret['Last-Modified'] = lm_dt + # Get Cache-Control as a cache expiry time + cc = result['headers'].get('cache-control') + if cc is not None: + age = result['headers'].get('age') + if age is None: + age = 0 + else: + age = int(age) + max_age_values = [int(c[8:]) for c in [v.strip() for v in cc.split(',')] if c[:8] == 'max-age='] + if len(max_age_values) > 0: + cc = datetime.datetime.now(tz=datetime.timezone.utc)+datetime.timedelta(seconds=min(max_age_values)-age) + else: + cc = None + ret['Cache-Until'] = cc + return ret + + def __debug(self, *args, **kwargs) -> None: + '''Output a debug message + + :meta private: + :param args: Positional arguments to pass to `logger.debug()`. + :param kwargs: Keyword arguments to pass to `logger.debug()`. + ''' + self.__log.debug(*args, **kwargs) + +__all__ = [ + # Types + 'ProvisioningSessionResponse', + 'ContentHostingConfigurationResponse', + 'ServerCertificateResponse', + 'ServerCertificateSigningRequestResponse', + 'ContentProtocolsResponse', + # Classes + 'M1Client', + ] diff --git a/tools/python3/lib/rt_m1_client/data_store.py b/tools/python3/lib/rt_m1_client/data_store.py new file mode 100644 index 0000000..21ed388 --- /dev/null +++ b/tools/python3/lib/rt_m1_client/data_store.py @@ -0,0 +1,144 @@ +#!/usr/bin/python3 +#============================================================================== +# 5G-MAG Reference Tools: M1 Session Persistent Data Store +#============================================================================== +# +# File: rt_m1_client/data_store.py +# License: 5G-MAG Public License (v1.0) +# Author: David Waring +# Copyright: (C) 2022 British Broadcasting Corporation +# +# For full license terms please see the LICENSE file distributed with this +# program. If this file is missing then the license can be retrieved from +# https://drive.google.com/file/d/1cinCiA778IErENZ3JN52VFW-1ffHpx7Z/view +# +#============================================================================== +# +# M1 Session DataStore classes +# ============================ +# +# This module contains classes to implement a persistent data store for use by +# the M1Session class. +# +# There are 2 classes DataStore is the base class and JSONFileDataStore is an +# implementation which stores the persistent data objects as JSON objects. +# +'''5G-MAG Reference Tools: M1 Session DataStore classes +==================================================== + +The DataStore class provides a base class for storing persistent data using +a key string. This data can then be retrieved later so that the application can carry on where it left off. + +The JSONFileDataStore class is an implementation that stores the data being +represented in JSON notation as a set of files. +''' +import aiofiles +import aiofiles.os +import json +import logging +import os +import os.path +from typing import Any + +class DataStore: + '''DataStore base class + ''' + def __await__(self): + '''Implement ``await`` on object creation + + This allows derived `DataStore` objects to perform asynchronous tasks on object instantiation. + + For example:: + data_store = await DataStore() + + This will await the `asyncInit()` method of this object. + ''' + return self.asyncInit().__await__() + + async def asyncInit(self): + '''Asynchronous DataStore initialisation + + Implementations should override this method to perform any object initialisation requiring asynchronous operations. + + This must always return *self*. + + :return: self + ''' + return self + + async def get(self, key: str, default: Any = None) -> Any: + '''Get a persisted value by key name + + :param str key: The key name to retrieve the `DataStore` value for. + :param default: The default value to return if the key does not exist in the `DataStore`. + + :return: The value of the retrieved key or the *default* value. + ''' + raise NotImplementedError('DataStore implementation should override this method') + + async def set(self, key: str, value: Any) -> bool: + '''Store a persisted value using the key name + + :param str key: The key name to set a value for. + :param value: The value to set. + + :return: ``True`` if the value was set in the `DataStore` or ``False`` if there was a failure. + ''' + raise NotImplementedError('DataStore implementation should override this method') + +class JSONFileDataStore(DataStore): + '''JSONFileDataStore class + + This class implements a DataStore as a set of files containing JSON. + ''' + def __init__(self, data_store_dir: str): + '''Constructor + + :param str data_store_dir: The directory path to use for the JSON file data store. + + Please note that this object should be instantiated using ``await JSONFileDataStore(data_store_dir)`` as it has + asynchronous initialisation to perform. + ''' + self.__dir = data_store_dir + + async def asyncInit(self): + '''Asynchronous JSONFileDataStore initialisation + + This will ensure that the data store directory for JSON files exists during instantiation. + + :return: self + :raise RuntimeError: if the data store path already exists but is not a directory. + ''' + if not await aiofiles.os.path.exists(self.__dir): + await aiofiles.os.makedirs(self.__dir) + if not await aiofiles.os.path.isdir(self.__dir): + raise RuntimeError(f'{self.__dir} is not a directory') + return self + + async def get(self, key: str, default: Any = None) -> Any: + '''Get a persisted value by key name + + :param str key: The key name to retrieve the `DataStore` value for. + :param default: The default value to return if the *key* does not exist in the `DataStore`. + + :return: The value of the retrieved key or the *default* value. + ''' + json_file = os.path.join(self.__dir, f'{key}.json') + if not await aiofiles.os.path.exists(json_file) or not await aiofiles.os.path.isfile(json_file): + return default + async with aiofiles.open(json_file, mode='r') as json_in: + val = json.loads(await json_in.read()) + return val + + async def set(self, key: str, value: Any) -> bool: + '''Store a persisted value using the key name + + :param str key: The key name to set a value for. + :param value: The value to set. + + :return: ``True`` if the value was set in the `DataStore` or ``False`` if there was a failure. + ''' + json_file = os.path.join(self.__dir, f'{key}.json') + async with aiofiles.open(json_file, mode='w') as json_out: + await json_out.write(json.dumps(value)) + return True diff --git a/tools/python3/lib/rt_m1_client/exceptions.py b/tools/python3/lib/rt_m1_client/exceptions.py new file mode 100644 index 0000000..0b2e41c --- /dev/null +++ b/tools/python3/lib/rt_m1_client/exceptions.py @@ -0,0 +1,135 @@ +#!/usr/bin/python3 +#============================================================================== +# 5G-MAG Reference Tools: M1 Client Exceptions +#============================================================================== +# +# File: rt_m1_client/exceptions.py +# License: 5G-MAG Public License (v1.0) +# Author: David Waring +# Copyright: (C) 2023 British Broadcasting Corporation +# +# For full license terms please see the LICENSE file distributed with this +# program. If this file is missing then the license can be retrieved from +# https://drive.google.com/file/d/1cinCiA778IErENZ3JN52VFW-1ffHpx7Z/view +# +#============================================================================== +# +# M1 Client Exceptions +# ==================== +# +# This module defines the exceptions used by the M1 client classes +# +# M1ClientError - Exception used for indicating client request issues as +# returned by the M1 Server (5GMS Application Function). +# +# M1ServerError - Exception used for indicating M1 server errors. The request +# that generated this error may succeed if retried at a later +# time. + +'''5G-MAG Reference Tools: M1 Client Exceptions +============================================ + +This module defines some custom exceptions used by the M1 Client class. + +The M1Error exception is the superclass of the M1ClientError and M1ServerError. +This can be used as a catch all for errors reported by the M1 Server in +response to a request. + +The M1ClientError exception derives from M1Error and is used when a request +response indicates a 4XX status code. This means that there was a problem with +the request and it should not be retried without modification to correct the +issues. + +The M1ServerError exception derives from M1Error and is used when a request +response indicates a 5XX status code. This means that there was an error on the +server. The request may be retried at a later time and may then succeed. +''' +from typing import Optional + +from .types import ProblemDetail, InvalidParam + +def format_invalid_param(inv_param: InvalidParam) -> str: + ''' + Format an InvalidParams entry for display + + :param InvalidParam inv_param: The `InvalidParam` to generate a formatted string for. + + :return: a `str` containing the invalid parameter name optionally followed by the reason. + :rtype: str + ''' + ret: str = inv_param['param'] + if 'reason' in inv_param and inv_param['reason'] is not None: + ret += ' : ' + inv_param['reason'] + return ret + +class M1Error(Exception): + '''Exception base class for all M1 Exceptions + + This can be used to catch both M1ClientError and M1ServerError exceptions. + ''' + def __init__(self, reason: str, # pylint: disable=useless-super-delegation + status_code: Optional[int] = None, problem_detail: Optional[ProblemDetail] = None): + '''Constructor + + :param str reason: The reason for the error. + :param Optional[int] status_code: An optional HTTP status code to associate with the error. + :param Optional[ProblemDetail] problem_detail: An optional `ProblemDetail` to associate with the error. + ''' + super().__init__(reason, status_code, problem_detail) + + def __str__(self) -> str: + '''String representation of the error + + :return: a formatted string representation of the `M1Error`. + ''' + # If a ProblemDetail is available use it + if self.args[2] is not None: + problem = self.args[2] + ret: str = '' + if self.args[1] is not None: + ret = f'[{self.args[1]}] ' + if 'title' in problem: + ret += problem['title']+'\n' + if 'description' in problem: + ret += problem['description'] + if 'invalidParams' in problem and problem['invalidParams'] is not None: + ret += '\nInvalid Parameters:\n'+'\n'.join( + [' '+format_invalid_param(p) for p in problem['invalidParams']]) + return ret + # Else if an HTTP status code is available use "[status_code] reason" as the format + if self.args[1] is not None: + return f'[{self.args[1]}] {self.args[0]}' + # Otherwise just use the reason string + return self.args[0] + + def __repr__(self) -> str: + '''Format a `str` representation of this error + + :return: The error formatted as a constructor for this error. + ''' + return f'{self.__class__.__name__}(reason={self.args[0]!r}, status_code={self.args[1]!r}, problem_detail={self.args[2]!r})' + +class M1ClientError(M1Error): + '''Raised when there was a client side problem during M1 operations + + This error is raised when there was a problem with the client request + detected either by this class, or by the M1 server (5GMS Application + Function) responding with a 4XX response. + + The request should not be repeated in this form as it will fail again. + ''' + +class M1ServerError(M1Error): + '''Raised when there was a server side problem during M1 operations + + This represents 5XX error responses from the M1 server (5GMS Application + Function). + + The request may be repeated at a future date and may or may not work then. + ''' + +__all__ = [ + "M1Error", + "M1ClientError", + "M1ServerError", + ] diff --git a/tools/python3/lib/rt_m1_client/session.py b/tools/python3/lib/rt_m1_client/session.py new file mode 100644 index 0000000..874a259 --- /dev/null +++ b/tools/python3/lib/rt_m1_client/session.py @@ -0,0 +1,687 @@ +#!/usr/bin/python3 +#============================================================================== +# 5G-MAG Reference Tools: M1 Session +#============================================================================== +# +# File: rt_m1_client/session.py +# License: 5G-MAG Public License (v1.0) +# Author: David Waring +# Copyright: (C) 2023 British Broadcasting Corporation +# +# For full license terms please see the LICENSE file distributed with this +# program. If this file is missing then the license can be retrieved from +# https://drive.google.com/file/d/1cinCiA778IErENZ3JN52VFW-1ffHpx7Z/view +# +#============================================================================== +# +# M1 Session class +# =============== +# +# This module contains an M1 Session management class written in Python 3 using +# asyncio. This class uses an M1Client object to communicate with the 5GMS +# Application Function via the interface at reference point M1. +# +# The module will maintain a persistent list of provisioning sessions and assist +# in managing the resources for those provisioning sessions. +# +'''5G-MAG Reference Tools: M1 Session class +======================================== + +This class provides an interface for managing provisioning sessions on a 5GMS +Application Function. + +This class uses the M1Client class to communicate with the 5GMS Application +Function via the interface at reference point M1. +''' +import datetime +import importlib +import logging +from typing import Optional, Union, Tuple, Dict, Any, TypedDict, List, Iterable + +import OpenSSL + +from .exceptions import (M1ClientError, M1ServerError, M1Error) +from .types import (ApplicationId, ContentHostingConfiguration, ContentProtocols, ProvisioningSessionType, ProvisioningSession, + ResourceId, PROVISIONING_SESSION_TYPE_DOWNLINK) +from .client import (M1Client, ProvisioningSessionResponse, ContentHostingConfigurationResponse, ServerCertificateResponse, + ServerCertificateSigningRequestResponse, ContentProtocolsResponse) +from .data_store import DataStore +from .certificates import CertificateSigner, DefaultCertificateSigner + +class M1Session: + '''M1 Session management class + =========================== + + This class is used as the top level class to manage a communication session with the 5GMS Application Function. It will + communicate using the `M1Client` class with the M1 Server (5GMS Application Function) and cache the results to improve + efficiency. It can also use a `DataStore` to provide persistence of information across different sessions, and can use a + `CertificateSigner` to perform signing of certificates when ``domainNameAlias`` is used. + ''' + + def __init__(self, host_address: Tuple[str,int], persistent_data_store: Optional[DataStore] = None, certificate_signer: Optional[Union[CertificateSigner,type,str]] = None): + '''Constructor + + :param host_address: A tuple containing the M1 server (5GMS Application Function) hostname/ip-address and TCP port number + to contact it at. + :param persistent_data_store: A `DataStore` object to use to provide persistent storage. + :param certificate_signer: A `CertificateSigner` to use when signing certificates with extra domain names. This can be either a `str` containing the full Python class name, a `CertificateSigner` class to instantiate if needed, or an instance of a `CertificateSigner` to use. If not given then ``rt_m1_client.certificates.DefaultCertificateSigner`` is used. + ''' + self.__m1_host = host_address + self.__data_store_dir = persistent_data_store + self.__cert_signer = certificate_signer + self.__m1_client = None + self.__provisioning_sessions = {} + self.__ca_key = None + self.__ca = None + self.__log = logging.getLogger(__name__ + '.' + self.__class__.__name__) + + def __await__(self): + '''``await`` provider for asynchronous instansiation. + ''' + return self.__asyncInit().__await__() + + async def __asyncInit(self): + '''Asynchronous object instantiation + + Loads previous state from the DataStore. + + :meta private: + :return: self + ''' + await self.__reloadFromDataStore() + return self + + # Provisioning Session Management + + async def provisioningSessionIds(self) -> Iterable: + '''Get the list of current known provisioning session ids + + :return: an iterable for the provisioning session ids. + ''' + return self.__provisioning_sessions.keys() + + async def provisioningSessionProtocols(self, provisioning_session_id: ResourceId) -> Optional[ContentProtocols]: + '''Get the ContentProtocols for the existing provisioning session + + :param ResourceId provisioning_session_id: The provisioning session to get `ContentProtocols` for. + :return: a `ContentProtocols` for the provisioning session or ``None`` if the `ContentProtocols` could not be found. + ''' + if provisioning_session_id not in self.__provisioning_sessions: + return None + await self.__cacheProtocols(provisioning_session_id) + return self.__provisioning_sessions[provisioning_session_id]['protocols']['contentprotocols'] + + async def provisioningSessionCertificateIds(self, provisioning_session_id: ResourceId) -> Optional[List[ResourceId]]: + '''Get the list of certificate Ids for a provisioning session + + :param ResourceId provisioning_session_id: The provisioning session id to get the certificate ids for. + :return: a list of certificate ids associated with the *provisioning_session_id* or ``None`` if they could not be found. + ''' + if provisioning_session_id not in self.__provisioning_sessions: + return None + await self.__cacheProvisioningSession(provisioning_session_id) + ps = self.__provisioning_sessions[provisioning_session_id]['provisioningsession'] + if 'certificates' not in ps: + return [] + return ps['certificates'] + + async def provisioningSessionContentHostingConfiguration(self, provisioning_session_id: ResourceId) -> Optional[ContentHostingConfiguration]: + '''Get the ContentHostingConfiguration associated with the provisioning session + + :param ResourceId provisioning_session_id: The provisioning session id to get the `ContentHostingConfiguration` for. + :return: ``None`` if the provisioning session does not exist or if there is no `ContentHostingConfiguration` associated + with the provisioning session, otherwise return the `ContentHostingConfiguration`. + ''' + if provisioning_session_id not in self.__provisioning_sessions: + return None + await self.__cacheContentHostingConfiguration(provisioning_session_id) + ps = self.__provisioning_sessions[provisioning_session_id] + chc_resp = ps['content-hosting-configuration'] + if chc_resp is None: + # Nothing got cached from the AF, probably an error, but no CHC found + return None + chc = chc_resp['ContentHostingConfiguration'] + return chc + + async def provisioningSessionDestroy(self, provisioning_session_id: ResourceId) -> Optional[bool]: + '''Destroy a provisioning session + + :param provisioning_session_id: The provisioning session id of the session to destroy. + :return: ``True`` if the provisioning session was destroyed, ``False`` if it could not be destroyed or ``None`` if the + provisioning session does not exist. + ''' + if provisioning_session_id not in self.__provisioning_sessions: + return None + await self.__connect() + result = await self.__m1_client.destroyProvisioningSession(provisioning_session_id) + if result: + del self.__provisioning_sessions[provisioning_session_id] + if self.__data_store_dir: + await self.__data_store_dir.set('provisioning_sessions', list(self.__provisioning_sessions.keys())) + return True + return False + + async def provisioningSessionCreate(self, prov_type: ProvisioningSessionType, app_id: ApplicationId, asp_id: Optional[ApplicationId] = None) -> Optional[ResourceId]: + '''Create a provisioning session + + The *prov_type* should be `rt_m1_client.types.PROVISIONING_SESSION_TYPE_DOWNLINK` or + `rt_m1_client.types.PROVISIONING_SESSION_TYPE_UPLINK`. The *app_id* is the mandatory external application id, and the + *asp_id* is the optional ASP identfier. + + :param prov_type: The provisioning session type, either `PROVISIONING_SESSION_TYPE_DOWNLINK` or + `PROVISIONING_SESSION_TYPE_UPLINK`. + :param app_id: This is the External Application Id. + :param asp_id: This is the optional Application Service Provider Id. + + :return: the provisioning session id for the new provisioning session or ``None`` if the `ProvisioningSession` could not + be created. + ''' + await self.__connect() + prov_sess_resp: Optional[ProvisioningSessionResponse] = await self.__m1_client.createProvisioningSession(prov_type, app_id, asp_id) + if prov_sess_resp is None: + self.__log.debug("provisioningSessionCreate: no response") + return None + ps_id = prov_sess_resp['ProvisioningSessionId'] + # Register the provisioning session id + self.__provisioning_sessions[ps_id] = None + # Store in the `DataStore` if available + if self.__data_store_dir: + await self.__data_store_dir.set('provisioning_sessions', list(self.__provisioning_sessions.keys())) + return ps_id + + async def provisioningSessionIdByIngestUrl(self, ingesturl: str, entrypoint: Optional[str] = None) -> Optional[ResourceId]: + ret = None + for ps_id in self.__provisioning_sessions.keys(): + await self.__cacheContentHostingConfiguration(ps_id) + ps = await self.__getProvisioningSessionCache(ps_id) + if ps is None or ps['content-hosting-configuration'] is None: + continue + if ps['content-hosting-configuration']['contenthostingconfiguration']['ingestConfiguration']['baseURL'] == ingesturl: + if (entrypoint is None and 'entryPointPath' not in ps['content-hosting-configuration']['contenthostingconfiguration']) or (entrypoint is not None and 'entryPointPath' in ps['content-hosting-configuration']['contenthostingconfiguration'] and ps['content-hosting-configuration']['contenthostingconfiguration']['entryPointPath'] == entrypoint): + ret = ps_id + break + return ret + + # Certificates management + + async def certificateIds(self, provisioning_session_id: ResourceId) -> Optional[List[ResourceId]]: + '''Get a list of certificate Ids + + :param provisioning_session_id: The provisioning session id to retrieve certificate ids for. + :return: a list of certificate ids or ``None`` if the provisioning session doesn't exist or cannot be retrieved. + ''' + if provisioning_session_id not in self.__provisioning_sessions: + return None + await self.__cacheProvisioningSession(provisioning_session_id) + ps = self.__provisioning_sessions[provisioning_session_id]['provisioningsession'] + if 'serverCertificateIds' not in ps: + return [] + return ps['serverCertificateIds'] + + async def certificateCreate(self, provisioning_session_id: ResourceId) -> Optional[ResourceId]: + '''Create a new certificate + + This creates a new M1 Server signed certificate in the provisioning session and returns the new certificate id. + + :param provisioning_session_id: The provisioning session to create the new certificate in. + + :return: the certificate id of the new certificate or ``None`` if the provisioning session does not exist or if there was + no response from the M1 Server. + ''' + if provisioning_session_id not in self.__provisioning_sessions: + return None + await self.__connect() + cert_resp: ServerCertificateResponse = await self.__m1_client.createServerCertificate(provisioning_session_id) + if cert_resp is None: + return None + cert_id = cert_resp['ServerCertificateId'] + ps = await self.__getProvisioningSessionCache(provisioning_session_id) + if ps is not None: + if 'certificates' not in ps or ps['certificates'] is None: + # create certificates cache + ps['certificates'] = {cert_id: {k.lower(): v for k,v in cert_resp.items()}} + elif cert_id not in ps['certificates'] or ps['certificates'][cert_id] is None: + # Store new certificate info + ps['certificates'][cert_id] = {k.lower(): v for k,v in cert_resp.items()} + else: + # Update the certificate info + if cert_resp['ServerCertificate'] is None: + cert_resp['ServerCertificate'] = ps['certificates'][cert_id]['servercertificate'] + ps['certificates'][cert_id] = {k.lower(): v for k,v in cert_resp.items()} + + return cert_id + + async def certificateGet(self, provisioning_session_id: ResourceId, certificate_id: ResourceId) -> Optional[str]: + '''Retrieve a public certificate + + :param provisioning_session_id: The provisioning session id to use to look up the certificate. + :param certificate_id: The certificate id for the certificate in the provisioning session. + + :return: The PEM string for the public certificate or ``None`` if the certificate could not be found. + ''' + ret_err = None + if provisioning_session_id not in self.__provisioning_sessions: + return None + try: + await self.__cacheCertificates(provisioning_session_id) + except M1Error as err: + # This error may happen for a different certificate, so just remember it for now + ret_err = err + ps = self.__provisioning_sessions[provisioning_session_id] + # If the certificate does not exist return None + if 'certificates' not in ps or ps['certificates'] is None or certificate_id not in ps['certificates']: + return None + # If there was an error caching certificates and this certificate failed to cache then forward the exception + if ret_err is not None and ps['certificates'][certificate_id]['servercertificate'] is None: + raise ret_err + # Return the cached certificate + return ps['certificates'][certificate_id]['servercertificate'] + + async def certificateNewSigningRequest(self, provisioning_session_id: ResourceId) -> Optional[Tuple[ResourceId,str]]: + '''Create a new CSR for a provisioning session + + This reserves a new certificate in the provisioning session and returns the new certificate id and CSR PEM string. + It is the responsibility of the caller to generate a signed public certificate from the CSR and post it back to the M1 + Server using the `certificateSet` method. + + :param provisioning_session_id: The provisioning session to reserve the new certificate in. + + :return: a tuple of certificate id and CSR PEM string for the new certificate or ``None`` if the provisioning session does + not exist or if there was no response from the M1 Server. + ''' + if provisioning_session_id not in self.__provisioning_sessions: + return None + await self.__connect() + cert_resp: ServerCertificateSigningRequestResponse = await self.__m1_client.reserveServerCertificate( + provisioning_session_id) + if cert_resp is None: + return None + cert_id = cert_resp['ServerCertificateId'] + ps = await self.__getProvisioningSessionCache(provisioning_session_id) + if ps is not None: + if 'certificates' not in ps or ps['certificates'] is None: + ps['certificates'] = [cert_id] + elif cert_id not in ps['certificates']: + ps['certificates'] += [cert_id] + return (cert_id,cert_resp['CertificateSigningRequestPEM']) + + async def certificateSet(self, provisioning_session_id: ResourceId, certificate_id: ResourceId, pem: str) -> Optional[bool]: + '''Set the public certificate for a reserved certificate in a provisioning session + + This is used to provide a signed public certificate to the M1 Server after reserving the certificate with + `certificateNewSigningRequest`. This can only be done once per certificate reservation, once the public certificate is set + then further updates to it are not allowed. + + :param provisioning_session_id: The provisioning session id of the provisioning session to upload the certificate to. + :param certificate_id: The certificate id in the provisioning session to upload the certificate to. + :param pem: The public certificate as a PEM string to be uploaded. + + :return: ``True`` if the certificate was set, ``False`` if it has already been set and ``None`` if the provisioning + session or certificate id was not found. + ''' + if provisioning_session_id not in self.__provisioning_sessions: + return None + await self.__connect() + return await self.__m1_client.uploadServerCertificate(provisioning_session_id, certificate_id, pem) + + # ContentHostingConfiguration methods + + async def contentHostingConfigurationCreate(self, provisioning_session: ResourceId, chc: ContentHostingConfiguration) -> bool: + '''Store a new `ContentHostingConfiguration` for a provisioning session + + :param provisioning_session: The provisioning session id of the provisioning session to set the + `ContentHostingConfiguration` in. + :param chc: The `ContentHostingConfiguration` to set in the provisioning session. + :return: ``True`` if the new `ContentHostingConfiguration` was successfully set in the provisioning session or ``False`` if + the operation failed (e.g. because there was already a `ContentHostingConfiguration` set). + ''' + if provisioning_session not in self.__provisioning_sessions: + return False + await self.__connect() + chc_resp: Union[bool,ContentHostingConfigurationResponse] = await self.__m1_client.createContentHostingConfiguration( + provisioning_session, chc) + if isinstance(chc_resp,bool): + return chc_resp + ps = await self.__getProvisioningSessionCache(provisioning_session) + if ps is not None: + ps['content-hosting-configuration'] = {k.lower(): v for k,v in chc_resp.items()} + return True + + async def contentHostingConfigurationGet(self, provisioning_session: ResourceId) -> Optional[ContentHostingConfiguration]: + '''Retrieve the `ContentHostingConfiguration` set on a provisioning session + + :param provisioning_session: The provisioning session id to retrieve the `ContentHostingConfiguration` for. + + :return: a `ContentHostingConfiguration` for the provisioning session or ``None`` if the provisioning session does not + exist or if it has no `ContentHostingConfiguration` set. + ''' + if provisioning_session not in self.__provisioning_sessions: + return None + await self.__cacheContentHostingConfiguration(provisioning_session) + ps = await self.__getProvisioningSessionCache(provisioning_session) + if ps is None or ps['content-hosting-configuration'] is None: + return None + return ContentHostingConfiguration(ps['content-hosting-configuration']['contenthostingconfiguration']) + + async def contentHostingConfigurationUpdate(self, provisioning_session: ResourceId, chc: ContentHostingConfiguration) -> bool: + '''Update the `ContentHostingConfiguration` for a provisioning session + + :param provisioning_session: The provisioning session id of the provisioning session to set the + `ContentHostingConfiguration` in. + :param chc: The `ContentHostingConfiguration` to set in the provisioning session. + :return: ``True`` if the new `ContentHostingConfiguration` was successfully set in the provisioning session or ``False`` if + the operation failed (e.g. because there was no `ContentHostingConfiguration` set). + ''' + if provisioning_session not in self.__provisioning_sessions: + return False + await self.__connect() + return await self.__m1_client.updateContentHostingConfiguration(provisioning_session, chc) + + # Convenience methods + + async def createDownlinkPullProvisioningSession(self, app_id: ApplicationId, asp_id: Optional[ApplicationId] = None) -> Optional[ResourceId]: + '''Create a downlink provisioning session + + :param app_id: The mandatory external application id for the provisioning session. + :param asp_id: The optional ASP id for the provisioning session. + :return: the new provisioning session id or ``None`` if creation failed. + ''' + return await self.provisioningSessionCreate(PROVISIONING_SESSION_TYPE_DOWNLINK, app_id, asp_id) + + async def createNewCertificate(self, provisioning_session: ResourceId, domain_name_alias: Optional[str] = None) -> Optional[ResourceId]: + '''Create a new certificate + + This will create a new certificate for the provisioning session. If *domain_name_alias* is not given this will leave + creation of the certificate up to the M1 server (5GMS Application Function). If *domain_name_alias* is given and is not + ``None`` then this will reserve a certificate for the provisioning session, sign the CSR using the local `CertificateSigner` + object and set the signed public certificate for the provisioning session. + + :param provisioning_session: The provisioning session id of the provisioning session to create the certificate in. + :param domain_name_alias: An optional ``domainNameAlias`` to add to the certificate. + :return: The certificate id of the newly created certificate or ``None`` if the certificate could not be created. + ''' + # simple case just create the certificate + if domain_name_alias is not None and len(domain_name_alias) == 0: + domain_name_alias = None + if domain_name_alias is None: + return await self.certificateCreate(provisioning_session) + # When domainNameAlias is used we need to use a CSR + csr: Optional[Tuple[ResourceId,str]] = await self.certificateNewSigningRequest(provisioning_session) + if csr is None: + return None + cert_id = csr[0] + csr_pem = csr[1] + cert_signer = await self.__getCertificateSigner() + cert: str = await cert_signer.signCertificate(csr_pem, domain_name_alias=domain_name_alias) + # Send new cert to the AF + if not await self.certificateSet(provisioning_session, cert_id, cert): + self.__log.error('Failed to upload certificate with domainNameAlias') + return None + return cert_id + + async def createNewDownlinkPullStream(self, ingesturl: str, app_id: ApplicationId, entrypoint: Optional[str] = None, name: Optional[str] = None, asp_id: Optional[ApplicationId] = None, ssl: bool = False, insecure: bool = True, domain_name_alias: Optional[str] = None) -> ResourceId: + '''Create a new downlink pull stream + + This will create a new provisioning session, reserve any necessary certificates (if *ssl* is requested) and set the + `ContentHostingConfiguration`. + + The provisioning session is created with the *app_id* and *asp_id* provided. + + If *ssl* is ``True`` then a certificate will be created in the new provisioning session. This certificate will use the + *domain_name_alias* if set. + + The `ContentHostingConfiguration` set in the new provisioning session is created from the *ingesturl*, *entrypoint* and + *name* and will contain a ``distributionConfiguration`` for an HTTP distribution if *insecure* is ``True`` (the default) + and an HTTPS distribution, using the new certificate, if *ssl* is ``True`` (default is no HTTPS). + + :param ingesturl: The ingest URL for the `ContentHostingConfiguration` to create. + :param app_id: The external application id for creatation of the provisioning session. + :param entrypoint: Optional ``entryPointPath`` for the `ContentHostingConfiguration`. + :param name: Optional ``name`` for the `ContentHostingConfiguration`. + :param asp_id: Optional Application Service Provider Id for creating the provisioning session. + :param ssl: If ``True`` include an HTTPS ``distributionConfiguration`` in the `ContentHostingConfiguration`. + :param insecure: If ``True`` include an HTTP ``distributionConfiguration`` in the `ContentHostingConfiguration`. + :param domain_name_alias: Optional ``domainNameAlias`` to include in the ``distributionConfiguration`` in the + `ContentHostingConfiguration`. + + :return: The provisioning session id + :raise RuntimeError: if the creation of provisioning session, certificate or content hosting configuration fails. + ''' + # Abort if bad parameters + if not ssl and not insecure: + raise RuntimeError('Cannot create a stream without HTTP and HTTPS distributions.') + # Create a new provisioning session + provisioning_session: ResourceId = await self.provisioningSessionCreate(PROVISIONING_SESSION_TYPE_DOWNLINK, app_id, asp_id) + if provisioning_session is None: + raise RuntimeError('Failed to create a provisioning session') + # Create an SSL certificate if requested + if ssl: + cert: Optional[ResourceId] = await self.createNewCertificate(provisioning_session, domain_name_alias=domain_name_alias) + if cert is None: + if insecure: + self.__log.warn('Failed to create hosting with HTTPS, continuing with just HTTP') + else: + raise RuntimeError('Failed to create hosting, unable to create SSL certificate') + # If no name given, generate one + if name is None: + name = self.__next_auto_stream_name() + # Build and send the ContentHostingConfiguration + chc: ContentHostingConfiguration = { + 'name': name, + 'ingestConfiguration': { + 'pull': True, + 'protocol': 'urn:3gpp:5gms:content-protocol:http-pull-ingest', + 'baseURL': ingesturl, + }, + 'distributionConfigurations': [] + } + if ssl and cert is not None: + dc = {'certificateId': cert} + if domain_name_alias is not None: + dc['domainNameAlias'] = domain_name_alias + chc['distributionConfigurations'] += [dc] + if insecure: + dc = {} + if domain_name_alias is not None: + dc['domainNameAlias'] = domain_name_alias + chc['distributionConfigurations'] += [dc] + if entrypoint is not None: + chc['entryPointPath'] = entrypoint + if not await self.contentHostingConfigurationCreate(provisioning_session, chc): + raise RuntimeError('Failed to create the content hosting configuration') + return provisioning_session + + # Private methods + + async def __reloadFromDataStore(self) -> None: + '''Reload persistent information from the DataStore + + Checks the provisioning session ids retrieved from the DataStore against the M1 server and will delete any that are no + longer available. + + :meta private: + :return: None + ''' + if self.__data_store_dir is None: + return + + sessions = await self.__data_store_dir.get('provisioning_sessions'); + if sessions is None: + return + + # Check the provisioning session still exist with the AF + await self.__connect() + to_remove = [] + for prov_sess in sessions: + if await self.__m1_client.getProvisioningSessionById(prov_sess) is None: + to_remove += [prov_sess] + if len(to_remove) > 0: + for prov_sess in to_remove: + sessions.remove(prov_sess) + if self.__data_store_dir: + await self.__data_store_dir.set('provisioning_sessions', sessions) + + # Populate provisioning session resource keys + self.__provisioning_sessions = {} + for prov_sess in sessions: + self.__provisioning_sessions[prov_sess] = None + + async def __getProvisioningSessionCache(self, provisioning_session_id: ResourceId) -> Optional[dict]: + '''Find a provisioning session cache + + :meta private: + :param provisioning_session_id: The provisioning session id to get the cache for. + :return: The cache `dict` or ``None`` if the cache doesn't exist. + ''' + if provisioning_session_id not in self.__provisioning_sessions: + return None + await self.__cacheProvisioningSession(provisioning_session_id) + return self.__provisioning_sessions[provisioning_session_id] + + async def __cacheResources(self) -> None: + '''Cache the provisioning session resources lists + + Caches the provisioning session information for each known provisioning session + ''' + if len(self.__provisioning_sessions) == 0: + return + for prov_sess in self.__provisioning_sessions.keys(): + self.__cacheProvisioningSession(prov_sess) + + async def __cacheProvisioningSession(self, prov_sess: ResourceId) -> None: + '''Cache the provisioning session resource lists for a provisioning session + + Will only cache if the old cache didn't exist or has expired. + + :meta private: + :param prov_sess: The id of provisioning session to cache. + ''' + ps = self.__provisioning_sessions[prov_sess] + now = datetime.datetime.now(datetime.timezone.utc) + if ps is None or ps['cache-until'] is None or ps['cache-until'] < now: + await self.__connect() + result = await self.__m1_client.getProvisioningSessionById(prov_sess) + if result is not None: + if ps is None: + ps = {} + self.__provisioning_sessions[prov_sess] = ps + ps.update({k.lower(): v for k,v in result.items()}) + ps.update({ + 'protocols': None, + 'content-hosting-configuration': None, + 'certificates': None, + }) + if 'serverCertificateIds' in ps['provisioningsession']: + ps['certificates'] = {k: None for k in ps['provisioningsession']['serverCertificateIds']} + + async def __cacheProtocols(self, provisioning_session_id: ResourceId): + '''Cache the ContentProtocols for a provisioning session + + Will only cache if the old cache didn't exist or has expired. + + :meta private: + :param provisioning_session_id: The id of provisioning session to cache the `ContentProtocols` for. + ''' + await self.__cacheProvisioningSession(provisioning_session_id) + ps = self.__provisioning_sessions[provisioning_session_id] + now = datetime.datetime.now(datetime.timezone.utc) + if ps['protocols'] is None or ps['protocols']['cache-until'] is None or ps['protocols']['cache-until'] < now: + await self.__connect() + result = await self.__m1_client.retrieveContentProtocols(provisioning_session_id) + if result is not None: + if ps['protocols'] is None: + ps['protocols'] = {} + ps['protocols'].update({k.lower(): v for k,v in result.items()}) + + async def __cacheCertificates(self, provisioning_session_id: ResourceId): + '''Cache all public certificates for the provisioning session + + Will only cache if the old cache didn't exist or has expired. + + :meta private: + :param provisioning_session_id: The id of provisioning session to cache the public certificates for. + ''' + await self.__cacheProvisioningSession(provisioning_session_id) + ps = self.__provisioning_sessions[provisioning_session_id] + now = datetime.datetime.now(datetime.timezone.utc) + if ps['certificates'] is None: + return + ret_err = None + for cert_id,cert in list(ps['certificates'].items()): + if cert is None: + cert = {'etag': None, 'last-modified': None, 'cache-until': None, 'servercertificateid': cert_id, + 'servercertificate': None} + ps['certificates'][cert_id] = cert + if cert['cache-until'] is None or cert['cache-until'] < now: + await self.__connect() + try: + result = await self.__m1_client.retrieveServerCertificate(provisioning_session_id, cert_id) + if result is not None: + cert.update({k.lower(): v for k,v in result.items()}) + except M1Error as err: + if ret_err is None: + ret_err = err + if ret_err is not None: + raise ret_err + + async def __cacheContentHostingConfiguration(self, provisioning_session_id: ResourceId) -> None: + '''Cache the `ContentHostingConfiguration` for a provisioning session + + Will only cache if the old cache didn't exist or has expired. + + :meta private: + :param provisioning_session_id: The id of provisioning session to cache the `ContentHostingConfiguration` for. + ''' + await self.__cacheProvisioningSession(provisioning_session_id) + ps = self.__provisioning_sessions[provisioning_session_id] + now = datetime.datetime.now(datetime.timezone.utc) + chc = ps['content-hosting-configuration'] + if chc is None or chc['cache-until'] is None or chc['cache-until'] < now: + await self.__connect() + result = await self.__m1_client.retrieveContentHostingConfiguration(provisioning_session_id) + if result is not None: + if chc is None: + chc = {} + ps['content-hosting-configuration'] = chc + chc.update({k.lower(): v for k,v in result.items()}) + else: + ps['content-hosting-configuration'] = None + + async def __getCertificateSigner(self) -> CertificateSigner: + '''Get the `CertificateSigner` + + Creates the CertificateSigner object if we don't already have one. + + :meta private: + :return: a `CertificateSigner` + :raise RuntimeError: if the certificate signer requested is not derived from `CertificateSigner`. + ''' + if self.__cert_signer is None: + self.__cert_signer = 'rt_m1_client.certificates.DefaultCertificateSigner' + if isinstance(self.__cert_signer, str): + cert_sign_cls_mod, cert_sign_cls_name = self.__cert_signer.rsplit('.', 1) + cert_sign_cls_mod = importlib.import_module(cert_sign_cls_mod) + self.__cert_signer = getattr(cert_sign_cls_mod, cert_sign_cls_name) + if issubclass(self.__cert_signer, CertificateSigner): + self.__cert_signer = await self.__cert_signer(data_store=self.__data_store_dir) + if not isinstance(self.__cert_signer, CertificateSigner): + raise RuntimeError('The certificate signer class given is not derived from CertificateSigner') + return self.__cert_signer + + async def __connect(self) -> None: + '''Connect to the M1Client + + :meta private: + ''' + if self.__m1_client is None: + self.__m1_client = M1Client(self.__m1_host) + + def _dump_state(self) -> None: + '''Dump the current provisioning session cache to the log + ''' + self.__log.debug(repr(self.__provisioning_sessions)) + +__all__ = [ + # Classes + 'M1Session', + ] diff --git a/tools/python3/lib/rt_m1_client/types.py b/tools/python3/lib/rt_m1_client/types.py new file mode 100644 index 0000000..bf361d1 --- /dev/null +++ b/tools/python3/lib/rt_m1_client/types.py @@ -0,0 +1,434 @@ +#!/usr/bin/python3 +#============================================================================== +# 5G-MAG Reference Tools: M1 Client types +#============================================================================== +# +# File: rt_m1_client/types.py +# License: 5G-MAG Public License (v1.0) +# Author: David Waring +# Copyright: (C) 2023 British Broadcasting Corporation +# +# For full license terms please see the LICENSE file distributed with this +# program. If this file is missing then the license can be retrieved from +# https://drive.google.com/file/d/1cinCiA778IErENZ3JN52VFW-1ffHpx7Z/view +# +#============================================================================== +# +# M1 Client types +# =============== +# +# Defines various types from TS 29.571 and TS 26.512 used in M1 requests + +'''5G-MAG Reference Tools: M1 Client types +======================================= + +This module defines various types from TS 29.571 and TS 26.512 used in M1 requests. + +These types can be used in static type checking in Python 3. +''' +import enum +import json +from typing import List, Literal, TypedDict + +# TS 26.512 ProvisioningSession + +ApplicationId = str +ResourceId = str +Uri = str +ProvisioningSessionId = ResourceId +ProvisioningSessionType = Literal['DOWNLINK','UPLINK'] + +class ProvisioningSessionMandatory(TypedDict): + '''Madatory fields for a `ProvisioningSession` + ''' + provisioningSessionId: ProvisioningSessionId + provisioningSessionType: ProvisioningSessionType + externalApplicationId: ApplicationId + +class ProvisioningSession (ProvisioningSessionMandatory, total=False): + '''A `ProvisioningSession` object as defined in TS 26.512 + ''' + aspId: ApplicationId + + @staticmethod + def fromJSON(json_str: str) -> "ProvisioningSession": + '''Create a `ProvisioningSession` from a JSON string + + :param str json_str: The JSON string to convert to a `ProvisioningSession`. + + :return: a `ProvisioningSession` holding the data from the *json_str*. + :rtype: ProvisioningSession + :raise TypeError: if there is a problem with interpretting the *json_str* as a `ProvisioningSession`. + ''' + ret: dict = json.loads(json_str) + for mandatory_field in ProvisioningSessionMandatory.__required_keys__: + if mandatory_field not in ret: + raise TypeError(f'ProvisioningSession must contain a {mandatory_field} field: {json_str}') + if ret['provisioningSessionType'] not in ProvisioningSessionType.__args__: + raise TypeError(f'ProvisioningSession.provisioningSessionType must be one of: {", ".join(ProvisioningSessionType.__args__)}: {json_str}') + + return ProvisioningSession(ret) + +PROVISIONING_SESSION_TYPE_DOWNLINK: ProvisioningSessionType = 'DOWNLINK' #: Downlink `ProvisioningSessionType`. +PROVISIONING_SESSION_TYPE_UPLINK: ProvisioningSessionType = 'UPLINK' #: Uplink `ProvisioningSessionType`. + +# TS 26.512 ContentHostingConfiguration + +class PathRewriteRule(TypedDict): + '''PathRewriteRule structure in TS 26.512 + ''' + requestPathPattern: str #: A regex to match the request path. + mappedPath: str #: The path to map in instead of the matched path. + +class CachingDirectiveMandatory(TypedDict): + '''Mandatory fields from CachingConfiguration.cachingDirectives structure in TS 26.512 + ''' + noCache: bool #: ``True`` if ``no-cache`` should be included for this directive. + +class CachingDirective(CachingDirectiveMandatory, total=False): + '''CachingConfiguration.cachingDirectives structure in TS 26.512 + ''' + statusCodeFilters: List[int] #: A list of status codes to apply this cache directive for. + maxAge: int #: A ``max-age`` to apply for this directive. + +class CachingConfigurationMandatory(TypedDict): + '''Mandatory fields from CachingConfiguration structure in TS 26.512 + ''' + urlPatternFilter: str #: A URL pattern to match for the cache configuration + +class CachingConfiguration(CachingConfigurationMandatory, total=False): + '''CachingConfiguration structure in TS 26.512 + ''' + cachingDirectives: List[CachingDirective] #: Array of cache directives for the matched URL + +class ContentProtocolDescriptorMandatory(TypedDict): + '''Mandatory fields from ContentProtocolDescriptor in TS 26.512 + ''' + termIdentifier: Uri #: A URI (usually URN) to identify an ingest protocol. + +class ContentProtocolDescriptor(ContentProtocolDescriptorMandatory, total=False): + '''ContentProtocolDescriptor structure in TS 26.512 + ''' + descriptionLocator: Uri #: A URL to documentation describing the *termIdentfier*. + +class ContentProtocols(TypedDict, total=False): + '''ContentProtocols structure in TS 26.512 + ''' + downlinkIngestProtocols: List[ContentProtocolDescriptor] #: An array of available downlink ingest protocols. + uplinkEgestProtocols: List[ContentProtocolDescriptor] #: An array of available uplink ingest protocols. + geoFencingLocatorTypes: List[Uri] #: An array of available geo-fencing location types. + + @staticmethod + def fromJSON(json_str: str) -> "ContentProtocols": + '''Create a `ContentProtocols` from a JSON string + + :param str json_str: The JSON string to convert to a `ContentProtocols`. + :return: a `ContentProtocols` containing the data from the *json_str*. + :rtype: ContentProtocols + :raise TypeError: if the *json_str* could not be interpretted as a `ContentProtocols`. + ''' + return ContentProtocols(json.loads(json_str)) + +class DistributionNetworkType(enum.Enum): + '''Enumeration DistributionNetworkType in TS 26.512 + ''' + NETWORK_EMBMS = enum.auto() #: Distribution type is via EMBMS network. + + def __str__(self) -> str: + '''String representation of the `DistributionNetworkType`. + + :return: a `str` containing the name of the enumerated `DistributionNetworkType`. + ''' + return self.name + +class DistributionMode(enum.Enum): + '''Enumeration DistributionMode in TS 26.512 + ''' + MODE_EXCLUSIVE = enum.auto() #: Distribution mode is exclusive + MODE_HYBRID = enum.auto() #: Distribution mode is hybrid + MODE_DYNAMIC = enum.auto() #: Distribution mode is dynamic + + def __str__(self): + '''String representation of the `DistributionMode`. + + :return: a `str` containing the name of the enumerated `DistributionMode`. + ''' + return self.name + +class DistributionConfiguration(TypedDict, total=False): + ''' + DistributionConfiguration structure in TS 26.512 + ''' + contentPreparationTemplateId: ResourceId + canonicalDomainName: str + domainNameAlias: str + baseURL: Uri + pathRewriteRules: List[PathRewriteRule] + cachingConfigurations: List[CachingConfiguration] + geoFencing: TypedDict('GeoFencing', {'locatorType': str, 'locators': List[str]}) + urlSignature: TypedDict('URLSignature', { + 'urlPattern': str, + 'tokenName': str, + 'passphraseName': str, + 'passphrase': str, + 'tokenExpiryName': str, + 'useIPAddress': bool + }) + certificateId: ResourceId + supplementaryDistributionNetworks: List[TypedDict('SupplementaryDistributionNetwork', { + 'distributionNetworkType': DistributionNetworkType, + 'distributionMode': DistributionMode, + })] + +class IngestConfiguration(TypedDict, total=False): + ''' + IngestConfiguration structure from TS 26.512 + ''' + pull: bool + protocol: Uri + baseURL: Uri + +class ContentHostingConfigurationMandatory(TypedDict): + ''' + Mandatory fields from ContentHostingConfiguration structure in TS 26.512 + ''' + name: str + ingestConfiguration: IngestConfiguration + distributionConfigurations: List[DistributionConfiguration] + +class ContentHostingConfiguration(ContentHostingConfigurationMandatory, total=False): + ''' + ContentHostingConfiguration structure in TS 26.512 + ''' + entryPointPath: str + + @staticmethod + def fromJSON(chc_json: str) -> "ContentHostingConfiguration": + ''' + Generate a ContentHostingConfiguration structure from a JSON string + ''' + # parse the JSON + chc = json.loads(chc_json) + # convert enums + if 'distributionConfigurations' in chc: + for dist_conf in chc['distributionConfigurations']: + if 'supplementaryDistributionNetworks' in dist_conf: + for supp_net in dist_conf['supplementaryDistributionNetworks']: + supp_net['distributionNetworkType'] = DistributionNetworkType( + supp_net['distributionNetworkType']) + supp_net['distributionMode'] = DistributionMode( + supp_net['distributionMode']) + # Validate against ContentHostingConfiguration type + return ContentHostingConfiguration(chc) + + @classmethod + def format(cls, chc: "ContentHostingConfiguration") -> str: + '''Get a formatted `str` representation of a `ContentHostingConfiguration`. + + :param ContentHostingConfiguration chc: The `ContentHostingConfiguration` to format. + :return: a formatted `str` representation of the `ContentHostingConfiguration`. + ''' + return f'''Name: {chc['name']} +{cls.__formatEntryPoint(chc)}Ingest: + Type: {chc['ingestConfiguration']['protocol']} + URL: {chc['ingestConfiguration']['baseURL']} +Distributions: +{cls.__formatDistributions(chc, indent=2)} +''' + + @classmethod + def __formatDistributions(cls, chc: "ContentHostingConfiguration", indent: int = 0) -> str: + '''Format a ContentHostingConfiguration.distributionConfigurations + + :meta private: + :param ContentHostingConfiguration chc: The `ContentHostingConfiguration` to get the distributionConfigurations from. + :param int indent: The amount of spaces to indent the formatted distributionConfigurations by. + + :return: a `str` containing the distributionConfigurations as formatted text. + ''' + prefix = ' '*indent + dists = [] + for d in chc['distributionConfigurations']: + s = f"{prefix}- URL: {d['baseURL']}" + if 'canonicalDomainName' in d: + s += f"\n{prefix} Canonical Domain Name: {d['canonicalDomainName']}" + if 'contentPreparationTemplateId' in d: + s += f"\n{prefix} Content Preparation Template: {d['contentPreparationTemplateId']}" + if 'certificateId' in d: + s += f"\n{prefix} Certificate: {d['certificateId']}" + if 'domainNameAlias' in d: + s += f"\n{prefix} Domain Name Alias: {d['domainNameAlias']}" + if 'pathRewriteRules' in d: + s += f"\n{prefix} Path Rewrite Rules:" + for prr in d['pathRewriteRules']: + s += f"\n{prefix} - {prr['requestPathPattern']} => {prr['mappedPath']}" + if 'cachingConfigurations' in d: + s += f"\n{prefix} Caching Configurations:" + for cc in d['cachingConfigurations']: + s += f"\n{prefix} - URL Pattern: {cc['urlPatternFilter']}" + if 'cachingDirectives' in cc: + cd = cc['cachingDirectives'] + s += f"\n{prefix} Directive:" + s += f"\n{prefix} no-cache={repr(cd['noCache'])}" + if 'maxAge' in cd: + s += f"\n{prefix} max-age={cd['maxAge']}" + if 'statusCodeFilters' in cd: + s += f"\n{prefix} filters=[{', '.join([str(i) for i in cd['statusCodeFilters']])}]" + if 'geoFencing' in d: + gf = d['geoFencing'] + s += f"\n{prefix} Geo-fencing({gf['locatorType']}):" + for l in gf['locators']: + s += f"\n{prefix} - {l}" + if 'urlSignature' in d: + us = d['urlSignature'] + s += f"\n{prefix} URL Signature:" + s += f"\n{prefix} - Pattern: {us['urlPattern']}" + s += f"\n{prefix} Token: {us['tokenName']}" + s += f"\n{prefix} Passphase name: {us['passphraseName']}" + s += f"\n{prefix} Passphase: {us['passphrase']}" + s += f"\n{prefix} Token Expiry name: {us['tokenExpiryName']}" + s += f"\n{prefix} Use IP Address?: {repr(us['useIPAddress'])}" + if 'ipAddressName' in us: + s += f"\n{prefix} IP Address name: {us['ipAddressName']}" + dists += [s] + return '\n'.join(dists) + + @classmethod + def __formatEntryPoint(cls, chc: "ContentHostingConfiguration", indent: int = 0) -> str: + '''Format an ``entryPointPath`` as a string. + + :meta private: + :param ContentHostingConfiguration chc: The `ContentHostingConfiguration` to look for an ``entryPointPath`` in. + :param int indent: The amount of spaces to indent the formatted ``entryPointPath`` by. + + :return: the formatted ``entryPointPath`` if it exists or an empty string if it does not. + ''' + if 'entryPointPath' not in chc: + return '' + prefix = ' '*indent + return f"{prefix}Entry Point Path: {chc['entryPointPath']}\n" + +# TS 29.571 ProblemDetail +class InvalidParamMandatory(TypedDict): + ''' + Mandatory fields from InvalidParam structure in TS 29.571 + ''' + param: str + +class InvalidParam(InvalidParamMandatory, total=False): + ''' + InvalidParam structure from TS 29.571 + ''' + reason: str + +class AccessTokenErrError(enum.Enum): + ''' + AccessTokenErrError enumeration from TS 29.571 + ''' + invalid_request = enum.auto() # pylint: disable=invalid-name + invalid_client = enum.auto() # pylint: disable=invalid-name + invalid_grant = enum.auto() # pylint: disable=invalid-name + unauthorized_client = enum.auto() # pylint: disable=invalid-name + unsupported_grant_type = enum.auto() # pylint: disable=invalid-name + invalid_scope = enum.auto() # pylint: disable=invalid-name + + def __str__(self): + return self.name + +class AccessTokenErrMandatory(TypedDict): + ''' + Mandatory fields from AccessTokenErr structure in TS 29.571 + ''' + error: AccessTokenErrError + +class AccessTokenErr(AccessTokenErrMandatory, total=False): + ''' + AccessTokenErr structure in TS 29.571 + ''' + error_description: str + error_uri: str + +class AccessTokenReqGrantType(enum.Enum): + ''' + AccessTokenReqGrantType enumeration in TS 29.571 + ''' + client_credentials = enum.auto() # pylint: disable=invalid-name + + def __str__(self): + return self.name + +class AccessTokenReqMandatory(TypedDict): + ''' + Mandatory fields from AccessTokenReq structure in TS 29.571 + ''' + grant_type: AccessTokenReqGrantType + nfInstanceId: str + scope: str + +class AccessTokenReq(AccessTokenReqMandatory, total=False): + ''' + AccessTokenReq structure in TS 29.571 + ''' + nfType: str + targetNfType: str + targetNfInstanceId: str + requesterPlmn: str + requesterPlmnList: List[str] + requesterSnssaiList: List[str] + requesterFqdn: str + requesterSnpnList: List[str] + targetPlmn: str + targetSnssaiList: List[str] + targetNsiList: List[str] + targetNfSetId: str + targetNfServiceSetId: str + hnrfAccessTokenUri: str + sourceNfInstanceId: str + +class ProblemDetail(TypedDict, total=False): + ''' + ProblemDetail structure in TS 29.571 + ''' + problemtype: str + title: str + status: int + detail: str + instance: str + cause: str + invalidParams: List[InvalidParam] + supportedFeatures: str + accessTokenError: AccessTokenErr + accessTokenRequest: AccessTokenReq + nrfId: str + + @staticmethod + def fromJSON(problem_detail_json: str) -> "ProblemDetail": + ''' + Generate a `ProblemDetail` structure from a JSON string + + :param str problem_detail_json: The JSON string to convert to a `ProblemDetail`. + :return: a `ProblemDetail` containing the data from the *problem_detail_json* JSON string. + ''' + prob_detail = json.loads(problem_detail_json) + # Convert enumerated type strings to their enum values + if 'accessTokenError' in prob_detail: + for ate in prob_detail['accessTokenError']: + ate['error'] = AccessTokenErrError(ate['error']) + if 'accessTokenRequest' in prob_detail: + for atr in prob_detail['accessTokenRequest']: + atr['grant_type'] = AccessTokenReqGrantType(atr['grant_type']) + return prob_detail + +__all__ = [ + "ProblemDetail", + "AccessTokenErr", + "AccessTokenReq", + "InvalidParam", + "ApplicationId", + "ResourceId", + "ProvisioningSessionId", + "ProvisioningSessionType", + "ProvisioningSession", + "PROVISIONING_SESSION_TYPE_DOWNLINK", + "PROVISIONING_SESSION_TYPE_UPLINK", + ] diff --git a/tools/python3/m1_client_cli.py b/tools/python3/m1_client_cli.py new file mode 100755 index 0000000..2ce029d --- /dev/null +++ b/tools/python3/m1_client_cli.py @@ -0,0 +1,281 @@ +#!/usr/bin/python3 +#============================================================================== +# 5G-MAG Reference Tools: M1 Client CLI +#============================================================================== +# +# File: m1_client_cli.py +# License: 5G-MAG Public License (v1.0) +# Author: David Waring +# Copyright: (C) 2022 British Broadcasting Corporation +# +# For full license terms please see the LICENSE file distributed with this +# program. If this file is missing then the license can be retrieved from +# https://drive.google.com/file/d/1cinCiA778IErENZ3JN52VFW-1ffHpx7Z/view +# +#============================================================================== +# +# M1 Client CLI +# =============== +# +# This is a simple command line tool which will communicate with a 5GMS +# Application Function via the M1 interface. +# +'''5G-MAG Reference Tools: M1 Client CLI + +This provides a simple command line interface which can be used to manipulate +the configuration of a 5GMS Application Function via the M1 interface. + +Syntax: + m1-client -h + m1-client provisioning create [-h] (-d|-u) [] + m1-client provisioning show [-h] + m1-client provisioning delete [-h] + m1-client protocols [-h] + m1-client certificates create [-h] [--csr] + m1-client certificates upload [-h] + m1-client certificates show [-h] [--info] + m1-client certificates delete [-h] + m1-client hosting create [-h] + m1-client hosting show [-h] + m1-client hosting update [-h] + m1-client hosting delete [-h] + m1-client hosting purge [-h] [] +''' + +import argparse +import asyncio +import sys +from typing import Optional + +from rt_m1_client.client import M1Client, ProvisioningSessionResponse, ContentProtocolsResponse, ServerCertificateSigningRequestResponse, ServerCertificateResponse +from rt_m1_client.types import PROVISIONING_SESSION_TYPE_DOWNLINK +from rt_m1_client.exceptions import M1Error + +async def cmd_provisioning_create(args: argparse.Namespace) -> int: + client = await getClient(args) + asp_id = args.asp_id + app_id = args.external_app_id + if args.downlink: + prov_type = PROVISIONING_SESSION_TYPE_DOWNLINK + else: + prov_type = PROVISIONING_SESSION_TYPE_UPLINK + prov_sess_resp: Optional[ProvisioningSessionResponse] = await client.createProvisioningSession(prov_type, app_id, asp_id) + if prov_sess_resp is None: + print('Failed to create provisioning session') + return 1 + print(f"provisioning_session_id={prov_sess_resp['ProvisioningSessionId']}") + return 0 + +async def cmd_provisioning_show(args: argparse.Namespace) -> int: + client = await getClient(args) + provisioning_session_id = args.provisioning_session_id + prov_sess_resp: Optional[ProvisioningSessionResponse] = await client.getProvisioningSessionById(provisioning_session_id) + if prov_sess_resp is None: + print('Failed to fetch provisioning session') + return 1 + print(f"{prov_sess_resp['ProvisioningSession']!r}") + return 0 + +async def cmd_provisioning_delete(args: argparse.Namespace) -> int: + client = await getClient(args) + provisioning_session_id = args.provisioning_session_id + prov_sess_resp: bool = await client.destroyProvisioningSession(provisioning_session_id) + if not prov_sess_resp: + print('Failed to delete provisioning session') + return 1 + print('Provisioning session deleted') + return 0 + +async def cmd_protocols(args: argparse.Namespace) -> int: + client = await getClient(args) + provisioning_session_id = args.provisioning_session_id + resp: Optional[ContentProtocolsResponse] = await client.retrieveContentProtocols(provisioning_session_id) + if resp is None: + print('Failed to get ContentProtocols for provisioning session') + return 1 + print(f"{resp['ContentProtocols']!r}") + return 0 + +async def cmd_certificates_create(args: argparse.Namespace) -> int: + client = await getClient(args) + provisioning_session_id = args.provisioning_session_id + csr = args.csr + resp: Optional[ServerCertificateSigningRequestResponse] = await client.createOrReserveServerCertificate(provisioning_session_id, csr=csr) + if resp is None: + print('Failed to create a server certificate in the provisioning session') + return 1 + print(f"certificate_id={resp['ServerCertificateId']}") + if csr: + print(resp['CertificateSigningRequestPEM']) + return 0 + + +async def cmd_certificates_upload(args: argparse.Namespace) -> int: + client = await getClient(args) + provisioning_session_id = args.provisioning_session_id + certificate_id = args.certificate_id + pem_file = args.PEM_file + async with aiofiles.open(pem_file, mode='r') as in_file: + pem = await in_file.read() + resp: bool = await client.uploadServerCertificate(provisioning_session_id, certificate_id, pem) + if not resp: + print('Failed to upload public certificate') + return 1 + print('Public certificate uploaded') + return 0 + +async def cmd_certificates_show(args: argparse.Namespace) -> int: + client = await getClient(args) + provisioning_session_id = args.provisioning_session_id + certificate_id = args.certificate_id + info = args.info + resp: Optional[ServerCertificateResponse] = await client.retrieveServerCertificate(provisioning_session_id, certificate_id) + if resp is None: + print('Certificate pending upload') + return 1 + if info: + print(f'''certificate_id={resp['ServerCertificateId']} +** TODO: Use OpenSSL to print certificate details ** +''') + else: + print(resp['ServerCertificate']) + return 0 + +async def cmd_certificates_delete(args: argparse.Namespace) -> int: + raise NotImplementedError(__name__ + ' has not been implemented yet') + +async def cmd_hosting_create(args: argparse.Namespace) -> int: + raise NotImplementedError(__name__ + ' has not been implemented yet') + +async def cmd_hosting_show(args: argparse.Namespace) -> int: + raise NotImplementedError(__name__ + ' has not been implemented yet') + +async def cmd_hosting_update(args: argparse.Namespace) -> int: + raise NotImplementedError(__name__ + ' has not been implemented yet') + +async def cmd_hosting_delete(args: argparse.Namespace) -> int: + raise NotImplementedError(__name__ + ' has not been implemented yet') + +async def cmd_hosting_purge(args: argparse.Namespace) -> int: + raise NotImplementedError(__name__ + ' has not been implemented yet') + +async def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(prog='m1-client', description='M1 Client API tool') + subparsers = parser.add_subparsers(required=True) + + # Parent parser for AF address + parent_addr = argparse.ArgumentParser(add_help=False) + parent_addr.add_argument('address', metavar='address:port', help='Address of the 5GMS AF') + + # Parent parser for AF address and provisioning session id + parent_addr_prov = argparse.ArgumentParser(parents=[parent_addr], add_help=False) + parent_addr_prov.add_argument('provisioning-session-id', help='The provisioning session id') + + # m1-client provisioning ... + parser_provisioning = subparsers.add_parser('provisioning', help='Provisioning Session Management') + provisioning_subparsers = parser_provisioning.add_subparsers(required=True) + + # m1-client provisioning create [-h] (-d|-u) [] + parser_provisioning_create = provisioning_subparsers.add_parser('create', parents=[parent_addr], help='Create a new provisioning session') + parser_provisioning_create.set_defaults(command=cmd_provisioning_create) + parser_provisioning_create_type = parser_provisioning_create.add_mutually_exclusive_group(required=True) + parser_provisioning_create_type.add_argument('-d', '--downlink', help='Provisioning session is a downlink') + parser_provisioning_create_type.add_argument('-u', '--uplink', help='Provisioning session is an uplink') + parser_provisioning_create.add_argument('external-app-id', help='The external application id') + parser_provisioning_create.add_argument('asp-id', nargs='?', help='The Application Service Provider id') + + # m1-client provisioning show [-h] + parser_provisioning_show = provisioning_subparsers.add_parser('show', parents=[parent_addr_prov], help='Retreive and display a provisioning session') + parser_provisioning_show.set_defaults(command=cmd_provisioning_show) + + # m1-client provisioning delete [-h] + parser_provisioning_delete = provisioning_subparsers.add_parser('delete', parents=[parent_addr_prov], help='Delete a provisioning session') + parser_provisioning_delete.set_defaults(command=cmd_provisioning_delete) + + # m1-client protocols [-h] + parser_protocols = subparsers.add_parser('protocols', parents=[parent_addr_prov], help='Get the ContentProtocols for a provisioning session') + parser_protocols.set_defaults(command=cmd_protocols) + + # m1-client certificates ... + parser_certificates = subparsers.add_parser('certificates', help='ServerCertificatesProvisioning API') + certificates_subparsers = parser_certificates.add_subparsers(required=True) + + # m1-client certificates create [-h] [--csr] + parser_certificates_create = certificates_subparsers.add_parser('create', parents=[parent_addr_prov], help='Create or reserve a new certificate') + parser_certificates_create.set_defaults(command=cmd_certificates_create) + parser_certificates_create.add_argument('--csr', action='store_true', help='Reserve a certificate and return the CSR') + + # m1-client certificates upload [-h] + parser_certificates_upload = certificates_subparsers.add_parser('upload', parents=[parent_addr_prov], help='Upload a public certificate') + parser_certificates_upload.set_defaults(command=cmd_certificates_upload) + parser_certificates_upload.add_argument('certificate-id', help='The certificate id to upload') + parser_certificates_upload.add_argument('PEM-file', help='The public certificate PEM file to upload') + + # m1-client certificates show [-h] [--info] + parser_certificates_show = certificates_subparsers.add_parser('show', parents=[parent_addr_prov], help='Display a public certificate') + parser_certificates_show.add_argument('certificate-id', help='The certificate id to upload') + parser_certificates_show.add_argument('-i', '--info', action='store_true', help='Display certificate information instead of the PEM data') + + # m1-client certificates delete [-h] + parser_certificates_delete = certificates_subparsers.add_parser('delete', parents=[parent_addr_prov], help='Delete a certificate') + parser_certificates_show.add_argument('certificate-id', help='The certificate id to delete') + + # m1-client hosting ... + parser_hosting = subparsers.add_parser('hosting', help='ContentHostingProvisioing APIs') + hosting_subparsers = parser_hosting.add_subparsers(required=True) + + # m1-client hosting create [-h] + parser_hosting_create = hosting_subparsers.add_parser('create', parents=[parent_addr_prov], help='Add a ContentHostingConfiguration to a provisioning session') + parser_hosting_create.set_defaults(command=cmd_hosting_create) + parser_hosting_create.add_argument('CHC-JSON-file', help='Path to a ContentHostingConfiguration JSON file') + + # m1-client hosting show [-h] + parser_hosting_show = hosting_subparsers.add_parser('show', parents=[parent_addr_prov], help='Display the ContentHostingConfiguration for a provisioning session') + parser_hosting_show.set_defaults(command=cmd_hosting_show) + + # m1-client hosting update [-h] + parser_hosting_update = hosting_subparsers.add_parser('update', parents=[parent_addr_prov], help='Update the existing ContentHostingConfiguration in a provisioning session') + parser_hosting_update.set_defaults(command=cmd_hosting_update) + parser_hosting_update.add_argument('CHC-JSON-file', help='Path to a ContentHostingConfiguration JSON file') + + # m1-client hosting delete [-h] + parser_hosting_delete = hosting_subparsers.add_parser('delete', parents=[parent_addr_prov], help='Delete the ContentHostingConfiguration for a provisioning session') + parser_hosting_delete.set_defaults(command=cmd_hosting_delete) + + # m1-client hosting purge [-h] [] + parser_hosting_purge = hosting_subparsers.add_parser('purge', parents=[parent_addr_prov], help='Purge the cache for a provisioning session') + parser_hosting_purge.set_defaults(command=cmd_hosting_purge) + parser_hosting_purge.add_argument('path-regex', nargs='?', help='Regular expression to match for entries to purge') + + return parser.parse_args() + +async def getClient(args: argparse.Namespace) -> M1Client: + if not hasattr(args, 'address'): + raise RuntimeError('Attempt to connect to M1Client without an address') + (addr,port) = args.address.split(':') + port = int(port) + return M1Client((addr,port)) + +async def main(): + ''' + Async application entry point + ''' + try: + args = await parse_args() + if hasattr(args, 'command'): + return await args.command(args) + print('Command not understood') + return 1 + except M1Error as err: + print(f'Communication error: {err}') + return 2 + return 0 + +def app(): + ''' + Application entry point + ''' + return asyncio.run(main()) + +if __name__ == '__main__': + sys.exit(app()) diff --git a/tools/python3/m1_session_cli.py b/tools/python3/m1_session_cli.py new file mode 100755 index 0000000..492c246 --- /dev/null +++ b/tools/python3/m1_session_cli.py @@ -0,0 +1,804 @@ +#!/usr/bin/python3 +#============================================================================== +# 5G-MAG Reference Tools: M1 Session CLI +#============================================================================== +# +# File: m1_session_cli.py +# License: 5G-MAG Public License (v1.0) +# Author: David Waring +# Copyright: (C) 2023 British Broadcasting Corporation +# +# For full license terms please see the LICENSE file distributed with this +# program. If this file is missing then the license can be retrieved from +# https://drive.google.com/file/d/1cinCiA778IErENZ3JN52VFW-1ffHpx7Z/view +# +#============================================================================== +# +# M1 Session CLI +# =============== +# +# This is a command line tool to perform operations on the 5GMS Application +# Function via the M1 interface. +# +''' +====================================== +5G-MAG Reference Tools: M1 Session CLI +====================================== + +Perform operations on the 5GMS Application Function via the interface at +reference point M1. + +Syntax: + m1-session-cli -h + m1-session-cli configure -h + m1-session-cli configure show + m1-session-cli configure set + m1-session-cli configure get + m1-session-cli list -h + m1-session-cli list [-v] + m1-session-cli new-provisioning-session -h + m1-session-cli new-provisioning-session [-e ] [-a ] + m1-session-cli new-stream [-e ] [-a ] [-n ] [--with-ssl|--ssl-only] + [] + m1-session-cli del-stream -h + m1-session-cli del-stream -p + m1-session-cli del-stream [] + m1-session-cli set-stream -h + m1-session-cli set-stream -p + m1-session-cli new-certificate -h + m1-session-cli new-certificate -p [-d | --csr] + m1-session-cli show-certificate -h + m1-session-cli show-certificate -p -c + m1-session-cli set-certificate -h + m1-session-cli set-certificate -p -c [] + m1-session-cli check-certificates-renewal -h + m1-session-cli check-certificates-renewal + m1-session-cli renew-certificates -h + m1-session-cli renew-certificates -p + m1-session-cli renew-certificates [] + +Parameters: + -a ID --asp-id ID The application service provider id. + -c ID --certificate-id ID The certificate id to operate on. + -d FQDN --domain-name-alias FQDN The alternate domain name to use. + -e ID --external-app-id ID The external application id. + -h --help Display the help message. + -n NAME --name NAME The hosting name. + -p ID --provisioning-session-id ID The provisioning session id to use. + --ssl-only Provide HTTPS only. + --with-ssl Provide both HTTPS and HTTP. + +Arguments: + certificate-PEM-file The file path of a PEM holding a public certificate. + ContentHostingConfiguration-JSON The file path of a JSON file holding a ContentHostingConfiguration. + entry-point-suffix-URL Optional media entry URL path. + ingest-URL The base URL to fetch content from. + key The configuration field name. + value The configuration field value. +''' + +import aiofiles +import argparse +import asyncio +import configparser +import datetime +from io import StringIO +import logging +import os +import os.path +import sys +import traceback +from typing import Tuple, List + +import json +import OpenSSL + +from rt_m1_client.session import M1Session +from rt_m1_client.exceptions import M1Error +from rt_m1_client.data_store import JSONFileDataStore +from rt_m1_client.types import ContentHostingConfiguration + +class Configuration: + '''Application configuration container + + This class handles the loading and saving of the application configuration + ''' + + DEFAULT_CONFIG='''[DEFAULT] + log_dir = /var/log/rt-5gms + state_dir = /var/cache/rt-5gms + run_dir = /run/rt-5gms + + [m1-client] + log_level = info + data_store = %(state_dir)s/m1-client + m1_address = 127.0.0.23 + m1_port = 7777 + asp_id = + external_app_id = please-change-this + certificate_signing_class = rt_m1_client.certificates.DefaultCertificateSigner + ''' #: The default configuration + + def __init__(self): + '''Constructor + + Will load the previous configuration from ``/etc/rt-5gms/m1-client.conf`` if the command is run by root or + ``~/.rt-5gms/m1-client.conf`` if run by any other user. + ''' + self.__config_filename = None + if os.getuid() != 0: + self.__config_filename = os.path.expanduser(os.path.join('~', '.rt-5gms', 'm1-client.conf')) + else: + self.__config_filename = os.path.join(os.path.sep, 'etc', 'rt-5gms', 'm1-client.conf') + self.__default_config = configparser.ConfigParser() + self.__default_config.read_string(self.DEFAULT_CONFIG) + self.__config = configparser.ConfigParser() + self.__config.read_string(self.DEFAULT_CONFIG) + if os.path.exists(self.__config_filename): + self.__config.read(self.__config_filename) + + def isKey(self, key: str) -> str: + '''Does a configuration field key exist? + + This tests *key* for being a valid configuration option field key name. + + :returns: The key string if it is a valid configuration field key. + :raises: ValueError if the key string does not match a known configuration field key. + ''' + if key in self.__default_config['m1-client']: + return key + raise ValueError('Not a valid configuration option') + + def get(self, key: str, default: str = None, raw: bool = False) -> str: + '''Get a configuration value + + Retrieves the value for configuration option *key*. If the *key* does not exist the *default* will be returned. If *raw* is + ``True`` and the *key* option exists then the raw configuration (without ``%()`` interpolation) value will be returned. + + :returns: The configuration option *key* value or *default* if key does not exist. + ''' + return self.__config.get('m1-client', key, raw=raw, fallback=default) + + def set(self, key: str, value: str) -> bool: + '''Set a configuration value + + Sets the raw *value* for configuration option *key*. If *key* is not a valid configuration option then ValueError exception + will be raised. + + The configuration is saved once the *key* option has been set. + ''' + self.isKey(key) + if key in self.__default_config['DEFAULT']: + section = 'DEFAULT' + else: + section = 'm1-client' + self.__config.set(section, key, value) + self.__saveConfig() + return True + + def isDefault(self, key: str) -> bool: + '''Checks if a key contains the default configuration value + + :returns: ``True`` if the configuration value for *key* is the default value, or ``False`` otherwise. + ''' + return self.__config.get('m1-client', key) == self.__default_config.get('m1-client', key) + + def getKeys(self) -> List[str]: + '''Get a list of configuration field name keys + + :returns: A list of configuration key names. + ''' + return list(self.__default_config['m1-client'].keys()) + + def resetValue(self, key: str) -> bool: + '''Reset a configuration field to its default value + + :returns: ``True`` if the field was reset or ``False`` if the field already contained the default value. + ''' + if self.isDefault(key): + return False + return self.set(key, self.__default_config.get('m1-client', key)) + + def __saveConfig(self): + '''Save the current configuration to local storage + + :meta private-method: + + Will save the current configuration to the relevant local file. Fields with the default value will be saved as a comment. + ''' + cfgdir = os.path.dirname(self.__config_filename) + if not os.path.exists(cfgdir): + os.makedirs(cfgdir, mode=0o755) + with open(self.__config_filename, 'w') as cfgout: + for section in ['DEFAULT'] + self.__config.sections(): + cfgout.write(f'[{section}]\n') + for key in self.__config[section]: + cfgvalue = self.__config.get(section, key, raw=True) + defvalue = self.__default_config.get(section, key, raw=True) + if (section == 'DEFAULT' or key not in self.__config['DEFAULT']): + if cfgvalue == defvalue: + cfgout.write('#') + cfgout.write(f'{key} = {cfgvalue}\n') + cfgout.write('\n') + + def __str__(self): + '''String representation of the configuration + + :returns: A ``str`` representing the configuration. + ''' + buf = StringIO() + self.__config.write(buf) + return buf.getvalue() + + def __repr__(self): + '''Textual represnetation of the Configuration object + + :returns: A ``str`` representation of the Configuration object. + ''' + return f'Configuration(config="{self}")' + +async def cmd_configure_show(args: argparse.Namespace, config: Configuration) -> int: + '''Perform ``configure show`` operation + + Will write to stdout the current configuration. + ''' + default_marker = {True: ' (default)', False: ''} + print('Configuration settings:') + print('\n'.join([f'{key} = {config.get(key, raw=True)}{default_marker[config.isDefault(key)]}' for key in config.getKeys()])) + return 0 + +async def cmd_configure_reset(args: argparse.Namespace, config: Configuration) -> int: + '''Perform ``configure reset`` operation + + Will reset the configuration option *key* back to its default value. + ''' + config.resetValue(args.key) + return 0 + +async def cmd_configure_get(args: argparse.Namespace, config: Configuration) -> int: + '''Perform ``configure get`` operation + + Write to stdout an interpolated configuration option in the form ``=""``. This could be evaluated in an external + shell. + ''' + print(f'{args.key}={repr(config.get(args.key))}') + return 0 + +async def cmd_configure_set(args: argparse.Namespace, config: Configuration) -> int: + '''Perform ``configure set`` operation + + Set a configuration value and save the new configuration. + ''' + config.set(args.key, args.value) + return 0 + +def __formatX509Name(x509name: OpenSSL.crypto.X509Name) -> str: + '''Format an X509Name as a comma separated DN string + + :meta private: + :param OpenSSL.crypto.X509Name x509name: The X509 name to convert to a string. + :return: a ``str`` version of the X509 Name as comma separated DN fields. + :rtype: str + ''' + ret = ",".join([f"{name.decode('utf-8')}={value.decode('utf-8')}" for name,value in x509name.get_components()]) + return ret + +async def __prettyPrintCertificate(cert: str, indent: int = 0) -> None: + '''Print certificate information from X509 PEM data + + :param str cert: X509 certificate encoded as PEM data + :param int indent: The indent to use in the certificate output + ''' + x509 = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, cert) + serial = x509.get_serial_number() + subject = x509.get_subject() + issuer = x509.get_issuer() + start_str = x509.get_notBefore() + if isinstance(start_str, bytes): + start_str = start_str.decode('utf-8') + start = datetime.datetime.strptime(start_str, '%Y%m%d%H%M%SZ').replace(tzinfo=datetime.timezone.utc) + end_str = x509.get_notAfter() + if isinstance(end_str, bytes): + end_str = end_str.decode('utf-8') + end = datetime.datetime.strptime(end_str, '%Y%m%d%H%M%SZ').replace(tzinfo=datetime.timezone.utc) + subject_key = None + issuer_key = None + sans = [] + for ext_num in range(x509.get_extension_count()): + ext = x509.get_extension(ext_num) + ext_name = ext.get_short_name().decode('utf-8') + if ext_name == "subjectKeyIdentifier": + subject_key = str(ext) + elif ext_name == "authorityKeyIdentifier": + issuer_key = str(ext) + elif ext_name == "subjectAltName": + sans += [s.strip() for s in str(ext).split(',')] + cert_info_prefix=' '*indent + cert_desc=f'{cert_info_prefix}Serial = {serial}\n{cert_info_prefix}Not before = {start}\n{cert_info_prefix}Not after = {end}\n{cert_info_prefix}Subject = {__formatX509Name(subject)}\n' + if subject_key is not None: + cert_desc += f'{cert_info_prefix} key={subject_key}\n' + cert_desc += f'{cert_info_prefix}Issuer = {__formatX509Name(issuer)}' + if issuer_key is not None: + cert_desc += f'\n{cert_info_prefix} key={issuer_key}' + if len(sans) > 0: + cert_desc += f'\n{cert_info_prefix}Subject Alternative Names:' + cert_desc += ''.join([f'\n{cert_info_prefix} {san}' for san in sans]) + print(f'{cert_desc}') + +async def cmd_list_verbose(args: argparse.Namespace, config: Configuration) -> int: + '''Perform ``list -v`` operation + + Output to stdout a verbose list of the defined provisioning sessions and their resources. + ''' + session = await get_session(config) + for ps_id in await session.provisioningSessionIds(): + print(f'{ps_id}:') + certs = await session.certificateIds(ps_id) + print(' Certificates:') + for cert_id in certs: + print(f' {cert_id}:') + try: + cert = await session.certificateGet(ps_id, cert_id) + if cert is not None: + await __prettyPrintCertificate(cert, indent=6) + else: + print(' Certificate not yet uploaded') + except M1Error as err: + print(f' Certificate not available: {str(err)}') + chc = await session.contentHostingConfigurationGet(ps_id) + print(' ContentHostingConfiguration:') + if chc is not None: + print('\n'.join([' '+line for line in ContentHostingConfiguration.format(chc).split('\n')])) + else: + print(' Not defined') + return 0 + +async def cmd_list(args: argparse.Namespace, config: Configuration) -> int: + '''Perform ``list`` operation + + Output to stdout a list of the defined provisioning session ids, one per line. + ''' + if args.verbose: + return await cmd_list_verbose(args, config) + session = await get_session(config) + print('\n'.join(await session.provisioningSessionIds())) + return 0 + +async def cmd_new_provisioning_session(args: argparse.Namespace, config: Configuration) -> int: + '''Perform ``new-provisioning-session`` operation + + This will reserve a new, empty, provisioning session. + + Will output to stdout the result including the new provisioning session id. + ''' + session = await get_session(config) + app_id = args.app_id or config.get('external_app_id') + asp_id = args.asp_id or config.get('asp_id') + provisioning_session_id: Optional[ResourceId] = await session.createDownlinkPullProvisioningSession(app_id, asp_id=asp_id) + if provisioning_session_id is None: + print(f'Failed to create a new provisioing session') + return 1 + print(f'Provisioning session {provisioning_session_id} created') + return 0 + +async def cmd_set_stream(args: argparse.Namespace, config: Configuration) -> int: + '''Perform ``set-stream`` operation + + This will set the ContentHostingConfiguration for a provisioning session. + + Will output to stdout the result. + ''' + session = await get_session(config) + provisioning_session_id = args.provisioning_session + + async with aiofiles.open(args.file, 'r') as json_in: + chc = json.loads(await json_in.read()) + old_chc = await session.contentHostingConfigurationGet(provisioning_session_id) + if old_chc is None: + result = await session.contentHostingConfigurationCreate(provisioning_session_id, chc) + else: + # Remove any read-only fields + for dc in chc['distributionConfigurations']: + for strip_field in ['canonicalDomainName', 'baseURL']: + if strip_field in dc: + del dc[strip_field] + result = await session.contentHostingConfigurationUpdate(provisioning_session_id, chc) + if not result: + print(f'Failed to set hosting for provisioning session {provisioning_session_id}') + return 1 + print(f'Hosting set for provisioning session {provisioning_session_id}') + return 0 + +async def cmd_new_stream(args: argparse.Namespace, config: Configuration) -> int: + '''Perform ``new-stream`` operation + + This will generate and set the ContentHostingConfiguration for a provisioning session. If asked to provide an SSL distribution + point it will also generate the ServerCertificate within the provisioning session. + + Will output to stdout the result. + ''' + session = await get_session(config) + name = args.name + use_ssl = args.with_ssl or args.ssl_only + use_plain = not args.ssl_only + app_id = args.app_id or config.get('external_app_id') + asp_id = args.asp_id or config.get('asp_id') + domain_name_alias = args.domain_name_alias + provisioning_session_id = await session.createNewDownlinkPullStream(args.ingesturl, app_id, args.entrypoint, name=name, ssl=use_ssl, insecure=use_plain, asp_id=asp_id, domain_name_alias=domain_name_alias) + print(f'Hosting created as provisioning session {provisioning_session_id}') + return 0 + +async def cmd_delete_stream(args: argparse.Namespace, config: Configuration) -> int: + '''Perform ``delete-stream`` operation + + This will delete the provisioning session. + + This will remove the provisioning session and all its resources. + ''' + session = await get_session(config) + if args.provisioning_session is not None: + ps_id = args.provisioning_session + else: + ps_id = await session.provisioningSessionIdByIngestUrl(args.ingesturl, args.entrypoint) + if ps_id is None: + print('No such hosting session found') + return 1 + result = await session.provisioningSessionDestroy(ps_id) + if result is None: + print(f'Provisioning Session {ps_id} not found') + return 1 + if not result: + print(f'Failed to destroy Provisioning Session {ps_id}') + return 1 + print(f'Provisioning Session {ps_id} and all its resources were destroyed') + return 0 + +async def cmd_show_stream(args: argparse.Namespace, config: Configuration) -> int: + session = await get_session(config) + if args.provisioning_session is not None: + ps_id = args.provisioning_session + else: + ps_id = await session.provisioningSessionIdByIngestUrl(args.ingesturl, args.entrypoint) + if ps_id is None: + print('No such hosting session found') + return 1 + result = await session.contentHostingConfigurationGet(ps_id) + if result is None: + print(f'Provisioning Session {ps_id} does not have a ContentHostingConfiguration') + return 1 + if args.raw: + print(json.dumps(result, indent=2, sort_keys=True)) + else: + print(f'ContentHostingConfiguration for provisioning session {ps_id}:') + print('\n'.join([' '+line for line in ContentHostingConfiguration.format(result).split('\n')])) + return 0 + +async def cmd_protocols(args: argparse.Namespace, config: Configuration) -> int: + '''Perform ``protocols`` operation + + This will list the download and upload protocols for the provisioning session. + ''' + session = await get_session(config) + result = await session.provisioningSessionProtocols(args.provisioning_session) + if result is None: + print(f'Failed to fetch the content protocols for provisioning session {args.provisioning_session}') + return 1 + print(f'Protocols for {args.provisioning_session}:') + if 'downlinkIngestProtocols' in result: + print(' Downlink:') + print('\n'.join([f' {proto["termIdentifier"]}' for proto in result['downlinkIngestProtocols']])) + else: + print(' No downlink capability') + if 'uplinkEgestProtocols' in result: + print(' Uplink:') + print('\n'.join([f' {proto["termIdentifier"]}' for proto in result['uplinkEgestProtocols']])) + else: + print(' No uplink capability') + if 'geoFencingLocatorTypes' in result: + print(' Geo-fencing:') + print('\n'.join([f' {proto}' for proto in result['geoFencingLocatorTypes']])) + else: + print(' No geo-fencing capability') + return 0 + +async def cmd_new_certificate(args: argparse.Namespace, config: Configuration) -> int: + ''' Perform ``new-certificate`` operation + + This will create or reserve a new certificate in the provisioning session. + ''' + session = await get_session(config) + if args.csr: + result = await session.certificateNewSigningRequest(args.provisioning_session) + if result is None: + print('Failed to reserve certificate') + return 1 + cert_id, csr = result + print(f'certificate_id={cert_id}') + print(csr) + return 0 + cert_id = await session.createNewCertificate(args.provisioning_session, domain_name_alias=args.domain_name_alias) + if cert_id is None: + print('Failed to create certificate') + return 1 + print(f'certificate_id={cert_id}') + return 0 + +async def cmd_show_certificate(args: argparse.Namespace, config: Configuration) -> int: + ''' Perform ``show-certificate`` operation + + Display the certificate details for a given certificate. + ''' + session = await get_session(config) + result = await session.certificateGet(args.provisioning_session, args.certificate_id) + if result is None: + print(f'Unable to get certificate {args.certificate_id} for provisioning session {args.provisioning_session}') + return 1 + if args.raw: + print(result) + else: + print(f'Certificate details for {args.certificate_id}:') + await __prettyPrintCertificate(result, indent=2) + return 0 + +async def cmd_set_certificate(args: argparse.Namespace, config: Configuration) -> int: + ''' Perform ``set-certificate`` operation + + Set the public certificate for a ``new-certificate`` generated with the ``--csr`` flag. + ''' + session = await get_session(config) + if args.certificate_pem_file is None: + loop = asyncio.get_event_loop() + reader = asyncio.StreamReader() + protocol = asyncio.StreamReaderProtocol(reader) + await loop.connect_read_pipe(lambda: protocol, sys.stdin) + else: + reader = aiofiles.open(args.certificate_pem_file, 'r') + cert_pem = await reader.read() + await reader.close() + result = await session.certificateSet(args.provisioning_session, args.certificate_id, cert_pem) + if result is None: + print('Failed to set certificate') + return 1 + if not result: + print('Certificate already set') + return 1 + print('Certificate set') + return 0 + +async def cmd_check_all_renewal(args: argparse.Namespace, config: Configuration) -> int: + '''Perform ``check-all-renewal`` operation + + **TODO** + ''' + session = await get_session(config) + for ps_id in await session.provisioningSessionIds(): + chc = await session.getContentHostingConfiguration(ps_id) + # extract current cert ids + # get public cert for each cert id + # check for soon or past expiry + # request a new certificate + # change id in chc and remember old cert ids + # if any cert ids changed in chc upload replacement chc + # delete old certs + return 1 + +async def cmd_renew_certs(args: argparse.Namespace, config: Configuration) -> int: + '''Perform ``renew-certs`` operation + + **TODO** + ''' + session = await get_session(config) + ps_id = args.provisioning_session + chc = await session.getContentHostingConfiguration(ps_id) + # get list of unique cert ids in chc + # for each cert id in list + # request a new certificate + # change ids in chc for new cert id + # upload replacement chc + # delete old certs + return 1 + +async def parse_args() -> Tuple[argparse.Namespace,Configuration]: + '''Parse command line options and load app configuration + + :return: Tuple containing the command line arguments after validation and the app configuration + :rtype: Tuple[argparse.Namespace,Configuration] + ''' + cfg = Configuration() + + parser = argparse.ArgumentParser(prog='m1-session', description='M1 Session Tool') + subparsers = parser.add_subparsers(required=True) + + # m1-session-cli configure ... + parser_configure = subparsers.add_parser('configure', help='Local configuration') + configure_subparsers = parser_configure.add_subparsers(required=True) + # m1-session-cli configure show + parser_configure_show = configure_subparsers.add_parser('show', help='Show local configuration') + parser_configure_show.set_defaults(command=cmd_configure_show) + # m1-session-cli configure get + parser_configure_get = configure_subparsers.add_parser('get', help='Get local configuration value') + parser_configure_get.set_defaults(command=cmd_configure_get) + parser_configure_get.add_argument('key', metavar='KEY', type=cfg.isKey) + # m1-session-cli configure set + parser_configure_set = configure_subparsers.add_parser('set', help='Set local configuration value') + parser_configure_set.set_defaults(command=cmd_configure_set) + parser_configure_set.add_argument('key', metavar='KEY', type=cfg.isKey) + parser_configure_set.add_argument('value', metavar='VALUE') + # m1-session-cli configure reset + parser_configure_reset = configure_subparsers.add_parser('reset', help='Reset configuration value to its default') + parser_configure_reset.set_defaults(command=cmd_configure_reset) + parser_configure_reset.add_argument('key', metavar='KEY', type=cfg.isKey) + + # m1-session-cli list [-v] + parser_list = subparsers.add_parser('list', help='List provisioning sessions') + parser_list.set_defaults(command=cmd_list) + parser_list.add_argument('-v', '--verbose', required=False, action='store_true') + + # m1-session-cli new-stream [-e ] [-a ] [-n ] [--with-ssl|--ssl-only] [-d ] \ + # [] + parser_newstream = subparsers.add_parser('new-stream', help='Create a new ingest stream') + parser_newstream.set_defaults(command=cmd_new_stream) + parser_newstream.add_argument('-n', '--name', metavar='NAME', help='The name of the new stream', required=False) + parser_newstream.add_argument('-e', '--external-app-id', dest='app_id', metavar="APPLICATION-ID", help='The external application id to register the stream to', required=False) + parser_newstream.add_argument('-a','--asp-id', metavar="PROVIDER-ID", help="The Application Service Provider Id to use", required=False) + parser_newstream_ssl_options = parser_newstream.add_mutually_exclusive_group(required=False) + parser_newstream_ssl_options.add_argument('--with-ssl', action='store_true') + parser_newstream_ssl_options.add_argument('--ssl-only', action='store_true') + parser_newstream.add_argument('-d', '--domain-name-alias', dest='domain_name_alias', metavar='FQDN', help='Optional domain name alias for the distribution', required=False) + parser_newstream.add_argument('ingesturl', metavar='ingest-URL', help='The ingest URL prefix to use') + parser_newstream.add_argument('entrypoint', metavar='entry-point-path', nargs='?', + help='The media player entry point path suffix.') + + # m1-session-cli del-stream -p + # m1-session-cli del-stream [] + parser_delstream = subparsers.add_parser('del-stream', help='Delete an ingest stream') + parser_delstream.set_defaults(command=cmd_delete_stream) + parser_delstream_filter = parser_delstream.add_mutually_exclusive_group(required=True) + parser_delstream_filter.add_argument('-p', '--provisioning-session', help='Delete by provisioning session id') + parser_delstream_filter.add_argument('ingesturl', metavar='ingest-URL', nargs='?', help='The ingest URL prefix to use') + # The entry-point-path should go with ingest-URL, but argparser lacks the ability to do subgroups + parser_delstream.add_argument('entrypoint', metavar='entry-point-path', nargs='?', help='The media player entry point suffix.') + + # m1-session-cli set-stream -p + parser_set_stream = subparsers.add_parser('set-stream', help='Set the hosting for a provisioning session from a JSON file') + parser_set_stream.set_defaults(command=cmd_set_stream) + parser_set_stream.add_argument('-p', '--provisioning-session', help='The provisioning session id to set the hosting for', + required=True) + parser_set_stream.add_argument('file', metavar='CHC-JSON-FILE', help='A filepath to a JSON encoded ContentHostingConfiguration') + + # m1-session-cli show-stream (-p | []) [-r] + parser_show_stream = subparsers.add_parser('show-stream', + help='Display the ContentHostingConfiguration for a provisioning session') + parser_show_stream.set_defaults(command=cmd_show_stream) + parser_show_stream_filter = parser_show_stream.add_mutually_exclusive_group(required=True) + parser_show_stream_filter.add_argument('-p', '--provisioning-session', help='The provisioning session id to show', + required=False) + parser_show_stream_filter.add_argument('ingesturl', metavar='ingest-URL', nargs='?', help='The ingest URL prefix used') + parser_show_stream.add_argument('entrypoint', metavar='entry-point-path', nargs='?', + help='The media player entry point suffix.') + parser_show_stream.add_argument('-r', '--raw', required=False, action="store_true", + help='Use "raw" output mode to present the ContentHostingConfiguration JSON') + + # m1-session-cli new-provisioning-session [-e ] [-a ] + parser_new_provisioning_session = subparsers.add_parser('new-provisioning-session', help='Create a new provisioning session') + parser_new_provisioning_session.set_defaults(command=cmd_new_provisioning_session) + parser_new_provisioning_session.add_argument('-e', '--external-app-id', dest='app_id', metavar="APPLICATION-ID", + help='The external application id to register the stream to', required=False) + parser_new_provisioning_session.add_argument('-a','--asp-id', metavar="PROVIDER-ID", + help="The Application Service Provider Id to use", required=False) + + # m1-session-cli protocols -p + parser_protocols = subparsers.add_parser('protocols', + help='Get the available upload/download protocols for a provisioning session') + parser_protocols.set_defaults(command=cmd_protocols) + parser_protocols.add_argument('-p', '--provisioning-session', + help='Provisioning session id to list the upload and download protocols for') + + # m1-session-cli new-certificate -p [-d | --csr] + parser_new_certificate = subparsers.add_parser('new-certificate', help='Create a new certificate') + parser_new_certificate.set_defaults(command=cmd_new_certificate) + parser_new_certificate.add_argument('-p', '--provisioning-session', + help='Provisioning session id to create the new certificate for') + parser_new_certificate_extras = parser_new_certificate.add_mutually_exclusive_group(required=False) + parser_new_certificate_extras.add_argument('-d', '--domain-name-alias', dest='domain_name_alias', + help='FQDN to add as an extra domain name to the certificate') + parser_new_certificate_extras.add_argument('--csr', action='store_true', + help='Return a CSR to be signed externally and returned using set-certificate') + + # m1-session-cli show-certificate -p -c + parser_show_certificate = subparsers.add_parser('show-certificate', help='Retrieve a public certificate') + parser_show_certificate.set_defaults(command=cmd_show_certificate) + parser_show_certificate.add_argument('-p', '--provisioning-session', required=True, + help='Provisioning session id to show the certificate for') + parser_show_certificate.add_argument('-c', '--certificate-id', required=True, + help='The certificate id of the certificate to show') + parser_show_certificate.add_argument('-r', '--raw', required=False, action="store_true", + help='Use "raw" output mode to present the public certificate PEM data') + + # m1-session-cli set-certificate -p -c [] + parser_set_certificate = subparsers.add_parser('set-certificate', + help='Set the public certificate for a certificate created using --csr') + parser_set_certificate.set_defaults(command=cmd_set_certificate) + parser_set_certificate.add_argument('-p', '--provisioning-session', required=True, + help='Provisioning session id to set the certificate for') + parser_set_certificate.add_argument('-c', '--certificate-id', required=True, + help='The certificate id of the certificate to set') + parser_set_certificate.add_argument('certificate-PEM-file', nargs='?', + help='PEM file to load the public certificate from, if omitted will use stdin instead') + + # m1-session-cli check-certificate-renewal + #parser_checkrenewal = subparsers.add_parser('check-certificate-renewal', help='Renew all certificates if close to expiry') + #parser_checkrenewal.set_defaults(command=cmd_check_all_renewal) + + # m1-session-cli renew-certificate -p + # m1-session-cli renew-certificate [] + #parser_renewcert = subparsers.add_parser('renew-certificate', help='Force renewal of a specific certificate') + #parser_renewcert.set_defaults(command=cmd_renew_certs) + #parser_renewcert_filter = parser_renewcert.add_mutually_exclusive_group(required=True) + #parser_renewcert_filter.add_argument('-p', '--provisioning-session', help='Renew by provisioning session id') + #parser_renewcert_filter.add_argument('ingesturl', metavar='ingest-URL', nargs='?', help='The ingest URL prefix to use') + # The entry-point-path should go with ingest-URL, but argparser lacks the ability to do subgroups + #parser_renewcert.add_argument('entrypoint', metavar='entry-point-path', nargs='?', help='The media player entry point suffix.') + + args = parser.parse_args() + + return (args,cfg) + +_m1_session = None #: singleton variable for the M1Session object + +async def get_session(config: Configuration) -> M1Session: + '''Get the current M1Session object + + If the M1Session object does not exist, create it. + + :param Configuration config: The application configuration to use for connection information. + :return: the M1Session instance. + :rtype: M1Session + ''' + global _m1_session + if _m1_session is None: + data_store_dir = config.get('data_store') + if data_store_dir is not None: + data_store = await JSONFileDataStore(config.get('data_store')) + else: + data_store = None + _m1_session = await M1Session((config.get('m1_address', 'localhost'), config.get('m1_port',7777)), data_store, config.get('certificate_signing_class')) + return _m1_session + +async def main(): + ''' + Async application entry point + ''' + log_levels = { + 'debug': logging.DEBUG, + 'info': logging.INFO, + 'warn': logging.WARN, + 'error': logging.ERROR, + 'crit': logging.CRITICAL, + } + try: + (args, config) = await parse_args() + logging.basicConfig(level=log_levels[config.get('log_level')]) + log = logging.getLogger() + if hasattr(args, 'command'): + return await args.command(args, config) + else: + print(repr(parse_args())) + except M1Error as err: + print(f'Communication error: {err}') + return 2 + except Exception as err: + print(f'General failure: {err}') + #traceback.print_exc() # add in for debugging + return 2 + return 0 + +def app(): + ''' + Sync application entry point + ''' + logging.basicConfig(level=logging.INFO) + return asyncio.run(main()) + +if __name__ == '__main__': + sys.exit(app()) diff --git a/tools/python3/pylint.rc b/tools/python3/pylint.rc new file mode 100644 index 0000000..a338feb --- /dev/null +++ b/tools/python3/pylint.rc @@ -0,0 +1,615 @@ +[MAIN] + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Load and enable all available extensions. Use --list-extensions to see a list +# all available extensions. +#enable-all-extensions= + +# In error mode, messages with a category besides ERROR or FATAL are +# suppressed, and no reports are done by default. Error mode is compatible with +# disabling specific errors. +#errors-only= + +# Always return a 0 (non-error) status code, even if lint errors are found. +# This is primarily useful in continuous integration scripts. +#exit-zero= + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-allow-list= + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. (This is an alternative name to extension-pkg-allow-list +# for backward compatibility.) +extension-pkg-whitelist= + +# Return non-zero exit code if any of these messages/categories are detected, +# even if score is above --fail-under value. Syntax same as enable. Messages +# specified are enabled, while categories only check already-enabled messages. +fail-on= + +# Specify a score threshold to be exceeded before program exits with error. +fail-under=10 + +# Interpret the stdin as a python script, whose filename needs to be passed as +# the module_or_package argument. +#from-stdin= + +# Files or directories to be skipped. They should be base names, not paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the ignore-list. The +# regex matches against paths and can be in Posix or Windows format. +ignore-paths= + +# Files or directories matching the regex patterns are skipped. The regex +# matches against base names, not paths. The default value ignores Emacs file +# locks +ignore-patterns=^\.# + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use, and will cap the count on Windows to +# avoid hangs. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# Minimum Python version to use for version dependent checks. Will default to +# the version used to run pylint. +py-version=3.11 + +# Discover python modules and packages in the file system subtree. +recursive=no + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + +# In verbose mode, extra non-checker-related info will be displayed. +#verbose= + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'fatal', 'error', 'warning', 'refactor', +# 'convention', and 'info' which contain the number of messages in each +# category, as well as 'statement' which is the total number of statements +# analyzed. This score is used by the global evaluation report (RP0004). +evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +#output-format= + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, +# UNDEFINED. +confidence=HIGH, + CONTROL_FLOW, + INFERENCE, + INFERENCE_FAILURE, + UNDEFINED + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then re-enable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. If left empty, argument names will be checked with the set +# naming style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. If left empty, attribute names will be checked with the set naming +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. If left empty, class attribute names will be checked +# with the set naming style. +#class-attribute-rgx= + +# Naming style matching correct class constant names. +class-const-naming-style=UPPER_CASE + +# Regular expression matching correct class constant names. Overrides class- +# const-naming-style. If left empty, class constant names will be checked with +# the set naming style. +#class-const-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. If left empty, class names will be checked with the set naming style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. If left empty, constant names will be checked with the set naming +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. If left empty, function names will be checked with the set +# naming style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _ + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. If left empty, inline iteration names will be checked +# with the set naming style. +#inlinevar-rgx= + +# Naming style matching correct method names. +#method-naming-style=camelCase + +# Regular expression matching correct method names. Overrides method-naming- +# style. If left empty, method names will be checked with the set naming style. +method-rgx=^(?:__[a-zA-Z][a-zA-Z0-9_]*|_?[a-z][a-zA-Z0-9]*)$ + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. If left empty, module names will be checked with the set naming style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Regular expression matching correct type variable names. If left empty, type +# variable names will be checked with the set naming style. +#typevar-rgx= + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. If left empty, variable names will be checked with the set +# naming style. +#variable-rgx= + + +[CLASSES] + +# Warn about protected attribute access inside special methods +check-protected-access-in-special-methods=no + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=cls + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit,argparse.parse_error + + +[DESIGN] + +# List of regular expressions of class ancestor names to ignore when counting +# public methods (see R0903) +exclude-too-few-public-methods= + +# List of qualified class names to ignore when counting class parents (see +# R0901) +ignored-parents= + +# Maximum number of arguments for function / method. +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when caught. +overgeneral-exceptions=BaseException, + Exception + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules= + +# Output a graph (.gv or any supported image format) of external dependencies +# to the given file (report RP0402 must not be disabled). +ext-import-graph= + +# Output a graph (.gv or any supported image format) of all (i.e. internal and +# external) dependencies to the given file (report RP0402 must not be +# disabled). +import-graph= + +# Output a graph (.gv or any supported image format) of internal dependencies +# to the given file (report RP0402 must not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + +# Regular expression of note tags to take in consideration. +notes-rgx= + + +[SIMILARITIES] + +# Comments are removed from the similarity computation +ignore-comments=yes + +# Docstrings are removed from the similarity computation +ignore-docstrings=yes + +# Imports are removed from the similarity computation +ignore-imports=yes + +# Signatures are removed from the similarity computation +ignore-signatures=yes + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: en_AG (hunspell), en_AU +# (hunspell), en_BS (hunspell), en_BW (hunspell), en_BZ (hunspell), en_CA +# (hunspell), en_DK (hunspell), en_GB (hunspell), en_GH (hunspell), en_HK +# (hunspell), en_IE (hunspell), en_IN (hunspell), en_JM (hunspell), en_MW +# (hunspell), en_NA (hunspell), en_NG (hunspell), en_NZ (hunspell), en_PH +# (hunspell), en_SG (hunspell), en_TT (hunspell), en_US (hunspell), en_ZA +# (hunspell), en_ZM (hunspell), en_ZW (hunspell). +spelling-dict= + +# List of comma separated words that should be considered directives if they +# appear at the beginning of a comment and should not be checked. +spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of symbolic message names to ignore for Mixin members. +ignored-checks-for-mixins=no-member, + not-async-context-manager, + not-context-manager, + attribute-defined-outside-init + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# Regex pattern to define which classes are considered mixins. +mixin-class-rgx=.*[Mm]ixin + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of names allowed to shadow builtins +allowed-redefined-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io