From 3ba32ace53bd7b16e34d8d9cecae4912eb29e36b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Ku=C4=87ma?= Date: Tue, 28 May 2024 11:34:06 +0200 Subject: [PATCH] Anjay 3.8.0 BREAKING CHANGES: - Timeout in case of sending a Confirmable Notification does not cancel the observation anymore by default. Features: - Added a ``connection_error_is_registration_failure`` configuration option that allows handling connection errors as Register failures, including the automatic retry mechanism - Added experimental server connection status API. Improvements: - (commercial version only) Changed MSISDN matching method in SMS binding feature to allow handling messages with Short Codes as originating number Bugfixes: - Fixed a corner case in which a connection error during a non-first Register attempt could cause uncontrolled infinite retries - Fixed a bug in demo of Advanced Firmware Update module that prevented proper handling of security config for targets other than APP - Fixed a bug that caused anjay_next_planned_notify_trigger family APIs to return an invalid value after canceling observations --- CHANGELOG.md | 28 ++ CMakeLists.txt | 4 +- demo/advanced_firmware_update.c | 16 +- demo/advanced_firmware_update.h | 7 + demo/advanced_firmware_update_addimg.c | 11 +- demo/advanced_firmware_update_app.c | 16 +- demo/demo.c | 21 ++ demo/demo_args.c | 8 + demo/demo_args.h | 1 + demo/demo_cmds.c | 24 ++ demo/demo_utils.c | 24 ++ demo/demo_utils.h | 5 + deps/avs_coap/CMakeLists.txt | 1 + deps/avs_coap/doc/CMakeLists.txt | 1 + .../avsystem/coap/avs_coap_config.h.in | 14 +- .../src/async/avs_coap_async_server.c | 9 +- deps/avs_commons | 2 +- doc/sphinx/snippet_sources.md5 | 6 +- .../AT-NetworkErrorHandling.rst | 72 ++-- doc/sphinx/source/Migrating.rst | 1 + .../Migrating/MigratingFromAnjay214.rst | 3 +- .../Migrating/MigratingFromAnjay215.rst | 3 +- .../Migrating/MigratingFromAnjay225.rst | 3 +- .../source/Migrating/MigratingFromAnjay24.rst | 3 +- .../source/Migrating/MigratingFromAnjay26.rst | 3 +- .../source/Migrating/MigratingFromAnjay27.rst | 3 +- .../source/Migrating/MigratingFromAnjay28.rst | 3 +- .../source/Migrating/MigratingFromAnjay30.rst | 3 +- .../source/Migrating/MigratingFromAnjay32.rst | 3 +- .../source/Migrating/MigratingFromAnjay33.rst | 3 +- .../source/Migrating/MigratingFromAnjay34.rst | 5 +- .../source/Migrating/MigratingFromAnjay37.rst | 28 ++ .../embedded_lwm2m10/anjay/anjay_config.h | 7 + .../avsystem/coap/avs_coap_config.h | 14 +- .../embedded_lwm2m11/anjay/anjay_config.h | 7 + .../avsystem/coap/avs_coap_config.h | 14 +- .../linux_lwm2m10/anjay/anjay_config.h | 7 + .../avsystem/coap/avs_coap_config.h | 14 +- .../linux_lwm2m11/anjay/anjay_config.h | 7 + .../avsystem/coap/avs_coap_config.h | 14 +- include_public/anjay/anjay_config.h.in | 7 + include_public/anjay/core.h | 171 +++++++++ src/anjay_config_log.h | 10 + src/anjay_modules/anjay_servers.h | 9 + src/core/anjay_bootstrap_core.c | 33 ++ src/core/anjay_core.c | 13 +- src/core/anjay_core.h | 6 + src/core/anjay_dm_core.c | 8 + src/core/anjay_servers_private.h | 18 + src/core/io/anjay_base64_out.c | 8 +- src/core/io/anjay_json_encoder.c | 3 +- src/core/observe/anjay_observe_core.c | 53 +-- src/core/servers/anjay_activate.c | 107 ++++++ src/core/servers/anjay_connections.c | 98 +++++ src/core/servers/anjay_register.c | 39 +- src/core/servers/anjay_reload.c | 4 + src/core/servers/anjay_servers_internal.h | 34 +- .../suites/default/notifications.py | 54 +++ .../suites/default/retransmissions.py | 336 +++++++++++++----- 59 files changed, 1223 insertions(+), 206 deletions(-) create mode 100644 doc/sphinx/source/Migrating/MigratingFromAnjay37.rst diff --git a/CHANGELOG.md b/CHANGELOG.md index 83e8b130..58434034 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,33 @@ # Changelog +## 3.8.0 (May 28th, 2024) + +### BREAKING CHANGES + +- Timeout in case of sending a Confirmable Notification does not cancel the + observation anymore by default. + +### Features + +- Added a ``connection_error_is_registration_failure`` configuration option that + allows handling connection errors as Register failures, including the + automatic retry mechanism +- Added experimental server connection status API. + +### Improvements + +- (commercial version only) Changed MSISDN matching method in SMS binding + feature to allow handling messages with Short Codes as originating number + +### Bugfixes + +- Fixed a corner case in which a connection error during a non-first Register + attempt could cause uncontrolled infinite retries +- Fixed a bug in demo of Advanced Firmware Update module that prevented + proper handling of security config for targets other than APP +- Fixed a bug that caused anjay_next_planned_notify_trigger family APIs to + return an invalid value after canceling observations + ## 3.7.0 (February 16th, 2024) ### Features diff --git a/CMakeLists.txt b/CMakeLists.txt index 9b66190f..5f8c17e4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,7 +8,7 @@ cmake_minimum_required(VERSION 3.6.0) project(anjay C) -set(ANJAY_VERSION "3.7.0" CACHE STRING "Anjay library version") +set(ANJAY_VERSION "3.8.0" CACHE STRING "Anjay library version") set(ANJAY_BINARY_VERSION 1.0.0) set(ANJAY_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}") @@ -210,6 +210,7 @@ else() endif() option(WITH_AVS_COAP_TCP "Enable CoAP over TCP support" "${WITH_LWM2M11}") +option(WITH_CONN_STATUS_API "Enable support for the experimental anjay_get_server_connection_status() API and related callback." ON) @@ -557,6 +558,7 @@ set(ANJAY_WITH_SECURITY_STRUCTURED "${WITH_SECURITY_STRUCTURED}") set(ANJAY_WITH_SEND "${WITH_SEND}") set(ANJAY_WITH_SENML_JSON "${WITH_SENML_JSON}") set(ANJAY_WITHOUT_QUEUE_MODE_AUTOCLOSE "${WITHOUT_QUEUE_MODE_AUTOCLOSE}") +set(ANJAY_WITH_CONN_STATUS_API "${WITH_CONN_STATUS_API}") configure_file(include_public/anjay/anjay_config.h.in include_public/anjay/anjay_config.h) diff --git a/demo/advanced_firmware_update.c b/demo/advanced_firmware_update.c index 714754e8..f201862e 100644 --- a/demo/advanced_firmware_update.c +++ b/demo/advanced_firmware_update.c @@ -1206,7 +1206,7 @@ int advanced_firmware_update_install( (int) state.result); result = advanced_firmware_update_additional_image_install( anjay, fw_logic_add_inst->iid, fw_table, &state, - ADD_IMG_NAMES[i]); + security_info, ADD_IMG_NAMES[i]); if (result) { demo_log(ERROR, "AFU instance %u install failed", @@ -1261,3 +1261,17 @@ void advanced_firmware_update_uninstall(advanced_fw_update_logic_t *fw_table) { afu_logic_destroy(&fw_table[i]); } } + +int advanced_firmware_update_get_security_config( + anjay_iid_t iid, + void *fw_, + anjay_security_config_t *out_security_config, + const char *download_uri) { + (void) iid; + advanced_fw_update_logic_t *fw_table = (advanced_fw_update_logic_t *) fw_; + advanced_fw_update_logic_t *fw = &fw_table[FW_UPDATE_IID_APP]; + (void) download_uri; + memset(out_security_config, 0, sizeof(*out_security_config)); + out_security_config->security_info = fw->security_info; + return 0; +} diff --git a/demo/advanced_firmware_update.h b/demo/advanced_firmware_update.h index 88bbf898..e66eb334 100644 --- a/demo/advanced_firmware_update.h +++ b/demo/advanced_firmware_update.h @@ -102,6 +102,7 @@ int advanced_firmware_update_additional_image_install( anjay_iid_t iid, advanced_fw_update_logic_t *fw_table, anjay_advanced_fw_update_initial_state_t *init_state, + const avs_net_security_info_t *security_info, const char *component_name); const char * @@ -143,6 +144,12 @@ int fw_update_common_perform_upgrade( size_t requested_supplemental_iids_count); int fw_update_common_maybe_create_firmware_file(advanced_fw_update_logic_t *fw); +int advanced_firmware_update_get_security_config( + anjay_iid_t iid, + void *fw_, + anjay_security_config_t *out_security_config, + const char *download_uri); + typedef struct { anjay_advanced_fw_update_state_t inst_states[FW_UPDATE_IID_IMAGE_SLOTS]; anjay_advanced_fw_update_result_t inst_results[FW_UPDATE_IID_IMAGE_SLOTS]; diff --git a/demo/advanced_firmware_update_addimg.c b/demo/advanced_firmware_update_addimg.c index 1d742892..f856ecd0 100644 --- a/demo/advanced_firmware_update_addimg.c +++ b/demo/advanced_firmware_update_addimg.c @@ -92,7 +92,7 @@ static int update(advanced_fw_update_logic_t *fw) { return 0; } -static const anjay_advanced_fw_update_handlers_t handlers = { +static anjay_advanced_fw_update_handlers_t handlers = { .stream_open = fw_stream_open, .stream_write = fw_update_common_write, .stream_finish = fw_update_common_finish, @@ -107,10 +107,19 @@ int advanced_firmware_update_additional_image_install( anjay_iid_t iid, advanced_fw_update_logic_t *fw_table, anjay_advanced_fw_update_initial_state_t *init_state, + const avs_net_security_info_t *security_info, const char *component_name) { advanced_fw_update_logic_t *fw_logic = &fw_table[iid]; memcpy(fw_logic->current_ver, VER_DEFAULT, sizeof(VER_DEFAULT)); fw_global = fw_logic; + if (security_info) { + memcpy(&fw_logic->security_info, security_info, + sizeof(fw_logic->security_info)); + handlers.get_security_config = + advanced_firmware_update_get_security_config; + } else { + handlers.get_security_config = NULL; + } int result = anjay_advanced_fw_update_instance_add(anjay, fw_logic->iid, component_name, &handlers, diff --git a/demo/advanced_firmware_update_app.c b/demo/advanced_firmware_update_app.c index 48bbe259..16a1566b 100644 --- a/demo/advanced_firmware_update_app.c +++ b/demo/advanced_firmware_update_app.c @@ -189,19 +189,6 @@ static anjay_advanced_fw_update_handlers_t handlers = { .perform_upgrade = fw_update_common_perform_upgrade }; -static int fw_get_security_config(anjay_iid_t iid, - void *fw_, - anjay_security_config_t *out_security_config, - const char *download_uri) { - (void) iid; - advanced_fw_update_logic_t *fw_table = (advanced_fw_update_logic_t *) fw_; - advanced_fw_update_logic_t *fw = &fw_table[FW_UPDATE_IID_APP]; - (void) download_uri; - memset(out_security_config, 0, sizeof(*out_security_config)); - out_security_config->security_info = fw->security_info; - return 0; -} - int advanced_firmware_update_application_install( anjay_t *anjay, advanced_fw_update_logic_t *fw_table, @@ -215,7 +202,8 @@ int advanced_firmware_update_application_install( if (security_info) { memcpy(&fw_logic->security_info, security_info, sizeof(fw_logic->security_info)); - handlers.get_security_config = fw_get_security_config; + handlers.get_security_config = + advanced_firmware_update_get_security_config; } else { handlers.get_security_config = NULL; } diff --git a/demo/demo.c b/demo/demo.c index 14b22c91..06fcb93a 100644 --- a/demo/demo.c +++ b/demo/demo.c @@ -498,6 +498,21 @@ static void reschedule_notify_time_dependent(anjay_demo_t *demo) { } } +// !defined(ANJAY_WITH_CONN_STATUS_API) + +#ifdef ANJAY_WITH_CONN_STATUS_API +static void +server_connection_status_change_callback(void *demo_, + anjay_t *anjay, + anjay_ssid_t ssid, + anjay_server_conn_status_t status) { + (void) demo_; + (void) anjay; + demo_log(INFO, "Current status of the server with SSID %d is: %s", ssid, + translate_server_connection_status_enum_to_str(status)); +} +#endif // ANJAY_WITH_CONN_STATUS_API + static int demo_init(anjay_demo_t *demo, cmdline_args_t *cmdline_args) { demo->allocated_buffers = cmdline_args->allocated_buffers; cmdline_args->allocated_buffers = NULL; @@ -544,6 +559,8 @@ static int demo_init(anjay_demo_t *demo, cmdline_args_t *cmdline_args) { .update_immediately_on_dm_change = cmdline_args->update_immediately_on_dm_change, .enable_self_notify = cmdline_args->enable_self_notify, + .connection_error_is_registration_failure = + cmdline_args->connection_error_is_registration_failure, .default_tls_ciphersuites = { .ids = cmdline_args->default_ciphersuites, .num_ids = cmdline_args->default_ciphersuites_count @@ -555,6 +572,10 @@ static int demo_init(anjay_demo_t *demo, cmdline_args_t *cmdline_args) { #if defined(ANJAY_WITH_LWM2M11) && defined(WITH_AVS_COAP_TCP) .coap_tcp_request_timeout = cmdline_args->tcp_request_timeout, #endif // defined(ANJAY_WITH_LWM2M11) && defined(WITH_AVS_COAP_TCP) +#ifdef ANJAY_WITH_CONN_STATUS_API + .server_connection_status_cb = server_connection_status_change_callback, + .server_connection_status_cb_arg = demo, +#endif // ANJAY_WITH_CONN_STATUS_API }; #ifdef ANJAY_WITH_LWM2M11 diff --git a/demo/demo_args.c b/demo/demo_args.c index 81b1a8d1..88ac87bc 100644 --- a/demo/demo_args.c +++ b/demo/demo_args.c @@ -131,6 +131,7 @@ static const cmdline_args_t DEFAULT_CMDLINE_ARGS = { .prefer_hierarchical_formats = false, .update_immediately_on_dm_change = false, .enable_self_notify = false, + .connection_error_is_registration_failure = false, .prefer_same_socket_downloads = false, }; @@ -594,6 +595,9 @@ static void print_help(const struct option *options) { "over CoAP+TCP and HTTP" }, # endif // ANJAY_WITH_DOWNLOADER #endif // ANJAY_WITH_MODULE_SW_MGMT + { 348, NULL, NULL, + "Treat failures of the \"connect\" socket operation (e.g. (D)TLS " + "handshake failures) as a failed LwM2M Register operation." }, }; const size_t screen_width = get_screen_width(); @@ -994,6 +998,7 @@ int demo_parse_argv(cmdline_args_t *parsed_args, int argc, char *argv[]) { { "sw-mgmt-tcp-request-timeout", required_argument, 0, 347 }, # endif // ANJAY_WITH_DOWNLOADER #endif // ANJAY_WITH_MODULE_SW_MGMT + { "connection-error-is-registration-failure", no_argument, 0, 348 }, { 0, 0, 0, 0 } // clang-format on }; @@ -2001,6 +2006,9 @@ int demo_parse_argv(cmdline_args_t *parsed_args, int argc, char *argv[]) { } # endif // ANJAY_WITH_DOWNLOADER #endif // ANJAY_WITH_MODULE_SW_MGMT + case 348: + parsed_args->connection_error_is_registration_failure = true; + break; case 0: goto process; } diff --git a/demo/demo_args.h b/demo/demo_args.h index ceedfa3e..a38a6207 100644 --- a/demo/demo_args.h +++ b/demo/demo_args.h @@ -168,6 +168,7 @@ typedef struct cmdline_args { bool prefer_hierarchical_formats; bool update_immediately_on_dm_change; bool enable_self_notify; + bool connection_error_is_registration_failure; bool use_connection_id; bool start_offline; diff --git a/demo/demo_cmds.c b/demo/demo_cmds.c index 8ea2bb7e..38f41b5d 100644 --- a/demo/demo_cmds.c +++ b/demo/demo_cmds.c @@ -1430,6 +1430,24 @@ static void cmd_last_communication_time(anjay_demo_t *demo, } #endif // ANJAY_WITH_COMMUNICATION_TIMESTAMP_API +#ifdef ANJAY_WITH_CONN_STATUS_API +static void cmd_get_server_connection_status(anjay_demo_t *demo, + const char *args_string) { + anjay_ssid_t ssid = ANJAY_SSID_ANY; + anjay_server_conn_status_t result; + + if (*args_string && parse_ssid(args_string, &ssid)) { + demo_log(ERROR, "invalid Short Server ID: %s", args_string); + return; + } + + result = anjay_get_server_connection_status(demo->anjay, ssid); + + demo_log(INFO, "Current server connection status: %s", + translate_server_connection_status_enum_to_str(result)); +} +#endif // ANJAY_WITH_CONN_STATUS_API + static void cmd_help(anjay_demo_t *demo, const char *args_string); struct cmd_handler_def { @@ -1665,6 +1683,12 @@ static const struct cmd_handler_def COMMAND_HANDLERS[] = { "no argument specified) or a given server (if numeric SSID " "argument given)."), #endif // ANJAY_WITH_COMMUNICATION_TIMESTAMP_API +#ifdef ANJAY_WITH_CONN_STATUS_API + CMD_HANDLER("get-server-connection-status", "[SSID]", + cmd_get_server_connection_status, + "Displays current connection status for the server with SSID " + "specified by the argument."), +#endif // ANJAY_WITH_CONN_STATUS_API CMD_HANDLER("help", "", cmd_help, "Prints this message") // clang-format on }; diff --git a/demo/demo_utils.c b/demo/demo_utils.c index 16c164f3..8985ff5b 100644 --- a/demo/demo_utils.c +++ b/demo/demo_utils.c @@ -29,6 +29,30 @@ static struct { char **argv; } g_saved_args; +#ifdef ANJAY_WITH_CONN_STATUS_API +const char *translate_server_connection_status_enum_to_str( + anjay_server_conn_status_t status) { + static const char *const demo_server_connection_states_str[] = { + [ANJAY_SERV_CONN_STATUS_INVALID] = "INVALID", + [ANJAY_SERV_CONN_STATUS_ERROR] = "ERROR", + [ANJAY_SERV_CONN_STATUS_INITIAL] = "INITIAL", + [ANJAY_SERV_CONN_STATUS_CONNECTING] = "CONNECTING", + [ANJAY_SERV_CONN_STATUS_BOOTSTRAPPING] = "BOOTSTRAPPING", + [ANJAY_SERV_CONN_STATUS_BOOTSTRAPPED] = "BOOTSTRAPPED", + [ANJAY_SERV_CONN_STATUS_REGISTERING] = "REGISTERING", + [ANJAY_SERV_CONN_STATUS_REGISTERED] = "REGISTERED", + [ANJAY_SERV_CONN_STATUS_REG_FAILURE] = "REG_FAILURE", + [ANJAY_SERV_CONN_STATUS_DEREGISTERING] = "DEREGISTERING", + [ANJAY_SERV_CONN_STATUS_DEREGISTERED] = "DEREGISTERED", + [ANJAY_SERV_CONN_STATUS_SUSPENDING] = "SUSPENDING", + [ANJAY_SERV_CONN_STATUS_SUSPENDED] = "SUSPENDED", + [ANJAY_SERV_CONN_STATUS_REREGISTERING] = "REREGISTERING", + [ANJAY_SERV_CONN_STATUS_UPDATING] = "UPDATING" + }; + return demo_server_connection_states_str[status]; +} +#endif // ANJAY_WITH_CONN_STATUS_API + char **argv_get(void) { AVS_ASSERT(g_saved_args.argv, "argv_store not called before argv_get"); return g_saved_args.argv; diff --git a/demo/demo_utils.h b/demo/demo_utils.h index ce1d90ed..63a3a5b2 100644 --- a/demo/demo_utils.h +++ b/demo/demo_utils.h @@ -71,6 +71,11 @@ int copy_file_contents(FILE *dst, FILE *src); int calc_file_crc32(const char *filename, uint32_t *out_crc); +#ifdef ANJAY_WITH_CONN_STATUS_API +const char *translate_server_connection_status_enum_to_str( + anjay_server_conn_status_t status); +#endif // ANJAY_WITH_CONN_STATUS_API + #if defined(AVS_COMMONS_WITH_AVS_PERSISTENCE) \ && defined(AVS_COMMONS_STREAM_WITH_FILE) avs_error_t store_etag(avs_persistence_context_t *ctx, diff --git a/deps/avs_coap/CMakeLists.txt b/deps/avs_coap/CMakeLists.txt index 4d323b65..9cc79951 100644 --- a/deps/avs_coap/CMakeLists.txt +++ b/deps/avs_coap/CMakeLists.txt @@ -37,6 +37,7 @@ option(WITH_AVS_COAP_TCP "Enable CoAP over TCP support" ON) option(WITH_AVS_COAP_STREAMING_API "Enable streaming API" ON) option(WITH_AVS_COAP_OBSERVE "Enable support for observations" ON) +option(WITH_AVS_COAP_OBSERVE_CANCEL_ON_TIMEOUT "Turn on cancelling observation on a timeout " OFF) cmake_dependent_option(WITH_AVS_COAP_OBSERVE_PERSISTENCE "Enable observations persistence" ON "WITH_AVS_COAP_OBSERVE" OFF) option(WITH_AVS_COAP_BLOCK "Enable support for BLOCK/BERT transfers" ON) diff --git a/deps/avs_coap/doc/CMakeLists.txt b/deps/avs_coap/doc/CMakeLists.txt index 5aaa2a67..2cfc2068 100644 --- a/deps/avs_coap/doc/CMakeLists.txt +++ b/deps/avs_coap/doc/CMakeLists.txt @@ -48,6 +48,7 @@ if(EXISTS "${AVS_COAP_SOURCE_DIR}/Doxyfile.in") # TODO: List these flags automatically foreach(FLAG WITH_AVS_COAP_BLOCK WITH_AVS_COAP_OBSERVE + WITH_AVS_COAP_OBSERVE_CANCEL_ON_TIMEOUT WITH_AVS_COAP_OBSERVE_PERSISTENCE WITH_AVS_COAP_STREAMING_API WITH_AVS_COAP_TCP diff --git a/deps/avs_coap/include_public/avsystem/coap/avs_coap_config.h.in b/deps/avs_coap/include_public/avsystem/coap/avs_coap_config.h.in index edba0df2..b846abad 100644 --- a/deps/avs_coap/include_public/avsystem/coap/avs_coap_config.h.in +++ b/deps/avs_coap/include_public/avsystem/coap/avs_coap_config.h.in @@ -68,11 +68,23 @@ */ #cmakedefine WITH_AVS_COAP_OBSERVE +/** + * Turn on cancelling observation on a timeout. + * + * Only meaningful if WITH_AVS_COAP_OBSERVE is enabled. + * + * NOTE: LwM2M specification requires LwM2M server to send Cancel Observation + * request. Meanwhile CoAP RFC 7641 states that timeout on notification should + * cancel it. This setting is to enable both of these behaviors with default + * focused on keeping observations in case of bad connectivity. + */ +#cmakedefine WITH_AVS_COAP_OBSERVE_CANCEL_ON_TIMEOUT + /** * Enable support for observation persistence (avs_coap_observe_persist() * and avs_coap_observe_restore() calls). * - * Only meaningful WITH_AVS_COAP_OBSERVE is enabled. + * Only meaningful if WITH_AVS_COAP_OBSERVE is enabled. */ #cmakedefine WITH_AVS_COAP_OBSERVE_PERSISTENCE diff --git a/deps/avs_coap/src/async/avs_coap_async_server.c b/deps/avs_coap/src/async/avs_coap_async_server.c index 68d692ff..90cebd78 100644 --- a/deps/avs_coap/src/async/avs_coap_async_server.c +++ b/deps/avs_coap/src/async/avs_coap_async_server.c @@ -314,17 +314,16 @@ send_result_handler(avs_coap_ctx_t *ctx, }, code); } -#ifdef WITH_AVS_COAP_OBSERVE +#if defined(WITH_AVS_COAP_OBSERVE) \ + && defined(WITH_AVS_COAP_OBSERVE_CANCEL_ON_TIMEOUT) else if (fail_err.category == AVS_COAP_ERR_CATEGORY && fail_err.code == AVS_COAP_ERR_TIMEOUT) { - // According to RFC 7641, when trying to send a Confirmable notification - // ends in a timeout, the client "is considered no longer interested in - // the resource and is removed by the server from the list of observers" avs_coap_observe_cancel(ctx, (avs_coap_observe_id_t) { .token = token }); } -#endif // WITH_AVS_COAP_OBSERVE +#endif /* defined(WITH_AVS_COAP_OBSERVE) && \ + defined(WITH_AVS_COAP_OBSERVE_CANCEL_ON_TIMEOUT) */ // delivery status handler or observe cancel handler // might have canceled the exchange diff --git a/deps/avs_commons b/deps/avs_commons index 683a8cb3..2885ea16 160000 --- a/deps/avs_commons +++ b/deps/avs_commons @@ -1 +1 @@ -Subproject commit 683a8cb3d92efd094ff7dafc8322c8fce4666dbe +Subproject commit 2885ea16a2662c3b4ea2c245f3eac408c02e9d73 diff --git a/doc/sphinx/snippet_sources.md5 b/doc/sphinx/snippet_sources.md5 index c17470e4..268ce0b6 100644 --- a/doc/sphinx/snippet_sources.md5 +++ b/doc/sphinx/snippet_sources.md5 @@ -1,9 +1,9 @@ 563b9fc06ed0c55bae149f839ab80f11 deps/avs_coap/include_public/avsystem/coap/tcp.h 8d609e7d1e46df8b37f8d694f6d78e7f deps/avs_coap/include_public/avsystem/coap/udp.h 6979acbbeda62d10befc6f7aa7e0865e deps/avs_commons/include_public/avsystem/commons/avs_addrinfo.h -65c5f1a934166ea7ffe0b427f87e0d2d deps/avs_commons/include_public/avsystem/commons/avs_base64.h +0e56e80e053a9afedc7a714f2acbcc7b deps/avs_commons/include_public/avsystem/commons/avs_base64.h e9bc875ab314a3d88787889cfe282f8f deps/avs_commons/include_public/avsystem/commons/avs_crypto_common.h -9f284929ca4c327e8955e3e01fe8f007 deps/avs_commons/include_public/avsystem/commons/avs_crypto_pki.h +c351a8bd3738a441a32749df2f1489ca deps/avs_commons/include_public/avsystem/commons/avs_crypto_pki.h e23cc205e18c2bc699b7805ff2f04fe3 deps/avs_commons/include_public/avsystem/commons/avs_crypto_psk.h a9cad6508cf9c61d88f0dc809848bc9d deps/avs_commons/include_public/avsystem/commons/avs_socket.h 99c2b66c9d05b8af9138472a43014197 deps/avs_commons/include_public/avsystem/commons/avs_socket_v_table.h @@ -84,7 +84,7 @@ f971df7872a8f052bb72ae64610a8031 examples/tutorial/firmware-update/basic-implem 077f6b89dad59ef3b9b58e2abe93aff6 examples/tutorial/firmware-update/secure-downloads/src/firmware_update.c 99ef6e1d741fbfefab9ddf0db97c61e7 include_public/anjay/advanced_fw_update.h be8a4c22a41c2ab79bf3a68f1f74f301 include_public/anjay/attr_storage.h -6434100b9f5206dd350885fdab853504 include_public/anjay/core.h +8da010b13c862c6730a6e41624f16266 include_public/anjay/core.h 9771c6bc4940d778a12ab93cb400a11c include_public/anjay/dm.h 56b500ecbd5ffc8d468327c650883cbd include_public/anjay/fw_update.h b49ca67d9f4f04bf20261c261ad41822 include_public/anjay/io.h diff --git a/doc/sphinx/source/AdvancedTopics/AT-NetworkErrorHandling.rst b/doc/sphinx/source/AdvancedTopics/AT-NetworkErrorHandling.rst index 4d7f13bf..6e23e6ab 100644 --- a/doc/sphinx/source/AdvancedTopics/AT-NetworkErrorHandling.rst +++ b/doc/sphinx/source/AdvancedTopics/AT-NetworkErrorHandling.rst @@ -23,12 +23,14 @@ conditions happen while performing each of the client-initiated operations. | | Request | Register | Update | De-register | Notify | | | Bootstrap | | | | (confirmable) | +=================+==================+==================+==================+=============+===================+ -| **Timeout | Retry DTLS | Retry DTLS | Fall back | Ignored | Cancel | -| (DTLS)** [#t]_ | handshake [#hs]_ | handshake [#hs]_ | to Register | | observation | -+-----------------+------------------+------------------+ | | | -| **Timeout | Abort all | :ref:`Abort | | | | -| (NoSec)** [#t]_ | communication | registration | | | | -+-----------------+ [#a]_ | ` +------------------+ +-------------------+ +| **Timeout | Retry DTLS | Retry DTLS | Fall back | Ignored | Ignored by | +| (DTLS)** [#t]_ | handshake [#hs]_ | handshake [#hs]_ | to Register | | default; | ++-----------------+------------------+------------------+ | | configurable; | +| **Timeout | Abort all | :ref:`Abort | | | will be retried | +| (NoSec)** [#t]_ | communication | registration | | | whenever | +| | [#a]_ | ` | | | next notification | +| | | | | | is scheduled | ++-----------------+ | +------------------+ +-------------------+ | **Network | | | Fall back to | | Fall back to | | (e.g. ICMP) | | | Client-Initiated | | Client-Initiated | | error** | | | Bootstrap [#bs]_ | | Bootstrap [#bs]_ | @@ -73,20 +75,50 @@ condition is equivalent with the "fall back to Client-Initiated Bootstrap" Other error conditions ---------------------- -* **DTLS handshakes** are performed by the DTLS backend library used. This - includes handling non-fatal errors and retransmissions. In case of no response - from the server, DTLS handshake retransmissions are expected to follow - `RFC 6347, Section 4.2.4. Timeout and Retransmission - `_. - The handshake timers can be customized during Anjay initialization, by setting - `anjay_configuration_t::udp_dtls_hs_tx_params - <../api/structanjay__configuration.html#ab8ca076537138e7d78bd1ee5d5e2031a>`_. - - In case of the ultimate timeout, network-layer error, or an internal error - during the handshake attempt, Anjay will fall back to Client-Initiated - Bootstrap [#bs]_ or, if the attempt was to connect to a Bootstrap Server, - cease any attempts to communicate with it (note that unless regular Server - accounts are available, this will mean abortion of all communication [#a]_). +* **Connect operation errors** can occur for several reasons, the most common + being: + + * **(D)TLS handshake errors.** Handshakes are performed by the TLS backend + library used. This includes handling non-fatal errors and retransmissions. + In case of no response from the server, DTLS handshake retransmissions are + expected to follow `RFC 6347, Section 4.2.4. Timeout and Retransmission + `_. The handshake timers + can be customized during Anjay initialization, by setting + `anjay_configuration_t::udp_dtls_hs_tx_params + <../api/structanjay__configuration.html#ab8ca076537138e7d78bd1ee5d5e2031a>`_. + + Ultimate timeout, network-layer errors, and internal errors during the + handshake attempt will be treated as a failure of the "connect" operation. + + * **Domain name resolution errors.** If the ``getaddrinfo()`` call (or + equivalent) fails to return any usable IP address, this is also treated as + a failure of the "connect" operation. + + * **TCP handshake errors.** While the actual socket-level "connect" operation + does not involve any network communication for UDP and as such can almost + never fail, it performs actual handshake in case of TCP. Failure of this + handshake is also treated in the same way as the other cases mentioned here. + + * In some cases, **inconsistent data model state** may be treated equivalently + to a connection error, e.g. when there is no Security object instance that + would match a given Server object instance. + + Note that all of the operations mentioned above (domain name resolution and + both TCP and (D)TLS handshakes) are performed synchronously and will block all + other operations. + + If any of the above conditions happen, Anjay will, by default, fall back to + Client-Initiated Bootstrap [#bs]_ or, if the attempt was to connect to + a Bootstrap Server, cease any attempts to communicate with it (note that + unless regular Server accounts are available, this will mean abortion of all + communication [#a]_). + + This behavior can be changed by enabling the + `connection_error_is_registration_failure + <../api/structanjay__configuration.html#adcc95609ca645a5bd6a572f4c99a83fb>`_. + In that case, connection errors will trigger :ref:`err-abort-reg`, and thus + the automatic retry flow described in "Bootstrap and LwM2M Server Registration + Mechanisms" section mentioned above will be respected. * Errors while receiving an incoming request, or any unrecognized incoming packets, will be ignored diff --git a/doc/sphinx/source/Migrating.rst b/doc/sphinx/source/Migrating.rst index 545086e1..6aa7a560 100644 --- a/doc/sphinx/source/Migrating.rst +++ b/doc/sphinx/source/Migrating.rst @@ -35,3 +35,4 @@ Migrating from older versions Migrating/MigratingFromAnjay32 Migrating/MigratingFromAnjay33 Migrating/MigratingFromAnjay34 + Migrating/MigratingFromAnjay37 diff --git a/doc/sphinx/source/Migrating/MigratingFromAnjay214.rst b/doc/sphinx/source/Migrating/MigratingFromAnjay214.rst index a5299ff7..85ad7eaa 100644 --- a/doc/sphinx/source/Migrating/MigratingFromAnjay214.rst +++ b/doc/sphinx/source/Migrating/MigratingFromAnjay214.rst @@ -132,8 +132,7 @@ Changed flow of cancelling observations in case of errors ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ CoAP observations are implicitly cancelled if a notification bearing a 4.xx or -5.xx error code is delivered, or if an attempt to deliver a notification times -out. +5.xx error code is delivered. In Anjay 3.4.x and earlier, this cancellation (which involves calling the ``avs_coap_observe_cancel_handler_t`` callback) was performed *before* calling diff --git a/doc/sphinx/source/Migrating/MigratingFromAnjay215.rst b/doc/sphinx/source/Migrating/MigratingFromAnjay215.rst index 342c9980..4f187a9d 100644 --- a/doc/sphinx/source/Migrating/MigratingFromAnjay215.rst +++ b/doc/sphinx/source/Migrating/MigratingFromAnjay215.rst @@ -127,8 +127,7 @@ Changed flow of cancelling observations in case of errors ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ CoAP observations are implicitly cancelled if a notification bearing a 4.xx or -5.xx error code is delivered, or if an attempt to deliver a notification times -out. +5.xx error code is delivered. In Anjay 3.4.x and earlier, this cancellation (which involves calling the ``avs_coap_observe_cancel_handler_t`` callback) was performed *before* calling diff --git a/doc/sphinx/source/Migrating/MigratingFromAnjay225.rst b/doc/sphinx/source/Migrating/MigratingFromAnjay225.rst index c31a0b12..4899bf71 100644 --- a/doc/sphinx/source/Migrating/MigratingFromAnjay225.rst +++ b/doc/sphinx/source/Migrating/MigratingFromAnjay225.rst @@ -272,8 +272,7 @@ Changed flow of cancelling observations in case of errors ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ CoAP observations are implicitly cancelled if a notification bearing a 4.xx or -5.xx error code is delivered, or if an attempt to deliver a notification times -out. +5.xx error code is delivered. In Anjay 3.4.x and earlier, this cancellation (which involves calling the ``avs_coap_observe_cancel_handler_t`` callback) was performed *before* calling diff --git a/doc/sphinx/source/Migrating/MigratingFromAnjay24.rst b/doc/sphinx/source/Migrating/MigratingFromAnjay24.rst index c19ec016..6922a5ad 100644 --- a/doc/sphinx/source/Migrating/MigratingFromAnjay24.rst +++ b/doc/sphinx/source/Migrating/MigratingFromAnjay24.rst @@ -162,8 +162,7 @@ Changed flow of cancelling observations in case of errors ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ CoAP observations are implicitly cancelled if a notification bearing a 4.xx or -5.xx error code is delivered, or if an attempt to deliver a notification times -out. +5.xx error code is delivered. In Anjay 3.4.x and earlier, this cancellation (which involves calling the ``avs_coap_observe_cancel_handler_t`` callback) was performed *before* calling diff --git a/doc/sphinx/source/Migrating/MigratingFromAnjay26.rst b/doc/sphinx/source/Migrating/MigratingFromAnjay26.rst index fb59c3e3..dca5f55d 100644 --- a/doc/sphinx/source/Migrating/MigratingFromAnjay26.rst +++ b/doc/sphinx/source/Migrating/MigratingFromAnjay26.rst @@ -162,8 +162,7 @@ Changed flow of cancelling observations in case of errors ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ CoAP observations are implicitly cancelled if a notification bearing a 4.xx or -5.xx error code is delivered, or if an attempt to deliver a notification times -out. +5.xx error code is delivered. In Anjay 3.4.x and earlier, this cancellation (which involves calling the ``avs_coap_observe_cancel_handler_t`` callback) was performed *before* calling diff --git a/doc/sphinx/source/Migrating/MigratingFromAnjay27.rst b/doc/sphinx/source/Migrating/MigratingFromAnjay27.rst index 5be880e8..450ccd9f 100644 --- a/doc/sphinx/source/Migrating/MigratingFromAnjay27.rst +++ b/doc/sphinx/source/Migrating/MigratingFromAnjay27.rst @@ -155,8 +155,7 @@ Changed flow of cancelling observations in case of errors ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ CoAP observations are implicitly cancelled if a notification bearing a 4.xx or -5.xx error code is delivered, or if an attempt to deliver a notification times -out. +5.xx error code is delivered. In Anjay 3.4.x and earlier, this cancellation (which involves calling the ``avs_coap_observe_cancel_handler_t`` callback) was performed *before* calling diff --git a/doc/sphinx/source/Migrating/MigratingFromAnjay28.rst b/doc/sphinx/source/Migrating/MigratingFromAnjay28.rst index c5b3c700..3ec79fc0 100644 --- a/doc/sphinx/source/Migrating/MigratingFromAnjay28.rst +++ b/doc/sphinx/source/Migrating/MigratingFromAnjay28.rst @@ -150,8 +150,7 @@ Changed flow of cancelling observations in case of errors ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ CoAP observations are implicitly cancelled if a notification bearing a 4.xx or -5.xx error code is delivered, or if an attempt to deliver a notification times -out. +5.xx error code is delivered. In Anjay 3.4.x and earlier, this cancellation (which involves calling the ``avs_coap_observe_cancel_handler_t`` callback) was performed *before* calling diff --git a/doc/sphinx/source/Migrating/MigratingFromAnjay30.rst b/doc/sphinx/source/Migrating/MigratingFromAnjay30.rst index 49ac697e..a0ebbdda 100644 --- a/doc/sphinx/source/Migrating/MigratingFromAnjay30.rst +++ b/doc/sphinx/source/Migrating/MigratingFromAnjay30.rst @@ -62,8 +62,7 @@ Changed flow of cancelling observations in case of errors ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ CoAP observations are implicitly cancelled if a notification bearing a 4.xx or -5.xx error code is delivered, or if an attempt to deliver a notification times -out. +5.xx error code is delivered. In Anjay 3.4.x and earlier, this cancellation (which involves calling the ``avs_coap_observe_cancel_handler_t`` callback) was performed *before* calling diff --git a/doc/sphinx/source/Migrating/MigratingFromAnjay32.rst b/doc/sphinx/source/Migrating/MigratingFromAnjay32.rst index 70c90823..95254626 100644 --- a/doc/sphinx/source/Migrating/MigratingFromAnjay32.rst +++ b/doc/sphinx/source/Migrating/MigratingFromAnjay32.rst @@ -62,8 +62,7 @@ Changed flow of cancelling observations in case of errors --------------------------------------------------------- CoAP observations are implicitly cancelled if a notification bearing a 4.xx or -5.xx error code is delivered, or if an attempt to deliver a notification times -out. +5.xx error code is delivered. In Anjay 3.4.x and earlier, this cancellation (which involves calling the ``avs_coap_observe_cancel_handler_t`` callback) was performed *before* calling diff --git a/doc/sphinx/source/Migrating/MigratingFromAnjay33.rst b/doc/sphinx/source/Migrating/MigratingFromAnjay33.rst index b717daa0..fd267081 100644 --- a/doc/sphinx/source/Migrating/MigratingFromAnjay33.rst +++ b/doc/sphinx/source/Migrating/MigratingFromAnjay33.rst @@ -59,8 +59,7 @@ Changed flow of cancelling observations in case of errors --------------------------------------------------------- CoAP observations are implicitly cancelled if a notification bearing a 4.xx or -5.xx error code is delivered, or if an attempt to deliver a notification times -out. +5.xx error code is delivered. In Anjay 3.4.x and earlier, this cancellation (which involves calling the ``avs_coap_observe_cancel_handler_t`` callback) was performed *before* calling diff --git a/doc/sphinx/source/Migrating/MigratingFromAnjay34.rst b/doc/sphinx/source/Migrating/MigratingFromAnjay34.rst index 820ae11a..fbf21f42 100644 --- a/doc/sphinx/source/Migrating/MigratingFromAnjay34.rst +++ b/doc/sphinx/source/Migrating/MigratingFromAnjay34.rst @@ -25,8 +25,9 @@ Changed flow of cancelling observations in case of errors --------------------------------------------------------- CoAP observations are implicitly cancelled if a notification bearing a 4.xx or -5.xx error code is delivered, or if an attempt to deliver a notification times -out. +5.xx error code is delivered. If an attempt to deliver a confirmable +notification times out, CoAP observation is not cancelled by default anymore. +It can be adjusted by ``WITH_AVS_COAP_OBSERVE_CANCEL_ON_TIMEOUT``. In Anjay 3.4.x and earlier, this cancellation (which involves calling the ``avs_coap_observe_cancel_handler_t`` callback) was performed *before* calling diff --git a/doc/sphinx/source/Migrating/MigratingFromAnjay37.rst b/doc/sphinx/source/Migrating/MigratingFromAnjay37.rst new file mode 100644 index 00000000..f20aff7d --- /dev/null +++ b/doc/sphinx/source/Migrating/MigratingFromAnjay37.rst @@ -0,0 +1,28 @@ +.. + Copyright 2017-2024 AVSystem + AVSystem Anjay LwM2M SDK + All rights reserved. + + Licensed under the AVSystem-5-clause License. + See the attached LICENSE file for details. + +Migrating from Anjay 3.7 +======================== + +.. contents:: :local: + +.. highlight:: c + +Introduction +------------ + +Since Anjay 3.8.0, confirmable notifications are not cancelled anymore in case +of a timeout. + +Changed flow of cancelling observations in case of timeout +---------------------------------------------------------- + +If an attempt to deliver a confirmable notification times out, CoAP observation +is not cancelled by default anymore. It can be adjusted by +``WITH_AVS_COAP_OBSERVE_CANCEL_ON_TIMEOUT``. +The LwM2M Observe/Notify implementation in Anjay has been updated accordingly. diff --git a/example_configs/embedded_lwm2m10/anjay/anjay_config.h b/example_configs/embedded_lwm2m10/anjay/anjay_config.h index ab60d916..e169621c 100644 --- a/example_configs/embedded_lwm2m10/anjay/anjay_config.h +++ b/example_configs/embedded_lwm2m10/anjay/anjay_config.h @@ -656,6 +656,13 @@ * composite type request. */ /* #undef ANJAY_WITHOUT_COMPOSITE_OPERATIONS */ + +/** + * Enable support for the experimental + * anjay_get_server_connection_status() API and related + * anjay_server_connection_status_cb_t callback. + */ +/* #undef ANJAY_WITH_CONN_STATUS_API */ /**@}*/ #endif // ANJAY_CONFIG_H diff --git a/example_configs/embedded_lwm2m10/avsystem/coap/avs_coap_config.h b/example_configs/embedded_lwm2m10/avsystem/coap/avs_coap_config.h index a7e62a80..94dcddf0 100644 --- a/example_configs/embedded_lwm2m10/avsystem/coap/avs_coap_config.h +++ b/example_configs/embedded_lwm2m10/avsystem/coap/avs_coap_config.h @@ -68,11 +68,23 @@ */ #define WITH_AVS_COAP_OBSERVE +/** + * Turn on cancelling observation on a timeout. + * + * Only meaningful if WITH_AVS_COAP_OBSERVE is enabled. + * + * NOTE: LwM2M specification requires LwM2M server to send Cancel Observation + * request. Meanwhile CoAP RFC 7641 states that timeout on notification should + * cancel it. This setting is to enable both of these behaviors with default + * focused on keeping observations in case of bad connectivity. + */ +/* #undef WITH_AVS_COAP_OBSERVE_CANCEL_ON_TIMEOUT */ + /** * Enable support for observation persistence (avs_coap_observe_persist() * and avs_coap_observe_restore() calls). * - * Only meaningful WITH_AVS_COAP_OBSERVE is enabled. + * Only meaningful if WITH_AVS_COAP_OBSERVE is enabled. */ /* #undef WITH_AVS_COAP_OBSERVE_PERSISTENCE */ diff --git a/example_configs/embedded_lwm2m11/anjay/anjay_config.h b/example_configs/embedded_lwm2m11/anjay/anjay_config.h index b9f915d1..d6c598d6 100644 --- a/example_configs/embedded_lwm2m11/anjay/anjay_config.h +++ b/example_configs/embedded_lwm2m11/anjay/anjay_config.h @@ -656,6 +656,13 @@ * composite type request. */ /* #undef ANJAY_WITHOUT_COMPOSITE_OPERATIONS */ + +/** + * Enable support for the experimental + * anjay_get_server_connection_status() API and related + * anjay_server_connection_status_cb_t callback. + */ +/* #undef ANJAY_WITH_CONN_STATUS_API */ /**@}*/ #endif // ANJAY_CONFIG_H diff --git a/example_configs/embedded_lwm2m11/avsystem/coap/avs_coap_config.h b/example_configs/embedded_lwm2m11/avsystem/coap/avs_coap_config.h index 8982b7c3..c79812f8 100644 --- a/example_configs/embedded_lwm2m11/avsystem/coap/avs_coap_config.h +++ b/example_configs/embedded_lwm2m11/avsystem/coap/avs_coap_config.h @@ -68,11 +68,23 @@ */ #define WITH_AVS_COAP_OBSERVE +/** + * Turn on cancelling observation on a timeout. + * + * Only meaningful if WITH_AVS_COAP_OBSERVE is enabled. + * + * NOTE: LwM2M specification requires LwM2M server to send Cancel Observation + * request. Meanwhile CoAP RFC 7641 states that timeout on notification should + * cancel it. This setting is to enable both of these behaviors with default + * focused on keeping observations in case of bad connectivity. + */ +/* #undef WITH_AVS_COAP_OBSERVE_CANCEL_ON_TIMEOUT */ + /** * Enable support for observation persistence (avs_coap_observe_persist() * and avs_coap_observe_restore() calls). * - * Only meaningful WITH_AVS_COAP_OBSERVE is enabled. + * Only meaningful if WITH_AVS_COAP_OBSERVE is enabled. */ #define WITH_AVS_COAP_OBSERVE_PERSISTENCE diff --git a/example_configs/linux_lwm2m10/anjay/anjay_config.h b/example_configs/linux_lwm2m10/anjay/anjay_config.h index 93a59c83..8fb98c0b 100644 --- a/example_configs/linux_lwm2m10/anjay/anjay_config.h +++ b/example_configs/linux_lwm2m10/anjay/anjay_config.h @@ -656,6 +656,13 @@ * composite type request. */ /* #undef ANJAY_WITHOUT_COMPOSITE_OPERATIONS */ + +/** + * Enable support for the experimental + * anjay_get_server_connection_status() API and related + * anjay_server_connection_status_cb_t callback. + */ +#define ANJAY_WITH_CONN_STATUS_API /**@}*/ #endif // ANJAY_CONFIG_H diff --git a/example_configs/linux_lwm2m10/avsystem/coap/avs_coap_config.h b/example_configs/linux_lwm2m10/avsystem/coap/avs_coap_config.h index cf107322..745f2de9 100644 --- a/example_configs/linux_lwm2m10/avsystem/coap/avs_coap_config.h +++ b/example_configs/linux_lwm2m10/avsystem/coap/avs_coap_config.h @@ -68,11 +68,23 @@ */ #define WITH_AVS_COAP_OBSERVE +/** + * Turn on cancelling observation on a timeout. + * + * Only meaningful if WITH_AVS_COAP_OBSERVE is enabled. + * + * NOTE: LwM2M specification requires LwM2M server to send Cancel Observation + * request. Meanwhile CoAP RFC 7641 states that timeout on notification should + * cancel it. This setting is to enable both of these behaviors with default + * focused on keeping observations in case of bad connectivity. + */ +/* #undef WITH_AVS_COAP_OBSERVE_CANCEL_ON_TIMEOUT */ + /** * Enable support for observation persistence (avs_coap_observe_persist() * and avs_coap_observe_restore() calls). * - * Only meaningful WITH_AVS_COAP_OBSERVE is enabled. + * Only meaningful if WITH_AVS_COAP_OBSERVE is enabled. */ /* #undef WITH_AVS_COAP_OBSERVE_PERSISTENCE */ diff --git a/example_configs/linux_lwm2m11/anjay/anjay_config.h b/example_configs/linux_lwm2m11/anjay/anjay_config.h index e120d5e0..ee4e9afd 100644 --- a/example_configs/linux_lwm2m11/anjay/anjay_config.h +++ b/example_configs/linux_lwm2m11/anjay/anjay_config.h @@ -656,6 +656,13 @@ * composite type request. */ /* #undef ANJAY_WITHOUT_COMPOSITE_OPERATIONS */ + +/** + * Enable support for the experimental + * anjay_get_server_connection_status() API and related + * anjay_server_connection_status_cb_t callback. + */ +#define ANJAY_WITH_CONN_STATUS_API /**@}*/ #endif // ANJAY_CONFIG_H diff --git a/example_configs/linux_lwm2m11/avsystem/coap/avs_coap_config.h b/example_configs/linux_lwm2m11/avsystem/coap/avs_coap_config.h index bcff8a8d..83107b82 100644 --- a/example_configs/linux_lwm2m11/avsystem/coap/avs_coap_config.h +++ b/example_configs/linux_lwm2m11/avsystem/coap/avs_coap_config.h @@ -68,11 +68,23 @@ */ #define WITH_AVS_COAP_OBSERVE +/** + * Turn on cancelling observation on a timeout. + * + * Only meaningful if WITH_AVS_COAP_OBSERVE is enabled. + * + * NOTE: LwM2M specification requires LwM2M server to send Cancel Observation + * request. Meanwhile CoAP RFC 7641 states that timeout on notification should + * cancel it. This setting is to enable both of these behaviors with default + * focused on keeping observations in case of bad connectivity. + */ +/* #undef WITH_AVS_COAP_OBSERVE_CANCEL_ON_TIMEOUT */ + /** * Enable support for observation persistence (avs_coap_observe_persist() * and avs_coap_observe_restore() calls). * - * Only meaningful WITH_AVS_COAP_OBSERVE is enabled. + * Only meaningful if WITH_AVS_COAP_OBSERVE is enabled. */ #define WITH_AVS_COAP_OBSERVE_PERSISTENCE diff --git a/include_public/anjay/anjay_config.h.in b/include_public/anjay/anjay_config.h.in index 517ed789..753bbaf0 100644 --- a/include_public/anjay/anjay_config.h.in +++ b/include_public/anjay/anjay_config.h.in @@ -656,6 +656,13 @@ * composite type request. */ #cmakedefine ANJAY_WITHOUT_COMPOSITE_OPERATIONS + +/** + * Enable support for the experimental + * anjay_get_server_connection_status() API and related + * anjay_server_connection_status_cb_t callback. + */ +#cmakedefine ANJAY_WITH_CONN_STATUS_API /**@}*/ #endif // ANJAY_CONFIG_H diff --git a/include_public/anjay/core.h b/include_public/anjay/core.h index 19aa3048..8f440c7e 100644 --- a/include_public/anjay/core.h +++ b/include_public/anjay/core.h @@ -100,6 +100,144 @@ typedef struct { } anjay_lwm2m_version_config_t; #endif // ANJAY_WITH_LWM2M11 +#ifdef ANJAY_WITH_CONN_STATUS_API +/** + * @experimental This is experimental server connection status API. This API can + * change in future versions without any notice. + * + * This enum represents the possible states of a server connection. + */ +typedef enum anjay_server_conn_status { + /** + * Invalid state. It is only returned if a given server connection object + * does not exist, or there was an error in determining its state. + */ + ANJAY_SERV_CONN_STATUS_INVALID, + /** + * Generic state for any errors for which more specific states are not + * defined. For a regular LwM2M Server, this means that the primary + * connection is invalid or de-register/update operation has failed. For a + * LwM2M Bootstrap Server, it means that the bootstrap process failed. + */ + ANJAY_SERV_CONN_STATUS_ERROR, + /** + * This status means that the server connection object has been just created + * as a result of the Security and Server object instances having been + * previously added to the data model. A connection attempt is scheduled to + * be made during subsequent calls to @ref anjay_sched_run. + */ + ANJAY_SERV_CONN_STATUS_INITIAL, + /** + * This status is reported just before attempting the connection procedure, + * which may include binding the socket to a local port, performing DNS + * resolution, connecting the socket, and performing a (D)TLS handshake. + */ + ANJAY_SERV_CONN_STATUS_CONNECTING, + /** + * This status means that the bootstrap process is in progress. It only + * applies to LwM2M Bootstrap Servers. + */ + ANJAY_SERV_CONN_STATUS_BOOTSTRAPPING, + /** + * This status means that the bootstrap process has completed successfully. + * It only applies to LwM2M Bootstrap Servers. + */ + ANJAY_SERV_CONN_STATUS_BOOTSTRAPPED, + /** + * This status means that the register operation is in progress. It only + * applies to regular LwM2M Servers. + */ + ANJAY_SERV_CONN_STATUS_REGISTERING, + /** + * This status means that the Anjay is registered to the LwM2M Server. It + * only applies to regular LwM2M Servers. + */ + ANJAY_SERV_CONN_STATUS_REGISTERED, + /** + * This status means that the last register operation has failed due to no + * response, a non-success CoAP response was received, or network problems. + * It only applies to regular LwM2M Servers. + */ + ANJAY_SERV_CONN_STATUS_REG_FAILURE, + /** + * This status means that the de-register operation is in progress. It + * only applies to regular LwM2M Servers. + */ + ANJAY_SERV_CONN_STATUS_DEREGISTERING, + /** + * This status means that the the library has de-registered with the LwM2M + * Server, either explicitly by successfully performing the De-register + * operation, or implicitly by discarding the registration state (which + * happens when the server is disabled while offline). It only applies to + * regular LwM2M Servers. + */ + ANJAY_SERV_CONN_STATUS_DEREGISTERED, + /** + * This status means that the /1/x/4 resource has been executed and Anjay + * will try to suspend the LwM2M Server. It only applies to regular LwM2M + * Servers. + */ + ANJAY_SERV_CONN_STATUS_SUSPENDING, + /** + * This status means that the suspend process has completed successfully. It + * only applies to regular LwM2M Servers. + */ + ANJAY_SERV_CONN_STATUS_SUSPENDED, + /** + * This status means that the last update operation has failed due to either + * no response or a non-success CoAP response was received. It only + * applies to regular LwM2M Servers. + */ + ANJAY_SERV_CONN_STATUS_REREGISTERING, + /** + * This status means that the update operation is in progress. It only + * applies to regular LwM2M Servers. + */ + ANJAY_SERV_CONN_STATUS_UPDATING +} anjay_server_conn_status_t; + +/** + * @experimental This is experimental server connection status API. This API can + * change in future versions without any notice. + * + * Callback called each time there is a transition of a server connection status + * (as listed in @ref anjay_server_conn_status_t ). + * + * @param arg Opaque argument as set through the @c + * server_connection_status_cb_arg field in @ref + * anjay_configuration_t + * + * @param anjay Anjay object that calls this callback + * + * @param ssid Short Server ID of the server for which the status is reported + * + * @param status New status of the server connection + */ +typedef void +anjay_server_connection_status_cb_t(void *arg, + anjay_t *anjay, + anjay_ssid_t ssid, + anjay_server_conn_status_t status); + +/** + * @experimental This is experimental server connection status API. This API can + * change in future versions without any notice. + * + * This function returns the server connection status. Possible statuses are + * given in the @ref anjay_server_conn_status_t. Statuses for a LwM2M Bootstrap + * Server are different from the statuses for a regular LwM2M Server. + * + * @param anjay Anjay object to operate on. + * @param ssid Short Server ID of the server which status will be returned. @ref + * ANJAY_SSID_ANY is not a valid argument. + * + * @returns Server status on success, in case of error + * ANJAY_SERV_CONN_STATUS_UNKNOWN. + */ +anjay_server_conn_status_t +anjay_get_server_connection_status(anjay_t *anjay, anjay_ssid_t ssid); +#endif // ANJAY_WITH_CONN_STATUS_API + typedef struct anjay_configuration { /** * Endpoint name as presented to the LwM2M server. Must be non-NULL, or @@ -285,6 +423,18 @@ typedef struct anjay_configuration { */ bool enable_self_notify; + /** + * Treat failures of the "connect" socket operation (e.g. (D)TLS handshake + * failures) as a failed LwM2M Register operation. This enables automatic + * retrying of them as described in the "Bootstrap and LwM2M Server + * Registration Mechanisms" of LwM2M Core TS 1.1. + * + * When disabled, such failures are treated as fatal errors and cause the + * entire registration sequence for that server to be aborted (which will + * trigger a fallback to Bootstrap if applicable). + */ + bool connection_error_is_registration_failure; + /** * (D)TLS ciphersuites to use if the "DTLS/TLS Ciphersuite" Resource * (/0/x/16) is not available or empty. @@ -424,6 +574,27 @@ typedef struct anjay_configuration { bool rebuild_client_cert_chain; #endif // ANJAY_WITH_LWM2M11 +#ifdef ANJAY_WITH_CONN_STATUS_API + /** + * @experimental This is experimental server connection status API. This API + * can change in future versions without any notice. + * + * Function called each time there is a transition of a server connection + * status (as listed in @ref anjay_server_conn_status_t ). + */ + anjay_server_connection_status_cb_t *server_connection_status_cb; + + /** + * @experimental This is experimental server connection status API. This API + * can change in future versions without any notice. + * + * Opaque argument that will be passed to the function configured in the + * server_connection_status_cb field. + * + * If server_connection_status_cb is NULL, this field is ignored. + */ + void *server_connection_status_cb_arg; +#endif // ANJAY_WITH_CONN_STATUS_API } anjay_configuration_t; /** diff --git a/src/anjay_config_log.h b/src/anjay_config_log.h index 9294e51a..6f1f609f 100644 --- a/src/anjay_config_log.h +++ b/src/anjay_config_log.h @@ -104,6 +104,11 @@ static inline void _anjay_log_feature_list(void) { #else // ANJAY_WITH_COMMUNICATION_TIMESTAMP_API _anjay_log(anjay, TRACE, "ANJAY_WITH_COMMUNICATION_TIMESTAMP_API = OFF"); #endif // ANJAY_WITH_COMMUNICATION_TIMESTAMP_API +#ifdef ANJAY_WITH_CONN_STATUS_API + _anjay_log(anjay, TRACE, "ANJAY_WITH_CONN_STATUS_API = ON"); +#else // ANJAY_WITH_CONN_STATUS_API + _anjay_log(anjay, TRACE, "ANJAY_WITH_CONN_STATUS_API = OFF"); +#endif // ANJAY_WITH_CONN_STATUS_API #ifdef ANJAY_WITH_CON_ATTR _anjay_log(anjay, TRACE, "ANJAY_WITH_CON_ATTR = ON"); #else // ANJAY_WITH_CON_ATTR @@ -700,6 +705,11 @@ static inline void _anjay_log_feature_list(void) { #else // WITH_AVS_COAP_OBSERVE _anjay_log(anjay, TRACE, "WITH_AVS_COAP_OBSERVE = OFF"); #endif // WITH_AVS_COAP_OBSERVE +#ifdef WITH_AVS_COAP_OBSERVE_CANCEL_ON_TIMEOUT + _anjay_log(anjay, TRACE, "WITH_AVS_COAP_OBSERVE_CANCEL_ON_TIMEOUT = ON"); +#else // WITH_AVS_COAP_OBSERVE_CANCEL_ON_TIMEOUT + _anjay_log(anjay, TRACE, "WITH_AVS_COAP_OBSERVE_CANCEL_ON_TIMEOUT = OFF"); +#endif // WITH_AVS_COAP_OBSERVE_CANCEL_ON_TIMEOUT #ifdef WITH_AVS_COAP_OBSERVE_PERSISTENCE _anjay_log(anjay, TRACE, "WITH_AVS_COAP_OBSERVE_PERSISTENCE = ON"); #else // WITH_AVS_COAP_OBSERVE_PERSISTENCE diff --git a/src/anjay_modules/anjay_servers.h b/src/anjay_modules/anjay_servers.h index 78a714ab..a247e0d3 100644 --- a/src/anjay_modules/anjay_servers.h +++ b/src/anjay_modules/anjay_servers.h @@ -113,6 +113,15 @@ int _anjay_schedule_disable_server_with_explicit_timeout_unlocked( */ int _anjay_enable_server_unlocked(anjay_unlocked_t *anjay, anjay_ssid_t ssid); +#ifdef ANJAY_WITH_CONN_STATUS_API +/** + * Set suspending flag for the server specified by the ssid argument. + */ +void _anjay_set_server_suspending_flag(anjay_unlocked_t *anjay, + anjay_ssid_t ssid, + bool val); +#endif // ANJAY_WITH_CONN_STATUS_API + VISIBILITY_PRIVATE_HEADER_END #endif /* ANJAY_INCLUDE_ANJAY_MODULES_SERVERS_H */ diff --git a/src/core/anjay_bootstrap_core.c b/src/core/anjay_bootstrap_core.c index 0bd10d5f..2982ed2c 100644 --- a/src/core/anjay_bootstrap_core.c +++ b/src/core/anjay_bootstrap_core.c @@ -97,6 +97,13 @@ static avs_error_t start_bootstrap_if_not_already_started( avs_sched_del(&anjay->bootstrap.purge_bootstrap_handle); } anjay->bootstrap.in_progress = true; +# ifdef ANJAY_WITH_CONN_STATUS_API + if (bootstrap_connection.server) { + _anjay_set_server_connection_status( + bootstrap_connection.server, + ANJAY_SERV_CONN_STATUS_BOOTSTRAPPING); + } +# endif // ANJAY_WITH_CONN_STATUS_API return AVS_OK; } @@ -705,6 +712,12 @@ static int bootstrap_finish_impl(anjay_unlocked_t *anjay, anjay_log( WARNING, _("Bootstrap configuration could not be committed, rejecting")); +# ifdef ANJAY_WITH_CONN_STATUS_API + if (bootstrap_connection.server) { + _anjay_set_server_connection_status(bootstrap_connection.server, + ANJAY_SERV_CONN_STATUS_ERROR); + } +# endif // ANJAY_WITH_CONN_STATUS_API return retval; } if ((retval = _anjay_notify_perform_without_servers( @@ -736,7 +749,20 @@ static int bootstrap_finish_impl(anjay_unlocked_t *anjay, _anjay_server_on_server_communication_error( bootstrap_connection.server, err); } +# ifdef ANJAY_WITH_CONN_STATUS_API + if (avs_is_err(err) && bootstrap_connection.server) { + _anjay_set_server_connection_status(bootstrap_connection.server, + ANJAY_SERV_CONN_STATUS_ERROR); + } +# endif // ANJAY_WITH_CONN_STATUS_API } else { +# ifdef ANJAY_WITH_CONN_STATUS_API + if (bootstrap_connection.server) { + _anjay_set_server_connection_status( + bootstrap_connection.server, + ANJAY_SERV_CONN_STATUS_BOOTSTRAPPED); + } +# endif // ANJAY_WITH_CONN_STATUS_API _anjay_schedule_reload_servers(anjay); } return retval; @@ -783,6 +809,13 @@ int _anjay_bootstrap_notify_regular_connection_available( BOOTSTRAP_FINISH_DISABLE_SERVER))); } else { cancel_client_initiated_bootstrap(anjay); +# ifdef ANJAY_WITH_CONN_STATUS_API + if (bootstrap_connection.server) { + _anjay_set_server_connection_status( + bootstrap_connection.server, + ANJAY_SERV_CONN_STATUS_BOOTSTRAPPED); + } +# endif // ANJAY_WITH_CONN_STATUS_API } if (!result) { reset_client_initiated_bootstrap_backoff(&anjay->bootstrap); diff --git a/src/core/anjay_core.c b/src/core/anjay_core.c index 9a69fd25..0be02ee6 100644 --- a/src/core/anjay_core.c +++ b/src/core/anjay_core.c @@ -47,7 +47,7 @@ VISIBILITY_SOURCE_BEGIN #ifndef ANJAY_VERSION -# define ANJAY_VERSION "3.7.0" +# define ANJAY_VERSION "3.8.0" #endif // ANJAY_VERSION #ifdef ANJAY_WITH_LWM2M11 @@ -127,6 +127,15 @@ static int init_anjay(anjay_unlocked_t *anjay, return -1; } +#ifdef ANJAY_WITH_CONN_STATUS_API + anjay->server_connection_status_cb = config->server_connection_status_cb; + + if (anjay->server_connection_status_cb) { + anjay->server_connection_status_cb_arg = + config->server_connection_status_cb_arg; + } +#endif // ANJAY_WITH_CONN_STATUS_API + anjay->socket_config = config->socket_config; anjay->udp_listen_port = config->udp_listen_port; @@ -232,6 +241,8 @@ static int init_anjay(anjay_unlocked_t *anjay, anjay->prefer_hierarchical_formats = config->prefer_hierarchical_formats; anjay->update_immediately_on_dm_change = config->update_immediately_on_dm_change; + anjay->connection_error_is_registration_failure = + config->connection_error_is_registration_failure; anjay->enable_self_notify = config->enable_self_notify; anjay->use_connection_id = config->use_connection_id; anjay->additional_tls_config_clb = config->additional_tls_config_clb; diff --git a/src/core/anjay_core.h b/src/core/anjay_core.h index 0c2a3ef1..6129406d 100644 --- a/src/core/anjay_core.h +++ b/src/core/anjay_core.h @@ -130,6 +130,11 @@ struct char *endpoint_name; anjay_transaction_state_t transaction_state; +#ifdef ANJAY_WITH_CONN_STATUS_API + anjay_server_connection_status_cb_t *server_connection_status_cb; + void *server_connection_status_cb_arg; +#endif // ANJAY_WITH_CONN_STATUS_API + #ifdef ANJAY_WITH_SEND anjay_sender_t sender; #endif // ANJAY_WITH_SEND @@ -143,6 +148,7 @@ struct bool prefer_hierarchical_formats; bool update_immediately_on_dm_change; bool enable_self_notify; + bool connection_error_is_registration_failure; #ifdef ANJAY_WITH_NET_STATS closed_connections_stats_t closed_connections_stats; #endif // ANJAY_WITH_NET_STATS diff --git a/src/core/anjay_dm_core.c b/src/core/anjay_dm_core.c index 96de0e3c..8c93b712 100644 --- a/src/core/anjay_dm_core.c +++ b/src/core/anjay_dm_core.c @@ -567,6 +567,14 @@ static int dm_execute(anjay_unlocked_t *anjay, request->uri.ids[ANJAY_ID_IID], request->uri.ids[ANJAY_ID_RID], execute_ctx); +#ifdef ANJAY_WITH_CONN_STATUS_API + /* RID == 4 -> Disable resource */ + if (!retval && request->uri.ids[ANJAY_ID_OID] == ANJAY_DM_OID_SERVER + && request->uri.ids[ANJAY_ID_RID] + == ANJAY_DM_RID_SERVER_DISABLE) { + _anjay_set_server_suspending_flag(anjay, ssid, true); + } +#endif // ANJAY_WITH_CONN_STATUS_API _anjay_execute_ctx_destroy(&execute_ctx); } return retval; diff --git a/src/core/anjay_servers_private.h b/src/core/anjay_servers_private.h index 29987bb6..3beebe77 100644 --- a/src/core/anjay_servers_private.h +++ b/src/core/anjay_servers_private.h @@ -536,6 +536,24 @@ _anjay_connection_transport(anjay_connection_ref_t conn_ref); AVS_LIST(const anjay_socket_entry_t) _anjay_collect_socket_entries(anjay_unlocked_t *anjay, bool include_offline); +#ifdef ANJAY_WITH_CONN_STATUS_API +/** + * Set connection status for the server specified by the server argument. This + * function supports LwM2M bootstrap and regular LwM2M servers. + */ +void _anjay_set_server_connection_status(anjay_server_info_t *server, + anjay_server_conn_status_t new_status); + +/** + * Check the server parameters and set one of the following server states: + * ANJAY_SERV_CONN_STATUS_UNKNOWN, ANJAY_SERV_CONN_STATUS_REGISTERING, + * ANJAY_SERV_CONN_STATUS_REREGISTERING, ANJAY_SERV_CONN_STATUS_REGISTERED or + * ANJAY_SERV_CONN_STATUS_UPDATING. This function supports only regular LwM2M + * servers. + */ +void _anjay_check_server_connection_status(anjay_server_info_t *server); +#endif // ANJAY_WITH_CONN_STATUS_API + VISIBILITY_PRIVATE_HEADER_END #endif // ANJAY_SERVERS_PRIVATE_H diff --git a/src/core/io/anjay_base64_out.c b/src/core/io/anjay_base64_out.c index 660a5007..da39b082 100644 --- a/src/core/io/anjay_base64_out.c +++ b/src/core/io/anjay_base64_out.c @@ -38,12 +38,8 @@ static int base64_ret_encode_and_write(base64_ret_bytes_ctx_t *ctx, return 0; } char encoded[4 * (TEXT_CHUNK_SIZE / 3) + 1]; - size_t encoded_size; - if (ctx->config.padding_char) { - encoded_size = avs_base64_encoded_size(buffer_size); - } else { - encoded_size = avs_base64_encoded_size_without_padding(buffer_size); - } + size_t encoded_size = + avs_base64_encoded_size_custom(buffer_size, ctx->config); assert(encoded_size <= sizeof(encoded)); int retval = avs_base64_encode_custom(encoded, encoded_size, buffer, buffer_size, ctx->config); diff --git a/src/core/io/anjay_json_encoder.c b/src/core/io/anjay_json_encoder.c index c234dbe0..4c9095e0 100644 --- a/src/core/io/anjay_json_encoder.c +++ b/src/core/io/anjay_json_encoder.c @@ -504,7 +504,8 @@ _anjay_senml_json_encoder_new(avs_stream_t *stream) { .alphabet = AVS_BASE64_URL_SAFE_CHARS, .padding_char = '\0', .allow_whitespace = false, - .require_padding = false + .require_padding = false, + .without_null_termination = false, }; json_encoder_t *ctx = json_encoder_new(stream, &SENML_JSON_ENCODER_VTABLE, senml_encode_key, BASE64_CONFIG); diff --git a/src/core/observe/anjay_observe_core.c b/src/core/observe/anjay_observe_core.c index e068106d..5f97c7fa 100644 --- a/src/core/observe/anjay_observe_core.c +++ b/src/core/observe/anjay_observe_core.c @@ -860,6 +860,31 @@ delete_observation(AVS_LIST(anjay_observe_connection_entry_t) *conn_ptr, } } +static void +recalculate_conn_trigger_times(anjay_observe_connection_entry_t *conn) { + avs_time_monotonic_t monotonic_now = avs_time_monotonic_now(); + avs_time_real_t real_now = avs_time_real_now(); + conn->next_trigger = AVS_TIME_REAL_INVALID; + conn->next_pmax_trigger = AVS_TIME_REAL_INVALID; + AVS_SORTED_SET_ELEM(anjay_observation_t) observation; + AVS_SORTED_SET_FOREACH(observation, conn->observations) { + if (avs_time_real_valid(observation->next_pmax_trigger) + && !avs_time_real_before(conn->next_pmax_trigger, + observation->next_pmax_trigger)) { + conn->next_pmax_trigger = observation->next_pmax_trigger; + } + avs_time_real_t next_trigger = avs_time_real_add( + real_now, + avs_time_monotonic_diff(avs_sched_time( + &observation->notify_task), + monotonic_now)); + if (avs_time_real_valid(next_trigger) + && !avs_time_real_before(conn->next_trigger, next_trigger)) { + conn->next_trigger = next_trigger; + } + } +} + static void observe_remove_entry(anjay_connection_ref_t connection, const avs_coap_token_t *token) { AVS_LIST(anjay_observe_connection_entry_t) *conn_ptr = @@ -874,6 +899,9 @@ static void observe_remove_entry(anjay_connection_ref_t connection, if (observation) { delete_observation(conn_ptr, &observation); } + if (*conn_ptr) { + recalculate_conn_trigger_times(*conn_ptr); + } } void _anjay_observe_cancel_handler(avs_coap_observe_id_t id, void *ref_ptr) { @@ -1458,31 +1486,6 @@ static void remove_all_unsent_values(anjay_observe_connection_entry_t *conn) { } } -static void -recalculate_conn_trigger_times(anjay_observe_connection_entry_t *conn) { - avs_time_monotonic_t monotonic_now = avs_time_monotonic_now(); - avs_time_real_t real_now = avs_time_real_now(); - conn->next_trigger = AVS_TIME_REAL_INVALID; - conn->next_pmax_trigger = AVS_TIME_REAL_INVALID; - AVS_SORTED_SET_ELEM(anjay_observation_t) observation; - AVS_SORTED_SET_FOREACH(observation, conn->observations) { - if (avs_time_real_valid(observation->next_pmax_trigger) - && !avs_time_real_before(conn->next_pmax_trigger, - observation->next_pmax_trigger)) { - conn->next_pmax_trigger = observation->next_pmax_trigger; - } - avs_time_real_t next_trigger = avs_time_real_add( - real_now, - avs_time_monotonic_diff(avs_sched_time( - &observation->notify_task), - monotonic_now)); - if (avs_time_real_valid(next_trigger) - && !avs_time_real_before(conn->next_trigger, next_trigger)) { - conn->next_trigger = next_trigger; - } - } -} - static bool connection_exists(anjay_unlocked_t *anjay, anjay_observe_connection_entry_t *conn) { AVS_LIST(anjay_observe_connection_entry_t) *conn_ptr = diff --git a/src/core/servers/anjay_activate.c b/src/core/servers/anjay_activate.c index f6803141..b9dd35a0 100644 --- a/src/core/servers/anjay_activate.c +++ b/src/core/servers/anjay_activate.c @@ -37,6 +37,12 @@ VISIBILITY_SOURCE_BEGIN */ static int deactivate_server(anjay_server_info_t *server) { assert(server); +#ifdef ANJAY_WITH_CONN_STATUS_API + if (server->suspending) { + _anjay_set_server_connection_status(server, + ANJAY_SERV_CONN_STATUS_SUSPENDING); + } +#endif // ANJAY_WITH_CONN_STATUS_API #ifndef ANJAY_WITHOUT_DEREGISTER if (server->ssid != ANJAY_SSID_BOOTSTRAP && !_anjay_bootstrap_in_progress(server->anjay)) { @@ -47,6 +53,19 @@ static int deactivate_server(anjay_server_info_t *server) { // optional anyway. _anjay_serve_deregister logs the error cause. _anjay_server_deregister(server); } +# ifdef ANJAY_WITH_CONN_STATUS_API + else if (server->connection_status != ANJAY_SERV_CONN_STATUS_ERROR + && server->connection_status != ANJAY_SERV_CONN_STATUS_INITIAL + && server->connection_status + != ANJAY_SERV_CONN_STATUS_REG_FAILURE + && server->connection_status + != ANJAY_SERV_CONN_STATUS_SUSPENDED + && !avs_time_real_before(server->reactivate_time, + avs_time_real_now())) { + _anjay_set_server_connection_status( + server, ANJAY_SERV_CONN_STATUS_DEREGISTERED); + } +# endif // ANJAY_WITH_CONN_STATUS_API } #endif // ANJAY_WITHOUT_DEREGISTER _anjay_server_clean_active_data(server); @@ -60,6 +79,10 @@ static int deactivate_server(anjay_server_info_t *server) { && _anjay_server_sched_activate(server)) { // not much we can do other than removing the server altogether anjay_log(ERROR, _("could not reschedule server reactivation")); +#ifdef ANJAY_WITH_CONN_STATUS_API + _anjay_check_server_connection_status(server); + _anjay_set_server_suspending_flag(server->anjay, server->ssid, false); +#endif // ANJAY_WITH_CONN_STATUS_API AVS_LIST(anjay_server_info_t) *server_ptr = _anjay_servers_find_ptr(&server->anjay->servers, server->ssid); assert(server_ptr); @@ -67,6 +90,13 @@ static int deactivate_server(anjay_server_info_t *server) { AVS_LIST_DELETE(server_ptr); return -1; } +#ifdef ANJAY_WITH_CONN_STATUS_API + if (server->suspending) { + _anjay_set_server_connection_status(server, + ANJAY_SERV_CONN_STATUS_SUSPENDED); + _anjay_set_server_suspending_flag(server->anjay, server->ssid, false); + } +#endif // ANJAY_WITH_CONN_STATUS_API return 0; } @@ -138,6 +168,10 @@ void _anjay_server_on_failure(anjay_server_info_t *server, debug_msg); // Abort any further bootstrap retries. _anjay_bootstrap_cleanup(server->anjay); +#ifdef ANJAY_WITH_CONN_STATUS_API + _anjay_set_server_connection_status(server, + ANJAY_SERV_CONN_STATUS_ERROR); +#endif // ANJAY_WITH_CONN_STATUS_API } else { #ifdef ANJAY_WITH_LWM2M11 if (server->registration_attempts > 0) { @@ -196,6 +230,12 @@ void _anjay_server_on_failure(anjay_server_info_t *server, anjay_log(DEBUG, _("Non-Bootstrap Server ") "%" PRIu16 _(": ") "%s" _("."), server->ssid, debug_msg); +#ifdef ANJAY_WITH_CONN_STATUS_API + if (server->connection_status != ANJAY_SERV_CONN_STATUS_REG_FAILURE) { + _anjay_set_server_connection_status(server, + ANJAY_SERV_CONN_STATUS_ERROR); + } +#endif // ANJAY_WITH_CONN_STATUS_API (void) _anjay_perform_bootstrap_action_if_appropriate( server->anjay, _anjay_servers_find_active(server->anjay, ANJAY_SSID_BOOTSTRAP), @@ -239,6 +279,12 @@ void _anjay_server_on_server_communication_timeout( server->anjay, server->ssid, AVS_TIME_DURATION_ZERO)) { server->refresh_failed = true; } else { +#ifdef ANJAY_WITH_CONN_STATUS_API + if (server->ssid != ANJAY_SSID_BOOTSTRAP) { + _anjay_set_server_connection_status( + server, ANJAY_SERV_CONN_STATUS_REG_FAILURE); + } +#endif // ANJAY_WITH_CONN_STATUS_API _anjay_server_on_server_communication_error(server, avs_errno(AVS_EBADF)); } @@ -276,6 +322,30 @@ void _anjay_server_on_refreshed(anjay_server_info_t *server, anjay_log(TRACE, _("could not initialize sockets for SSID ") "%" PRIu16, server->ssid); + if (server->ssid != ANJAY_SSID_BOOTSTRAP) { + if (server->anjay->connection_error_is_registration_failure) { + anjay_connection_type_t conn_type; + ANJAY_CONNECTION_TYPE_FOREACH(conn_type) { + _anjay_observe_invalidate((anjay_connection_ref_t) { + .server = server, + .conn_type = conn_type + }); + } + ++server->registration_attempts; + server->registration_info.expire_time = + AVS_TIME_REAL_INVALID; + server->registration_info.update_forced = false; + // defined(ANJAY_WITH_CORE_PERSISTENCE) +#ifdef ANJAY_WITH_CONN_STATUS_API + _anjay_set_server_connection_status( + server, ANJAY_SERV_CONN_STATUS_REG_FAILURE); +#endif // ANJAY_WITH_CONN_STATUS_API + } else { + // Interrupt any registration flow that may be in progress + server->registration_attempts = 0; + server->registration_sequences_performed = 0; + } + } _anjay_server_on_server_communication_error(server, err); } else if (_anjay_socket_transport_supported(server->anjay, primary_conn->transport) @@ -310,6 +380,20 @@ void _anjay_server_on_refreshed(anjay_server_info_t *server, } // _anjay_bootstrap_request_if_appropriate() may fail only due to // failure to schedule a job. Not much that we can do about it then. +#ifdef ANJAY_WITH_CONN_STATUS_API + if (server->connection_status == ANJAY_SERV_CONN_STATUS_CONNECTING) { + if (server->refresh_failed) { + _anjay_set_server_connection_status( + server, ANJAY_SERV_CONN_STATUS_ERROR); + } else if (action == ANJAY_BOOTSTRAP_ACTION_NONE + && !avs_coap_exchange_id_valid( + server->anjay->bootstrap + .outgoing_request_exchange_id)) { + _anjay_set_server_connection_status( + server, ANJAY_SERV_CONN_STATUS_BOOTSTRAPPED); + } + } +#endif // ANJAY_WITH_CONN_STATUS_API } else { assert(avs_is_ok(err)); _anjay_server_ensure_valid_registration(server); @@ -319,6 +403,17 @@ void _anjay_server_on_refreshed(anjay_server_info_t *server, void _anjay_server_on_updated_registration(anjay_server_info_t *server, anjay_registration_result_t result, avs_error_t err) { +#ifdef ANJAY_WITH_CONN_STATUS_API + if ((result == ANJAY_REGISTRATION_ERROR_NETWORK + || result == ANJAY_REGISTRATION_ERROR_OTHER) + && server->connection_status + != ANJAY_SERV_CONN_STATUS_REG_FAILURE) { + _anjay_set_server_connection_status(server, + ANJAY_SERV_CONN_STATUS_ERROR); + } else { + _anjay_check_server_connection_status(server); + } +#endif // ANJAY_WITH_CONN_STATUS_API if (result == ANJAY_REGISTRATION_SUCCESS) { if (_anjay_server_reschedule_update_job(server)) { // Updates are retryable, we only need to reschedule after success @@ -626,6 +721,12 @@ static int disable_server_impl(anjay_unlocked_t *anjay, int anjay_disable_server(anjay_t *anjay_locked, anjay_ssid_t ssid) { int result = -1; ANJAY_MUTEX_LOCK(anjay, anjay_locked); +#ifdef ANJAY_WITH_CONN_STATUS_API + anjay_server_info_t *server = _anjay_servers_find(anjay, ssid); + if (server) { + _anjay_set_server_suspending_flag(server->anjay, server->ssid, true); + } +#endif // ANJAY_WITH_CONN_STATUS_API result = disable_server_impl( anjay, ssid, ANJAY_SERVER_NEXT_ACTION_DISABLE_WITH_TIMEOUT_FROM_DM, "ANJAY_SERVER_NEXT_ACTION_DISABLE_WITH_TIMEOUT_FROM_DM", @@ -667,6 +768,12 @@ int anjay_disable_server_with_timeout(anjay_t *anjay_locked, avs_time_duration_t timeout) { int result = -1; ANJAY_MUTEX_LOCK(anjay, anjay_locked); +#ifdef ANJAY_WITH_CONN_STATUS_API + anjay_server_info_t *server = _anjay_servers_find(anjay, ssid); + if (server) { + _anjay_set_server_suspending_flag(server->anjay, server->ssid, true); + } +#endif // ANJAY_WITH_CONN_STATUS_API result = _anjay_schedule_disable_server_with_explicit_timeout_unlocked( anjay, ssid, timeout); ANJAY_MUTEX_UNLOCK(anjay_locked); diff --git a/src/core/servers/anjay_connections.c b/src/core/servers/anjay_connections.c index b6709780..6e14b1a7 100644 --- a/src/core/servers/anjay_connections.c +++ b/src/core/servers/anjay_connections.c @@ -43,6 +43,10 @@ #include "anjay_security.h" #include "anjay_server_connections.h" +#ifdef ANJAY_WITH_CONN_STATUS_API +# include "../anjay_servers_inactive.h" +#endif // ANJAY_WITH_CONN_STATUS_API + VISIBILITY_SOURCE_BEGIN avs_net_socket_t *_anjay_connection_internal_get_socket( @@ -371,6 +375,14 @@ avs_error_t _anjay_server_connection_internal_bring_online( return AVS_OK; } +#ifdef ANJAY_WITH_CONN_STATUS_API + if (conn_type == ANJAY_CONNECTION_PRIMARY) { + _anjay_set_server_connection_status(server, + ANJAY_SERV_CONN_STATUS_CONNECTING); + } +#endif // ANJAY_WITH_CONN_STATUS_API + // defined(ANJAY_WITH_CORE_PERSISTENCE) + bool session_resumed; avs_error_t err = AVS_OK; if (avs_is_err((err = def->connect_socket(server->anjay, connection)))) { @@ -735,6 +747,92 @@ bool _anjay_socket_transport_supported(anjay_unlocked_t *anjay, return true; } +#ifdef ANJAY_WITH_CONN_STATUS_API +void _anjay_set_server_suspending_flag(anjay_unlocked_t *anjay, + anjay_ssid_t ssid, + bool val) { + assert(ssid != ANJAY_SSID_ANY); + anjay_server_info_t *server = _anjay_servers_find(anjay, ssid); + if (!server) { + return; + } + server->suspending = val; +} + +void _anjay_set_server_connection_status( + anjay_server_info_t *server, anjay_server_conn_status_t new_status) { + if (new_status != server->connection_status) { + server->connection_status = new_status; + if (server->anjay->server_connection_status_cb) { + ANJAY_MUTEX_UNLOCK_FOR_CALLBACK(anjay_locked, server->anjay); + server->anjay->server_connection_status_cb( + server->anjay->server_connection_status_cb_arg, + anjay_locked, server->ssid, new_status); + ANJAY_MUTEX_LOCK_AFTER_CALLBACK(anjay_locked); + } + } + + if (new_status == ANJAY_SERV_CONN_STATUS_REGISTERED + || new_status == ANJAY_SERV_CONN_STATUS_INVALID + || new_status == ANJAY_SERV_CONN_STATUS_INITIAL + || new_status == ANJAY_SERV_CONN_STATUS_ERROR + || new_status == ANJAY_SERV_CONN_STATUS_DEREGISTERING + || new_status == ANJAY_SERV_CONN_STATUS_SUSPENDING + || new_status == ANJAY_SERV_CONN_STATUS_UPDATING) { + server->reregistration = false; + } +} + +void _anjay_check_server_connection_status(anjay_server_info_t *server) { + if (!server || server->ssid == ANJAY_SSID_BOOTSTRAP) { + return; + } + assert(server->ssid != ANJAY_SSID_ANY); + + anjay_server_conn_status_t current_status = server->connection_status; + + if (_anjay_server_connection_active((anjay_connection_ref_t) { + .server = server, + .conn_type = ANJAY_CONNECTION_PRIMARY + })) { + /* expired mean that we aren't registered or lifetime is exceeded */ + if (!_anjay_server_registration_expired(server)) { + if (avs_coap_exchange_id_valid( + server->registration_exchange_state.exchange_id)) { + current_status = ANJAY_SERV_CONN_STATUS_UPDATING; + } else { + current_status = ANJAY_SERV_CONN_STATUS_REGISTERED; + } + } else if (avs_coap_exchange_id_valid( + server->registration_exchange_state.exchange_id)) { + current_status = server->reregistration + ? ANJAY_SERV_CONN_STATUS_REREGISTERING + : ANJAY_SERV_CONN_STATUS_REGISTERING; + } + } else { + // In the contexts in which this function is called, + // this could only be an error + current_status = ANJAY_SERV_CONN_STATUS_ERROR; + } + _anjay_set_server_connection_status(server, current_status); +} + +anjay_server_conn_status_t +anjay_get_server_connection_status(anjay_t *anjay, anjay_ssid_t ssid) { + if (ssid == ANJAY_SSID_ANY) { + return ANJAY_SERV_CONN_STATUS_INVALID; + } + anjay_server_conn_status_t ret = ANJAY_SERV_CONN_STATUS_INVALID; + ANJAY_MUTEX_LOCK(anjay_unlocked, anjay); + anjay_server_info_t *server = _anjay_servers_find(anjay_unlocked, ssid); + if (server) { + ret = server->connection_status; + } + ANJAY_MUTEX_UNLOCK(anjay); + return ret; +} +#endif // ANJAY_WITH_CONN_STATUS_API + #ifdef ANJAY_WITH_LWM2M11 avs_error_t _anjay_server_read_sni(anjay_unlocked_t *anjay, anjay_iid_t security_iid, diff --git a/src/core/servers/anjay_register.c b/src/core/servers/anjay_register.c index 0eee039c..31af1abb 100644 --- a/src/core/servers/anjay_register.c +++ b/src/core/servers/anjay_register.c @@ -33,6 +33,8 @@ #include "anjay_server_connections.h" #include "anjay_servers_internal.h" +// !defined(ANJAY_WITH_CONN_STATUS_API) + VISIBILITY_SOURCE_BEGIN /** Update messages are sent to the server every @@ -641,7 +643,13 @@ handle_register_response(anjay_server_info_t *server, if (result != ANJAY_REGISTRATION_SUCCESS) { anjay_log(WARNING, _("could not register to server ") "%u", _anjay_server_ssid(server)); - AVS_LIST_CLEAR(move_endpoint_path) {} + AVS_LIST_CLEAR(move_endpoint_path); +#ifdef ANJAY_WITH_CONN_STATUS_API + if (result != ANJAY_REGISTRATION_ERROR_TIMEOUT) { + _anjay_set_server_connection_status( + server, ANJAY_SERV_CONN_STATUS_REG_FAILURE); + } +#endif // ANJAY_WITH_CONN_STATUS_API } else { _anjay_server_update_registration_info( server, move_endpoint_path, attempted_version, @@ -814,6 +822,9 @@ static void send_register(anjay_server_info_t *server, _anjay_server_set_last_communication_time(server); #endif // ANJAY_WITH_COMMUNICATION_TIMESTAMP_API } +#ifdef ANJAY_WITH_CONN_STATUS_API + _anjay_check_server_connection_status(server); +#endif // ANJAY_WITH_CONN_STATUS_API cleanup: avs_coap_options_cleanup(&request.options); } @@ -1033,6 +1044,12 @@ receive_update_response(avs_coap_ctx_t *coap, server->registration_info.update_forced = true; return; } +#ifdef ANJAY_WITH_CONN_STATUS_API + if (result == ANJAY_REGISTRATION_ERROR_TIMEOUT + || result == ANJAY_REGISTRATION_ERROR_REJECTED) { + server->reregistration = true; + } +#endif // ANJAY_WITH_CONN_STATUS_API on_registration_update_result(server, &state->new_params, result, err); } @@ -1097,6 +1114,9 @@ static void send_update(anjay_server_info_t *server, _anjay_server_set_last_communication_time(server); #endif // ANJAY_WITH_COMMUNICATION_TIMESTAMP_API } +#ifdef ANJAY_WITH_CONN_STATUS_API + _anjay_check_server_connection_status(server); +#endif // ANJAY_WITH_CONN_STATUS_API end: avs_coap_options_cleanup(&request.options); } @@ -1232,6 +1252,11 @@ static avs_error_t deregister(anjay_server_info_t *server) { AVS_ASSERT(coap, "Register is not supposed to be called on a connection " "that has no CoAP context"); +# ifdef ANJAY_WITH_CONN_STATUS_API + _anjay_set_server_connection_status(server, + ANJAY_SERV_CONN_STATUS_DEREGISTERING); +# endif // ANJAY_WITH_CONN_STATUS_API + avs_coap_request_header_t request = { 0 }; avs_coap_response_header_t response = { 0 }; avs_error_t err = @@ -1255,6 +1280,10 @@ static avs_error_t deregister(anjay_server_info_t *server) { err = avs_errno(AVS_EPROTO); goto end; } +# ifdef ANJAY_WITH_CONN_STATUS_API + _anjay_set_server_connection_status(server, + ANJAY_SERV_CONN_STATUS_DEREGISTERED); +# endif // ANJAY_WITH_CONN_STATUS_API anjay_log(INFO, _("De-register sent")); err = AVS_OK; @@ -1280,11 +1309,19 @@ avs_error_t _anjay_server_deregister(anjay_server_info_t *server) { }; if (!_anjay_connection_get_online_socket(connection)) { anjay_log(ERROR, _("server connection is not online, skipping")); +# ifdef ANJAY_WITH_CONN_STATUS_API + _anjay_set_server_connection_status( + server, ANJAY_SERV_CONN_STATUS_DEREGISTERED); +# endif // ANJAY_WITH_CONN_STATUS_API return AVS_OK; } avs_error_t err = deregister(server); if (avs_is_err(err)) { +# ifdef ANJAY_WITH_CONN_STATUS_API + _anjay_set_server_connection_status(server, + ANJAY_SERV_CONN_STATUS_ERROR); +# endif // ANJAY_WITH_CONN_STATUS_API anjay_log(ERROR, _("could not send De-Register request: ") "%s", AVS_COAP_STRERROR(err)); } diff --git a/src/core/servers/anjay_reload.c b/src/core/servers/anjay_reload.c index 6c5bedf0..377d42a1 100644 --- a/src/core/servers/anjay_reload.c +++ b/src/core/servers/anjay_reload.c @@ -62,6 +62,10 @@ static int reload_server_by_ssid(anjay_unlocked_t *anjay, new_server->reactivate_time = avs_time_real_now(); result = _anjay_server_sched_activate(new_server); } +#ifdef ANJAY_WITH_CONN_STATUS_API + _anjay_set_server_connection_status(new_server, + ANJAY_SERV_CONN_STATUS_INITIAL); +#endif // ANJAY_WITH_CONN_STATUS_API return result; } diff --git a/src/core/servers/anjay_servers_internal.h b/src/core/servers/anjay_servers_internal.h index d3ed3de1..346c84b9 100644 --- a/src/core/servers/anjay_servers_internal.h +++ b/src/core/servers/anjay_servers_internal.h @@ -189,9 +189,12 @@ struct anjay_server_info_struct { /** * Number of attempted (potentially) failed registrations. It is incremented - * in send_register(), then compared (if non-zero) against "Communication - * Retry Count" resource in _anjay_server_on_failure(). When the - * registration succeeds, it is reset to 0. + * in send_register() (and also _anjay_server_on_refreshed() in case of + * connection error if connection_error_is_registration_failure is enabled), + * then compared (if non-zero) against "Communication Retry Count" resource + * in _anjay_server_on_failure(). When the registration succeeds or in case + * of a connection error (when connection_error_is_registration_failure is + * disabled), it is reset to 0. */ uint32_t registration_attempts; @@ -208,6 +211,31 @@ struct anjay_server_info_struct { */ avs_time_real_t last_communication_time; #endif // ANJAY_WITH_COMMUNICATION_TIMESTAMP_API + +#ifdef ANJAY_WITH_CONN_STATUS_API + /** + * Stores current server connection status. + */ + anjay_server_conn_status_t connection_status; + + /** + * Indicates that the server is in the process of suspending. It is checked + * by the ANJAY_SERVER_NEXT_ACTION_DISABLE_WITH_EXPLICIT_TIMEOUT and the + * ANJAY_SERVER_NEXT_ACTION_DISABLE_WITH_TIMEOUT_FROM_DM actions to + * see if it was called due to /1/x/4 resource execution, function + * anjay_disable_server call or function anjay_disable_server_with_timeout + * call. + */ + bool suspending; + + /** + * Indicates that re-registration will be carried out due to an error + * (ANJAY_REGISTRATION_ERROR_TIMEOUT or ANJAY_REGISTRATION_ERROR_REJECTED) + * during update operation. If the reregistration process fails, this flag + * keeps its value. + */ + bool reregistration; +#endif // ANJAY_WITH_CONN_STATUS_API }; #ifndef ANJAY_WITHOUT_DEREGISTER diff --git a/tests/integration/suites/default/notifications.py b/tests/integration/suites/default/notifications.py index 4a48c0b7..ae29da2d 100644 --- a/tests/integration/suites/default/notifications.py +++ b/tests/integration/suites/default/notifications.py @@ -224,3 +224,57 @@ def runTest(self): notif = self.serv.recv(timeout_s=5) self.assertEqual(notif.code, coap.Code.RES_NOT_FOUND) self.assertEqual(notif.token, orig_notif.token) + + +class NextPlannedNotify(test_suite.Lwm2mSingleServerTest, test_suite.Lwm2mDmOperations): + def setUp(self): + super().setUp() + + def runTest(self): + # set up observe without attributes + self.observe(self.serv, oid=OID.Device, iid=0, rid=RID.Device.ErrorCode) + + # ensure there is no planned notify + self.communicate('next-planned-pmax-notify', + match_regex='NEXT_PLANNED_PMAX_NOTIFY=TIME_INVALID\n') + self.communicate('next-planned-notify', + match_regex='NEXT_PLANNED_NOTIFY=TIME_INVALID\n') + + # add an observation with pmin&pmax + write_atts = self.write_attributes(self.serv, oid=OID.Device, iid=0, rid=RID.Device.Timezone, + query=['pmin=5','pmax=5']) + self.assertEqual(write_atts.code, coap.Code.RES_CHANGED) + + second_notif = self.observe(self.serv, oid=OID.Device, iid=0, rid=RID.Device.Timezone) + self.assertEqual(second_notif.code, coap.Code.RES_CONTENT) + + expected_notify_time = time.time() + 5.0 + + time.sleep(0.1) + + # verify that Anjay schedules the notifications + planned_notify_time = float( + self.communicate('next-planned-notify', + match_regex='NEXT_PLANNED_NOTIFY=(.*)\n').group(1)) + self.assertAlmostEqual(expected_notify_time, planned_notify_time, delta=0.1) + + # wait for the notification and check if next-planned-notify was rescheduled + notif = self.serv.recv(timeout_s=6) + self.assertEqual(notif.code, coap.Code.RES_CONTENT) + self.assertEqual(notif.token, second_notif.token) + + expected_notify_time = expected_notify_time + 5 + planned_notify_time = float( + self.communicate('next-planned-notify', + match_regex='NEXT_PLANNED_NOTIFY=(.*)\n').group(1)) + self.assertAlmostEqual(expected_notify_time, planned_notify_time, delta=0.1) + + # cancel the observation + self.observe(self.serv, oid=OID.Device, iid=0, rid=RID.Device.Timezone, observe=1, token=notif.token) + time.sleep(0.2) + + # ensure there is no planned notify + self.communicate('next-planned-pmax-notify', + match_regex='NEXT_PLANNED_PMAX_NOTIFY=TIME_INVALID\n') + self.communicate('next-planned-notify', + match_regex='NEXT_PLANNED_NOTIFY=TIME_INVALID\n') diff --git a/tests/integration/suites/default/retransmissions.py b/tests/integration/suites/default/retransmissions.py index a327587e..71e8e3f3 100644 --- a/tests/integration/suites/default/retransmissions.py +++ b/tests/integration/suites/default/retransmissions.py @@ -22,10 +22,10 @@ class TestMixin: # Note that these values differ from default ones in Anjay. This is done to # speed up the test execution a bit by limiting the number of retransmissions # as well as wait intervals between them. - ACK_RANDOM_FACTOR=1.0 - ACK_TIMEOUT=2.0 - MAX_RETRANSMIT=2 - CONFIRMABLE_NOTIFICATIONS=False + ACK_RANDOM_FACTOR = 1.0 + ACK_TIMEOUT = 2.0 + MAX_RETRANSMIT = 2 + CONFIRMABLE_NOTIFICATIONS = False def tx_params(self): return TxParams(ack_random_factor=self.ACK_RANDOM_FACTOR, @@ -33,17 +33,20 @@ def tx_params(self): max_retransmit=self.MAX_RETRANSMIT) def setUp(self, *args, **kwargs): - extra_cmdline_args=[ + extra_cmdline_args = [ '--ack-random-factor', str(self.tx_params().ack_random_factor), '--ack-timeout', str(self.tx_params().ack_timeout), '--max-retransmit', str(self.tx_params().max_retransmit), - '--dtls-hs-retry-wait-min', str(self.tx_params().first_retransmission_timeout()), - '--dtls-hs-retry-wait-max', str(self.tx_params().last_retransmission_timeout()), + '--dtls-hs-retry-wait-min', str( + self.tx_params().first_retransmission_timeout()), + '--dtls-hs-retry-wait-max', str( + self.tx_params().last_retransmission_timeout()), ] if self.CONFIRMABLE_NOTIFICATIONS: extra_cmdline_args += ['--confirmable-notifications'] if 'extra_cmdline_args' in kwargs: - kwargs['extra_cmdline_args'] = extra_cmdline_args + kwargs['extra_cmdline_args'] + kwargs['extra_cmdline_args'] = extra_cmdline_args + \ + kwargs['extra_cmdline_args'] super().setUp(*args, **kwargs) else: super().setUp(*args, **kwargs, extra_cmdline_args=extra_cmdline_args) @@ -98,7 +101,8 @@ def tearDown(self): def runTest(self): for i in range(self.MAX_RETRANSMIT + 1): - self.assertPktIsDtlsClientHello(self.serv._raw_udp_socket.recv(4096), seq_number=i) + self.assertPktIsDtlsClientHello( + self.serv._raw_udp_socket.recv(4096), seq_number=i) self.wait_for_retransmission_response_timeout() # Ensure that the control is given back to the user. @@ -108,8 +112,8 @@ def runTest(self): class DtlsRegisterTimeoutFallbacksToHsTest(RetransmissionTest.TestMixin, test_suite.Lwm2mDtlsSingleServerTest): # These settings speed up tests considerably. - MAX_RETRANSMIT=1 - ACK_TIMEOUT=1 + MAX_RETRANSMIT = 1 + ACK_TIMEOUT = 1 def tearDown(self): super().teardown_demo_with_servers(auto_deregister=False) @@ -119,16 +123,19 @@ def runTest(self): # Register only falls back to handshake if it's not performed immediately after one self.communicate('send-update') pkt = self.assertDemoUpdatesRegistration(respond=False) - self.serv.send(Lwm2mErrorResponse.matching(pkt)(code=coap.Code.RES_NOT_FOUND)) + self.serv.send(Lwm2mErrorResponse.matching(pkt) + (code=coap.Code.RES_NOT_FOUND)) # Ignore register requests. for _ in range(self.MAX_RETRANSMIT + 1): - self.assertDemoRegisters(respond=False, timeout_s=self.last_retransmission_timeout()) + self.assertDemoRegisters( + respond=False, timeout_s=self.last_retransmission_timeout()) self.wait_for_retransmission_response_timeout() # Demo should fall back to DTLS handshake. - self.assertPktIsDtlsClientHello(self.serv._raw_udp_socket.recv(4096), seq_number=0) + self.assertPktIsDtlsClientHello( + self.serv._raw_udp_socket.recv(4096), seq_number=0) self.wait_for_retransmission_response_timeout() # Ensure that the control is given back to the user. @@ -188,13 +195,15 @@ def runTest(self): self.serv.close() # Wait for ICMP port unreachable. - self.wait_until_icmp_unreachable_count(1, timeout_s=self.last_retransmission_timeout()) + self.wait_until_icmp_unreachable_count( + 1, timeout_s=self.last_retransmission_timeout()) self.wait_for_retransmission_response_timeout() # Ensure that no more retransmissions occurred. self.assertEqual(1, self.count_icmp_unreachable_packets()) # Ensure that no more dtls handshake messages occurred. - self.assertEqual(num_initial_dtls_hs_packets, self.count_dtls_client_hello_packets()) + self.assertEqual(num_initial_dtls_hs_packets, + self.count_dtls_client_hello_packets()) # Ensure that the control is given back to the user. self.assertTrue(self.get_all_connections_failed()) @@ -203,8 +212,8 @@ def runTest(self): class RegisterIcmpTest(test_suite.PcapEnabledTest, RetransmissionTest.TestMixin, test_suite.Lwm2mSingleServerTest): - MAX_RETRANSMIT=1 - ACK_TIMEOUT=4 + MAX_RETRANSMIT = 1 + ACK_TIMEOUT = 4 def setUp(self): super().setUp(auto_register=False) @@ -219,7 +228,8 @@ def runTest(self): # Force Register self.communicate('reconnect') # Wait for ICMP port unreachable. - self.wait_until_icmp_unreachable_count(1, timeout_s=self.last_retransmission_timeout()) + self.wait_until_icmp_unreachable_count( + 1, timeout_s=self.last_retransmission_timeout()) # Ensure that the control is given back to the user. with self.assertRaises(socket.timeout, msg="unexpected packets from the client"): @@ -264,7 +274,8 @@ def runTest(self): # Close socket to induce ICMP port unreachables. self.serv.close() self.communicate('trim-servers 0') - self.wait_until_icmp_unreachable_count(1, timeout_s=2 * self.last_retransmission_timeout()) + self.wait_until_icmp_unreachable_count( + 1, timeout_s=2 * self.last_retransmission_timeout()) # Give demo time to realize deregister failed. time.sleep(self.ACK_TIMEOUT * self.ACK_RANDOM_FACTOR) # Ensure that no more retransmissions occurred. @@ -291,22 +302,27 @@ def tearDown(self): def runTest(self): new_lifetime = int(2 * self.max_transmit_wait()) # Change lifetime to 2*MAX_TRANSMIT_WAIT - self.write_resource(self.serv, oid=OID.Server, iid=1, rid=RID.Server.Lifetime, content=str(new_lifetime)) + self.write_resource(self.serv, oid=OID.Server, iid=1, + rid=RID.Server.Lifetime, content=str(new_lifetime)) self.assertDemoUpdatesRegistration(lifetime=new_lifetime) # Demo should attempt to update registration. for _ in range(self.MAX_RETRANSMIT + 1): - self.assertDemoUpdatesRegistration(respond=False, timeout_s=new_lifetime) + self.assertDemoUpdatesRegistration( + respond=False, timeout_s=new_lifetime) self.wait_for_retransmission_response_timeout() # Demo should re-register for _ in range(self.MAX_RETRANSMIT + 1): - self.assertDemoRegisters(respond=False, lifetime=new_lifetime, timeout_s=self.max_transmit_wait()) + self.assertDemoRegisters( + respond=False, lifetime=new_lifetime, timeout_s=self.max_transmit_wait()) self.wait_for_retransmission_response_timeout() - self.serv._raw_udp_socket.settimeout(self.last_retransmission_timeout() + 1) + self.serv._raw_udp_socket.settimeout( + self.last_retransmission_timeout() + 1) # Demo should attempt handshake for i in range(self.MAX_RETRANSMIT + 1): - self.assertPktIsDtlsClientHello(self.serv._raw_udp_socket.recv(4096)) + self.assertPktIsDtlsClientHello( + self.serv._raw_udp_socket.recv(4096)) self.wait_for_retransmission_response_timeout() with self.assertRaises(socket.timeout, msg="unexpected packets from the client"): @@ -321,11 +337,13 @@ class UpdateTimeoutFallbacksToRegisterTest(RetransmissionTest.TestMixin, def runTest(self): new_lifetime = int(2 * self.max_transmit_wait()) # Change lifetime to 2*MAX_TRANSMIT_WAIT - self.write_resource(self.serv, oid=OID.Server, iid=1, rid=RID.Server.Lifetime, content=str(new_lifetime)) + self.write_resource(self.serv, oid=OID.Server, iid=1, + rid=RID.Server.Lifetime, content=str(new_lifetime)) self.assertDemoUpdatesRegistration(lifetime=new_lifetime) # Demo should attempt to update registration. for _ in range(self.MAX_RETRANSMIT + 1): - self.assertDemoUpdatesRegistration(respond=False, timeout_s=new_lifetime) + self.assertDemoUpdatesRegistration( + respond=False, timeout_s=new_lifetime) self.wait_for_retransmission_response_timeout() # Demo should re-register @@ -341,7 +359,8 @@ def setUp(self): def runTest(self): self.communicate('send-update') for _ in range(self.MAX_RETRANSMIT + 1): - self.assertDemoUpdatesRegistration(respond=False, timeout_s=2 * self.max_transmit_wait()) + self.assertDemoUpdatesRegistration( + respond=False, timeout_s=2 * self.max_transmit_wait()) self.wait_for_retransmission_response_timeout() # Demo should re-register @@ -374,7 +393,8 @@ def runTest(self): # Ensure that no more retransmissions occurred. self.assertEqual(1, self.count_icmp_unreachable_packets()) # Ensure that no more dtls handshake messages occurred. - self.assertEqual(num_initial_dtls_hs_packets, self.count_dtls_client_hello_packets()) + self.assertEqual(num_initial_dtls_hs_packets, + self.count_dtls_client_hello_packets()) # Ensure that the control is given back to the user. self.assertTrue(self.get_all_connections_failed()) @@ -393,8 +413,8 @@ class UpdateIcmpTest(UpdateFailsOnIcmpTest.TestMixin, class DtlsRequestBootstrapTimeoutFallbacksToHsTest(RetransmissionTest.TestMixin, test_suite.Lwm2mDtlsSingleServerTest): # These settings speed up tests considerably. - MAX_RETRANSMIT=1 - ACK_TIMEOUT=1 + MAX_RETRANSMIT = 1 + ACK_TIMEOUT = 1 def setUp(self): super().setUp(bootstrap_server=Lwm2mServer(coap.DtlsServer(psk_identity=self.PSK_IDENTITY, psk_key=self.PSK_KEY)), @@ -411,19 +431,23 @@ def runTest(self): # Request Bootstrap only falls back to handshake if it's not performed immediately after one self.communicate('send-update') pkt = self.assertDemoUpdatesRegistration(respond=False) - self.serv.send(Lwm2mErrorResponse.matching(pkt)(code=coap.Code.RES_FORBIDDEN)) + self.serv.send(Lwm2mErrorResponse.matching(pkt) + (code=coap.Code.RES_FORBIDDEN)) pkt = self.assertDemoRegisters(respond=False) - self.serv.send(Lwm2mErrorResponse.matching(pkt)(code=coap.Code.RES_FORBIDDEN)) + self.serv.send(Lwm2mErrorResponse.matching(pkt) + (code=coap.Code.RES_FORBIDDEN)) # Ignore Request Bootstrap requests. for _ in range(self.MAX_RETRANSMIT + 1): - pkt = self.bootstrap_server.recv(timeout_s=self.last_retransmission_timeout()) + pkt = self.bootstrap_server.recv( + timeout_s=self.last_retransmission_timeout()) self.assertIsInstance(pkt, Lwm2mRequestBootstrap) self.wait_for_retransmission_response_timeout() # Demo should fall back to DTLS handshake. - self.assertPktIsDtlsClientHello(self.bootstrap_server._raw_udp_socket.recv(4096), seq_number=0) + self.assertPktIsDtlsClientHello( + self.bootstrap_server._raw_udp_socket.recv(4096), seq_number=0) self.wait_for_retransmission_response_timeout() # Ensure that the control is given back to the user. @@ -442,7 +466,8 @@ def setUp(self, bootstrap_server=True, *args, **kwargs): def runTest(self): # Ignore Request Bootstrap requests. for _ in range(self.MAX_RETRANSMIT + 1): - pkt = self.bootstrap_server.recv(timeout_s=self.last_retransmission_timeout() + 5) + pkt = self.bootstrap_server.recv( + timeout_s=self.last_retransmission_timeout() + 5) self.assertIsInstance(pkt, Lwm2mRequestBootstrap) self.wait_for_retransmission_response_timeout() @@ -486,16 +511,19 @@ def runTest(self): self.bootstrap_server.close() # respond with Forbidden to Register so that client falls back to Bootstrap - self.serv.send(Lwm2mErrorResponse.matching(pkt)(code=coap.Code.RES_FORBIDDEN)) + self.serv.send(Lwm2mErrorResponse.matching(pkt) + (code=coap.Code.RES_FORBIDDEN)) # Wait for ICMP port unreachable. - self.wait_until_icmp_unreachable_count(1, timeout_s=self.last_retransmission_timeout()) + self.wait_until_icmp_unreachable_count( + 1, timeout_s=self.last_retransmission_timeout()) self.wait_for_retransmission_response_timeout() # Ensure that no more retransmissions occurred. self.assertEqual(1, self.count_icmp_unreachable_packets()) # Ensure that no more dtls handshake messages occurred. - self.assertEqual(num_initial_dtls_hs_packets, self.count_dtls_client_hello_packets()) + self.assertEqual(num_initial_dtls_hs_packets, + self.count_dtls_client_hello_packets()) # Ensure that the control is given back to the user. self.assertTrue(self.get_all_connections_failed()) @@ -504,8 +532,8 @@ def runTest(self): class RequestBootstrapIcmpTest(test_suite.PcapEnabledTest, RetransmissionTest.TestMixin, test_suite.Lwm2mTest): - MAX_RETRANSMIT=1 - ACK_TIMEOUT=4 + MAX_RETRANSMIT = 1 + ACK_TIMEOUT = 4 def setUp(self): super().setUp(servers=0, bootstrap_server=True) @@ -522,7 +550,8 @@ def runTest(self): # Force Register self.communicate('reconnect') # Wait for ICMP port unreachable. - self.wait_until_icmp_unreachable_count(1, timeout_s=self.last_retransmission_timeout()) + self.wait_until_icmp_unreachable_count( + 1, timeout_s=self.last_retransmission_timeout()) # Ensure that the control is given back to the user. with self.assertRaises(socket.timeout, msg="unexpected packets from the client"): @@ -541,7 +570,7 @@ class NotificationIcmpTest(test_suite.PcapEnabledTest, RetransmissionTest.TestMixin, test_suite.Lwm2mSingleServerTest, test_suite.Lwm2mDmOperations): - CONFIRMABLE_NOTIFICATIONS=True + CONFIRMABLE_NOTIFICATIONS = True def tearDown(self): super().teardown_demo_with_servers(auto_deregister=False) @@ -567,7 +596,7 @@ class NotificationDtlsFailsOnIcmpTest(test_suite.PcapEnabledTest, RetransmissionTest.TestMixin, test_suite.Lwm2mDtlsSingleServerTest, test_suite.Lwm2mDmOperations): - CONFIRMABLE_NOTIFICATIONS=True + CONFIRMABLE_NOTIFICATIONS = True def tearDown(self): super().teardown_demo_with_servers(auto_deregister=False) @@ -591,18 +620,88 @@ def runTest(self): # Ensure that no more retransmissions occurred. self.assertEqual(1, self.count_icmp_unreachable_packets()) # Ensure that no more dtls handshake messages occurred. - self.assertEqual(num_initial_dtls_hs_packets, self.count_dtls_client_hello_packets()) + self.assertEqual(num_initial_dtls_hs_packets, + self.count_dtls_client_hello_packets()) # Ensure that the control is given back to the user. self.assertTrue(self.get_all_connections_failed()) +class NotificationTimeoutIsIgnored: + class TestMixin(RetransmissionTest.TestMixin, + test_suite.Lwm2mDmOperations): + CONFIRMABLE_NOTIFICATIONS = True + + def runTest(self): + import subprocess + import unittest + + output = subprocess.run([self._get_demo_executable(), '-e', 'dummy', '-u', 'invalid'], + stdout=subprocess.PIPE, stderr=subprocess.STDOUT).stdout.decode( + 'utf-8') + + if 'WITH_AVS_COAP_OBSERVE_CANCEL_ON_TIMEOUT = ON' in output: + raise unittest.SkipTest( + 'Timeout cancels observation in that configuration') + + # Trigger a CON notification + self.create_instance(self.serv, oid=OID.Test, iid=1) + + # force an Update so that change to the data model does not get notified later + self.communicate('send-update') + self.assertDemoUpdatesRegistration(content=ANY) + + self.observe(self.serv, oid=OID.Test, iid=1, rid=RID.Test.Counter) + self.execute_resource(self.serv, oid=OID.Test, + iid=1, rid=RID.Test.IncrementCounter) + + first_pkt = self.serv.recv(timeout_s=2) + first_attempt = time.time() + self.assertIsInstance(first_pkt, Lwm2mNotify) + + for attempt in range(self.MAX_RETRANSMIT): + self.assertIsInstance( + self.serv.recv(timeout_s=30), Lwm2mNotify) + last_attempt = time.time() + + transmit_span_lower_bound = self.ACK_TIMEOUT * \ + ((2 ** self.MAX_RETRANSMIT) - 1) + transmit_span_upper_bound = transmit_span_lower_bound * self.ACK_RANDOM_FACTOR + + self.assertGreater(last_attempt - first_attempt, + transmit_span_lower_bound - 1) + self.assertLess(last_attempt - first_attempt, + transmit_span_upper_bound + 1) + + time.sleep(self.last_retransmission_timeout() + 1) + + # check that following notifications still trigger attempts to send the value + self.execute_resource(self.serv, oid=OID.Test, + iid=1, rid=RID.Test.IncrementCounter) + pkt = self.serv.recv( + timeout_s=self.last_retransmission_timeout() + 2) + self.assertIsInstance(pkt, Lwm2mNotify) + self.assertEqual(pkt.content, first_pkt.content) + self.serv.send(Lwm2mReset.matching(pkt)()) + + class NotificationTimeoutCancelsObservation: class TestMixin(RetransmissionTest.TestMixin, test_suite.Lwm2mDmOperations): CONFIRMABLE_NOTIFICATIONS = True def runTest(self): + import subprocess + import unittest + + output = subprocess.run([self._get_demo_executable(), '-e', 'dummy', '-u', 'invalid'], + stdout=subprocess.PIPE, stderr=subprocess.STDOUT).stdout.decode( + 'utf-8') + + if 'WITH_AVS_COAP_OBSERVE_CANCEL_ON_TIMEOUT = OFF' in output: + raise unittest.SkipTest( + 'Timeout does not cancel observation in that configuration') + # Trigger a CON notification self.create_instance(self.serv, oid=OID.Test, iid=1) @@ -611,28 +710,45 @@ def runTest(self): self.assertDemoUpdatesRegistration(content=ANY) self.observe(self.serv, oid=OID.Test, iid=1, rid=RID.Test.Counter) - self.execute_resource(self.serv, oid=OID.Test, iid=1, rid=RID.Test.IncrementCounter) + self.execute_resource(self.serv, oid=OID.Test, + iid=1, rid=RID.Test.IncrementCounter) first_pkt = self.serv.recv(timeout_s=2) first_attempt = time.time() self.assertIsInstance(first_pkt, Lwm2mNotify) for attempt in range(self.MAX_RETRANSMIT): - self.assertIsInstance(self.serv.recv(timeout_s=30), Lwm2mNotify) + self.assertIsInstance( + self.serv.recv(timeout_s=30), Lwm2mNotify) last_attempt = time.time() - transmit_span_lower_bound = self.ACK_TIMEOUT * ((2 ** self.MAX_RETRANSMIT) - 1) + transmit_span_lower_bound = self.ACK_TIMEOUT * \ + ((2 ** self.MAX_RETRANSMIT) - 1) transmit_span_upper_bound = transmit_span_lower_bound * self.ACK_RANDOM_FACTOR - self.assertGreater(last_attempt - first_attempt, transmit_span_lower_bound - 1) - self.assertLess(last_attempt - first_attempt, transmit_span_upper_bound + 1) + self.assertGreater(last_attempt - first_attempt, + transmit_span_lower_bound - 1) + self.assertLess(last_attempt - first_attempt, + transmit_span_upper_bound + 1) time.sleep(self.last_retransmission_timeout() + 1) # check that the observation is really cancelled - self.execute_resource(self.serv, oid=OID.Test, iid=1, rid=RID.Test.IncrementCounter) + self.execute_resource(self.serv, oid=OID.Test, + iid=1, rid=RID.Test.IncrementCounter) with self.assertRaises(socket.timeout): - print(self.serv.recv(timeout_s=self.last_retransmission_timeout() + 5)) + print(self.serv.recv( + timeout_s=self.last_retransmission_timeout() + 5)) + + +class NotificationDtlsTimeoutIsIgnoredTest(NotificationTimeoutIsIgnored.TestMixin, + test_suite.Lwm2mDtlsSingleServerTest): + pass + + +class NotificationTimeoutIsIgnoredTest(NotificationTimeoutIsIgnored.TestMixin, + test_suite.Lwm2mSingleServerTest): + pass class NotificationDtlsTimeoutCancelsObservationTest(NotificationTimeoutCancelsObservation.TestMixin, @@ -648,8 +764,8 @@ class NotificationTimeoutCancelsObservationTest(NotificationTimeoutCancelsObserv class ReplacedBootstrapServerReconnectTest(RetransmissionTest.TestMixin, test_suite.Lwm2mDtlsSingleServerTest, test_suite.Lwm2mDmOperations): - MAX_RETRANSMIT=1 - ACK_TIMEOUT=1 + MAX_RETRANSMIT = 1 + ACK_TIMEOUT = 1 def setUp(self): super().setUp(bootstrap_server=Lwm2mServer(coap.DtlsServer(psk_identity=self.PSK_IDENTITY, psk_key=self.PSK_KEY)), @@ -672,33 +788,38 @@ def runTest(self): # replace the existing instance self.write_instance(self.bootstrap_server, oid=OID.Security, iid=1, - content=TLV.make_resource(RID.Security.ServerURI, 'coaps://127.0.0.1:%d' % self.bootstrap_server.get_listen_port()).serialize() - + TLV.make_resource(RID.Security.Bootstrap, 1).serialize() - + TLV.make_resource(RID.Security.Mode, coap.server.SecurityMode.PreSharedKey.value).serialize() - + TLV.make_resource(RID.Security.PKOrIdentity, self.PSK_IDENTITY).serialize() - + TLV.make_resource(RID.Security.SecretKey, self.PSK_KEY).serialize()) + content=TLV.make_resource( + RID.Security.ServerURI, 'coaps://127.0.0.1:%d' % self.bootstrap_server.get_listen_port()).serialize() + + TLV.make_resource(RID.Security.Bootstrap, 1).serialize() + + TLV.make_resource(RID.Security.Mode, coap.server.SecurityMode.PreSharedKey.value).serialize() + + TLV.make_resource(RID.Security.PKOrIdentity, self.PSK_IDENTITY).serialize() + + TLV.make_resource(RID.Security.SecretKey, self.PSK_KEY).serialize()) # provision the regular Server instance self.write_instance(self.bootstrap_server, oid=OID.Security, iid=2, - content=TLV.make_resource(RID.Security.ServerURI, 'coaps://127.0.0.1:%d' % self.serv.get_listen_port()).serialize() - + TLV.make_resource(RID.Security.Bootstrap, 0).serialize() - + TLV.make_resource(RID.Security.Mode, coap.server.SecurityMode.PreSharedKey.value).serialize() - + TLV.make_resource(RID.Security.ShortServerID, 2).serialize() - + TLV.make_resource(RID.Security.PKOrIdentity, self.PSK_IDENTITY).serialize() - + TLV.make_resource(RID.Security.SecretKey, self.PSK_KEY).serialize()) + content=TLV.make_resource( + RID.Security.ServerURI, 'coaps://127.0.0.1:%d' % self.serv.get_listen_port()).serialize() + + TLV.make_resource(RID.Security.Bootstrap, 0).serialize() + + TLV.make_resource(RID.Security.Mode, coap.server.SecurityMode.PreSharedKey.value).serialize() + + TLV.make_resource(RID.Security.ShortServerID, 2).serialize() + + TLV.make_resource(RID.Security.PKOrIdentity, self.PSK_IDENTITY).serialize() + + TLV.make_resource(RID.Security.SecretKey, self.PSK_KEY).serialize()) self.write_instance(self.bootstrap_server, oid=OID.Server, iid=2, - content=TLV.make_resource(RID.Server.Lifetime, int(lifetime)).serialize() - + TLV.make_resource(RID.Server.ShortServerID, 2).serialize() - + TLV.make_resource(RID.Server.NotificationStoring, True).serialize() - + TLV.make_resource(RID.Server.Binding, "U").serialize() - + TLV.make_resource(RID.Server.ServerCommunicationRetryCount, 1).serialize() - + TLV.make_resource(RID.Server.ServerCommunicationSequenceRetryCount, 1).serialize() - ) + content=TLV.make_resource( + RID.Server.Lifetime, int(lifetime)).serialize() + + TLV.make_resource(RID.Server.ShortServerID, 2).serialize() + + TLV.make_resource(RID.Server.NotificationStoring, True).serialize() + + TLV.make_resource(RID.Server.Binding, + "U").serialize() + + TLV.make_resource(RID.Server.ServerCommunicationRetryCount, 1).serialize() + + TLV.make_resource(RID.Server.ServerCommunicationSequenceRetryCount, 1).serialize() + ) # Bootstrap Finish pkt = Lwm2mBootstrapFinish() self.bootstrap_server.send(pkt) - self.assertMsgEqual(Lwm2mChanged.matching(pkt)(), self.bootstrap_server.recv()) + self.assertMsgEqual(Lwm2mChanged.matching(pkt)(), + self.bootstrap_server.recv()) # demo will refresh Bootstrap connection... self.assertDtlsReconnect(self.bootstrap_server) @@ -708,20 +829,26 @@ def runTest(self): # let the Update fail self.assertIsInstance(self.serv.recv(timeout_s=lifetime), Lwm2mUpdate) - self.assertIsInstance(self.serv.recv(timeout_s=self.ACK_TIMEOUT + 1), Lwm2mUpdate) + self.assertIsInstance(self.serv.recv( + timeout_s=self.ACK_TIMEOUT + 1), Lwm2mUpdate) # client falls back to Register pkt = self.serv.recv(timeout_s=lifetime) self.assertIsInstance(pkt, Lwm2mRegister) - self.serv.send(Lwm2mErrorResponse.matching(pkt)(coap.Code.RES_FORBIDDEN)) + self.serv.send(Lwm2mErrorResponse.matching(pkt) + (coap.Code.RES_FORBIDDEN)) # now the client falls back to Bootstrap, and doesn't get response - self.assertIsInstance(self.bootstrap_server.recv(), Lwm2mRequestBootstrap) - self.assertIsInstance(self.bootstrap_server.recv(timeout_s=self.ACK_TIMEOUT + 1), Lwm2mRequestBootstrap) + self.assertIsInstance( + self.bootstrap_server.recv(), Lwm2mRequestBootstrap) + self.assertIsInstance(self.bootstrap_server.recv( + timeout_s=self.ACK_TIMEOUT + 1), Lwm2mRequestBootstrap) # rehandshake should appear here - self.assertDtlsReconnect(self.bootstrap_server, timeout_s=2*self.ACK_TIMEOUT + 1) - self.assertIsInstance(self.bootstrap_server.recv(), Lwm2mRequestBootstrap) + self.assertDtlsReconnect( + self.bootstrap_server, timeout_s=2*self.ACK_TIMEOUT + 1) + self.assertIsInstance( + self.bootstrap_server.recv(), Lwm2mRequestBootstrap) class ModifyingTxParams(RetransmissionTest.TestMixin, bootstrap_client.BootstrapTest.Test): @@ -745,8 +872,10 @@ def runTest(self): self.assertMsgEqual(pkt2, pkt3) # check that it used the initial transmission params - self.assertAlmostEqual(pkt2_time - pkt1_time, self.ACK_TIMEOUT, delta=0.5) - self.assertAlmostEqual(pkt3_time - pkt2_time, 2.0 * self.ACK_TIMEOUT, delta=0.5) + self.assertAlmostEqual(pkt2_time - pkt1_time, + self.ACK_TIMEOUT, delta=0.5) + self.assertAlmostEqual(pkt3_time - pkt2_time, + 2.0 * self.ACK_TIMEOUT, delta=0.5) # Now let's change the transmission params # ACK_TIMEOUT=5, ACK_RANDOM_FACTOR=1, MAX_RETRANSMIT=1, NSTART=1 @@ -760,13 +889,15 @@ def runTest(self): # so check that the new ACK_TIMEOUT is in effect for the 1.0 attempt pkt1 = self.bootstrap_server.recv(timeout_s=self.max_transmit_wait()) pkt1_time = time.time() - self.assertMsgEqual(Lwm2mRequestBootstrap(endpoint_name=DEMO_ENDPOINT_NAME), pkt1) + self.assertMsgEqual(Lwm2mRequestBootstrap( + endpoint_name=DEMO_ENDPOINT_NAME), pkt1) pkt2 = self.bootstrap_server.recv(timeout_s=self.max_transmit_wait()) pkt2_time = time.time() self.assertMsgEqual(pkt1, pkt2) - self.assertAlmostEqual(pkt2_time - pkt1_time, self.ACK_TIMEOUT, delta=0.5) + self.assertAlmostEqual(pkt2_time - pkt1_time, + self.ACK_TIMEOUT, delta=0.5) # Respond to Request Bootstrap self.bootstrap_server.send(Lwm2mChanged.matching(pkt2)()) @@ -784,9 +915,11 @@ def runTest(self): pkt2_time = time.time() self.assertMsgEqual(pkt1, pkt2) - self.assertAlmostEqual(pkt2_time - pkt1_time, self.ACK_TIMEOUT, delta=0.5) + self.assertAlmostEqual(pkt2_time - pkt1_time, + self.ACK_TIMEOUT, delta=0.5) - self.serv.send(Lwm2mCreated.matching(pkt2)(location=self.DEFAULT_REGISTER_ENDPOINT)) + self.serv.send(Lwm2mCreated.matching(pkt2)( + location=self.DEFAULT_REGISTER_ENDPOINT)) # check that downloads also use the new transmission params dl_server = coap.Server() @@ -802,7 +935,8 @@ def runTest(self): pkt2_time = time.time() self.assertMsgEqual(pkt1, pkt2) - self.assertAlmostEqual(pkt2_time - pkt1_time, self.ACK_TIMEOUT, delta=0.5) + self.assertAlmostEqual( + pkt2_time - pkt1_time, self.ACK_TIMEOUT, delta=0.5) dl_server.send(Lwm2mErrorResponse.matching(pkt2)( code=coap.Code.RES_NOT_FOUND).fill_placeholders()) @@ -817,14 +951,18 @@ def runTest(self): self.assertDemoRequestsBootstrap() # change exchange lifetime to 2 seconds - self.communicate('set-coap-exchange-timeout udp %s' % (self.EXCHANGE_LIFETIME,)) + self.communicate('set-coap-exchange-timeout udp %s' % + (self.EXCHANGE_LIFETIME,)) # Create typical Server Object instance server_entries_no_ssid = [TLV.make_resource(RID.Server.Lifetime, 86400), - TLV.make_resource(RID.Server.NotificationStoring, True), + TLV.make_resource( + RID.Server.NotificationStoring, True), TLV.make_resource(RID.Server.Binding, "U"), - TLV.make_resource(RID.Server.ServerCommunicationRetryCount, 1), - TLV.make_resource(RID.Server.ServerCommunicationRetryTimer, 0), + TLV.make_resource( + RID.Server.ServerCommunicationRetryCount, 1), + TLV.make_resource( + RID.Server.ServerCommunicationRetryTimer, 0), TLV.make_resource( RID.Server.ServerCommunicationSequenceRetryCount, 1), TLV.make_resource( @@ -849,7 +987,8 @@ def runTest(self): req = packets[0] self.bootstrap_server.send(req) - self.assertMsgEqual(Lwm2mContinue.matching(req)(), self.bootstrap_server.recv()) + self.assertMsgEqual(Lwm2mContinue.matching(req)(), + self.bootstrap_server.recv()) # Wait for the exchange to time out time.sleep(self.EXCHANGE_LIFETIME + 0.5) @@ -858,12 +997,15 @@ def runTest(self): req = packets[1] self.bootstrap_server.send(req) self.assertMsgEqual( - Lwm2mErrorResponse.matching(req)(code=coap.Code.RES_REQUEST_ENTITY_INCOMPLETE), + Lwm2mErrorResponse.matching(req)( + code=coap.Code.RES_REQUEST_ENTITY_INCOMPLETE), self.bootstrap_server.recv()) # That's tested, now bootstrap normally - self.write_instance(self.bootstrap_server, oid=OID.Server, iid=1, content=server_tlv) - self.write_instance(self.bootstrap_server, oid=OID.Security, iid=1, content=security_tlv) + self.write_instance(self.bootstrap_server, + oid=OID.Server, iid=1, content=server_tlv) + self.write_instance(self.bootstrap_server, + oid=OID.Security, iid=1, content=security_tlv) self.perform_bootstrap_finish() self.assertDemoRegisters() @@ -886,7 +1028,8 @@ def runTest(self): req = packets[1] self.serv.send(req) self.assertMsgEqual( - Lwm2mErrorResponse.matching(req)(code=coap.Code.RES_REQUEST_ENTITY_INCOMPLETE), + Lwm2mErrorResponse.matching(req)( + code=coap.Code.RES_REQUEST_ENTITY_INCOMPLETE), self.serv.recv()) @@ -917,7 +1060,8 @@ def runTest(self): self.assertPktIsDtlsClientHello(self.serv._raw_udp_socket.recv(65536)) mgmt_hello_time = time.time() - self.assertPktIsDtlsClientHello(self.bootstrap_server._raw_udp_socket.recv(65536)) + self.assertPktIsDtlsClientHello( + self.bootstrap_server._raw_udp_socket.recv(65536)) bootstrap_hello_time = time.time() self.wait_until_socket_count(0, timeout_s=self.HANDSHAKE_TIMEOUT + 1)