/** * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0. */ #include "mqtt_client_connection.h" #include "mqtt_client.h" #include "http_connection.h" #include "http_message.h" #include #include #include #include #include #include static const char *AWS_NAPI_KEY_INCOMPLETE_OPERATION_COUNT = "incompleteOperationCount"; static const char *AWS_NAPI_KEY_INCOMPLETE_OPERATION_SIZE = "incompleteOperationSize"; static const char *AWS_NAPI_KEY_UNACKED_OPERATION_COUNT = "unackedOperationCount"; static const char *AWS_NAPI_KEY_UNACKED_OPERATION_SIZE = "unackedOperationSize"; static void s_transform_websocket_call(napi_env env, napi_value transform_websocket, void *context, void *user_data); void s_transform_websocket( struct aws_http_message *request, void *user_data, aws_mqtt_transform_websocket_handshake_complete_fn *complete_fn, void *complete_ctx); struct mqtt_connection_binding { struct aws_allocator *allocator; bool use_tls_options; struct aws_tls_connection_options tls_options; struct aws_mqtt_client_connection *connection; napi_env env; napi_ref node_external; napi_threadsafe_function on_connection_interrupted; napi_threadsafe_function on_connection_resumed; napi_threadsafe_function on_any_publish; napi_threadsafe_function transform_websocket; napi_threadsafe_function on_closed; napi_threadsafe_function on_connection_success; napi_threadsafe_function on_connection_failure; bool first_successfull_connection; }; struct aws_mqtt_client_connection *aws_napi_get_mqtt_client_connection_from_binding( struct mqtt_connection_binding *binding) { if (binding == NULL) { return NULL; } return binding->connection; } static void s_mqtt_client_connection_release_threadsafe_function_on_failure(struct mqtt_connection_binding *binding) { if (binding->on_connection_failure != NULL) { AWS_NAPI_ENSURE( binding->env, aws_napi_release_threadsafe_function(binding->on_connection_failure, napi_tsfn_abort)); binding->on_connection_failure = NULL; } } static void s_mqtt_client_connection_release_threadsafe_function(struct mqtt_connection_binding *binding) { if (binding->on_connection_interrupted != NULL) { AWS_NAPI_ENSURE( binding->env, aws_napi_release_threadsafe_function(binding->on_connection_interrupted, napi_tsfn_abort)); binding->on_connection_interrupted = NULL; } if (binding->on_connection_resumed != NULL) { AWS_NAPI_ENSURE( binding->env, aws_napi_release_threadsafe_function(binding->on_connection_resumed, napi_tsfn_abort)); binding->on_connection_resumed = NULL; } if (binding->on_any_publish != NULL) { AWS_NAPI_ENSURE(binding->env, aws_napi_release_threadsafe_function(binding->on_any_publish, napi_tsfn_abort)); binding->on_any_publish = NULL; } if (binding->transform_websocket != NULL) { AWS_NAPI_ENSURE( binding->env, aws_napi_release_threadsafe_function(binding->transform_websocket, napi_tsfn_abort)); binding->transform_websocket = NULL; } if (binding->on_closed != NULL) { AWS_NAPI_ENSURE(binding->env, aws_napi_release_threadsafe_function(binding->on_closed, napi_tsfn_abort)); binding->on_closed = NULL; } if (binding->on_connection_success != NULL) { AWS_NAPI_ENSURE( binding->env, aws_napi_release_threadsafe_function(binding->on_connection_success, napi_tsfn_abort)); binding->on_connection_success = NULL; } } static void s_mqtt_client_connection_finalize(napi_env env, void *finalize_data, void *finalize_hint) { (void)finalize_hint; (void)env; struct mqtt_connection_binding *binding = finalize_data; AWS_LOGF_DEBUG(AWS_LS_NODEJS_CRT_GENERAL, "Destroying binding for connection %p", (void *)binding->connection); /* Should have already been done, but just to be safe -- now that it's reentrant -- release the functions anyways */ s_mqtt_client_connection_release_threadsafe_function(binding); s_mqtt_client_connection_release_threadsafe_function_on_failure(binding); if (binding->use_tls_options) { aws_tls_connection_options_clean_up(&binding->tls_options); } if (binding->connection) { aws_mqtt_client_connection_release(binding->connection); } aws_mem_release(binding->allocator, binding); } napi_value aws_napi_mqtt_client_connection_close(napi_env env, napi_callback_info info) { struct mqtt_connection_binding *binding = NULL; napi_value node_args[1]; size_t num_args = AWS_ARRAY_SIZE(node_args); napi_value *arg = &node_args[0]; AWS_NAPI_CALL(env, napi_get_cb_info(env, info, &num_args, node_args, NULL, NULL), { napi_throw_error(env, NULL, "Failed to retreive callback information"); return NULL; }); if (num_args != AWS_ARRAY_SIZE(node_args)) { napi_throw_error(env, NULL, "mqtt_client_connection_close needs exactly 1 argument"); return NULL; } napi_value node_binding = *arg++; AWS_NAPI_CALL(env, napi_get_value_external(env, node_binding, (void **)&binding), { napi_throw_error(env, NULL, "Failed to extract connection from first argument"); return NULL; }); if (binding->connection == NULL) { napi_throw_error(env, NULL, "Connection has already been closed and cannot be closed again"); return NULL; } /* connection has been shutdown, no callbacks will happen after it */ s_mqtt_client_connection_release_threadsafe_function(binding); s_mqtt_client_connection_release_threadsafe_function_on_failure(binding); /* no more node interop will be done, free node resources */ if (binding->node_external) { napi_delete_reference(env, binding->node_external); binding->node_external = NULL; } if (binding->connection) { aws_mqtt_client_connection_release(binding->connection); binding->connection = NULL; } return NULL; } /******************************************************************************* * on_connection_interrupted ******************************************************************************/ struct connection_interrupted_args { int error_code; }; static void s_on_connection_interrupted_call(napi_env env, napi_value on_interrupted, void *context, void *user_data) { struct mqtt_connection_binding *binding = context; struct connection_interrupted_args *args = user_data; if (env) { if (binding->on_connection_interrupted) { napi_value params[1]; const size_t num_params = AWS_ARRAY_SIZE(params); AWS_NAPI_ENSURE(env, napi_create_int32(env, args->error_code, ¶ms[0])); AWS_NAPI_ENSURE( env, aws_napi_dispatch_threadsafe_function( env, binding->on_connection_interrupted, NULL, on_interrupted, num_params, params)); } else { AWS_LOGF_INFO( AWS_LS_NODEJS_CRT_GENERAL, "Interrupt callback invoked for connection binding %p but callback is null", (void *)binding); } } aws_mem_release(binding->allocator, args); } static void s_on_connection_interrupted( struct aws_mqtt_client_connection *connection, int error_code, void *user_data) { (void)connection; struct mqtt_connection_binding *binding = user_data; if (!binding->on_connection_interrupted) { return; } struct connection_interrupted_args *args = aws_mem_calloc(binding->allocator, 1, sizeof(struct connection_interrupted_args)); AWS_FATAL_ASSERT(args); args->error_code = error_code; AWS_NAPI_ENSURE(NULL, aws_napi_queue_threadsafe_function(binding->on_connection_interrupted, args)); } /******************************************************************************* * on_connection_resumed ******************************************************************************/ struct connection_resumed_args { enum aws_mqtt_connect_return_code return_code; bool session_present; }; static void s_on_connection_resumed_call(napi_env env, napi_value on_resumed, void *context, void *user_data) { struct mqtt_connection_binding *binding = context; struct connection_resumed_args *args = user_data; if (env) { if (binding->on_connection_resumed) { napi_value params[2]; const size_t num_params = AWS_ARRAY_SIZE(params); AWS_NAPI_ENSURE(env, napi_create_int32(env, args->return_code, ¶ms[0])); AWS_NAPI_ENSURE(env, napi_get_boolean(env, args->session_present, ¶ms[1])); AWS_NAPI_ENSURE( env, aws_napi_dispatch_threadsafe_function( env, binding->on_connection_resumed, NULL, on_resumed, num_params, params)); } else { AWS_LOGF_INFO( AWS_LS_NODEJS_CRT_GENERAL, "Resume callback invoked for connection binding %p but callback is null", (void *)binding); } } aws_mem_release(binding->allocator, args); } static void s_on_connection_resumed( struct aws_mqtt_client_connection *connection, enum aws_mqtt_connect_return_code return_code, bool session_present, void *user_data) { (void)connection; struct mqtt_connection_binding *binding = user_data; if (!binding->on_connection_resumed) { return; } struct connection_resumed_args *args = aws_mem_calloc(binding->allocator, 1, sizeof(struct connection_resumed_args)); AWS_FATAL_ASSERT(args); args->return_code = return_code; args->session_present = session_present; AWS_NAPI_ENSURE(NULL, aws_napi_queue_threadsafe_function(binding->on_connection_resumed, args)); } /******************************************************************************* * on_connection_success ******************************************************************************/ struct connection_success_args { enum aws_mqtt_connect_return_code return_code; bool session_present; }; static void s_on_connection_success_call(napi_env env, napi_value on_success, void *context, void *user_data) { struct mqtt_connection_binding *binding = context; struct connection_success_args *args = user_data; if (env) { if (binding->on_connection_success) { napi_value params[2]; const size_t num_params = AWS_ARRAY_SIZE(params); AWS_NAPI_ENSURE(env, napi_create_int32(env, args->return_code, ¶ms[0])); AWS_NAPI_ENSURE(env, napi_get_boolean(env, args->session_present, ¶ms[1])); AWS_NAPI_ENSURE( env, aws_napi_dispatch_threadsafe_function( env, binding->on_connection_success, NULL, on_success, num_params, params)); } else { AWS_LOGF_INFO( AWS_LS_NODEJS_CRT_GENERAL, "Success callback invoked for connection binding %p but callback is null", (void *)binding); } } aws_mem_release(binding->allocator, args); } static void s_on_connection_success( struct aws_mqtt_client_connection *connection, enum aws_mqtt_connect_return_code return_code, bool session_present, void *user_data) { (void)connection; struct mqtt_connection_binding *binding = user_data; binding->first_successfull_connection = true; if (!binding->on_connection_success) { return; } struct connection_success_args *args = aws_mem_calloc(binding->allocator, 1, sizeof(struct connection_success_args)); AWS_FATAL_ASSERT(args); args->return_code = return_code; args->session_present = session_present; AWS_NAPI_ENSURE(NULL, aws_napi_queue_threadsafe_function(binding->on_connection_success, args)); } /******************************************************************************* * on_connection_failure ******************************************************************************/ struct connection_failure_args { int error_code; }; static void s_on_connection_failure_call(napi_env env, napi_value on_failure, void *context, void *user_data) { struct mqtt_connection_binding *binding = context; struct connection_failure_args *args = user_data; if (env) { if (binding->on_connection_failure) { napi_value params[1]; const size_t num_params = AWS_ARRAY_SIZE(params); AWS_NAPI_ENSURE(env, napi_create_int32(env, args->error_code, ¶ms[0])); AWS_NAPI_ENSURE( env, aws_napi_dispatch_threadsafe_function( env, binding->on_connection_failure, NULL, on_failure, num_params, params)); s_mqtt_client_connection_release_threadsafe_function_on_failure(binding); } else { AWS_LOGF_INFO( AWS_LS_NODEJS_CRT_GENERAL, "Failure callback invoked for connection binding %p but callback is null", (void *)binding); } } aws_mem_release(binding->allocator, args); } static void s_on_connection_failure(struct aws_mqtt_client_connection *connection, int error_code, void *user_data) { (void)connection; struct mqtt_connection_binding *binding = user_data; if (!binding->on_connection_failure) { return; } struct connection_failure_args *args = aws_mem_calloc(binding->allocator, 1, sizeof(struct connection_failure_args)); AWS_FATAL_ASSERT(args); args->error_code = error_code; AWS_NAPI_ENSURE(NULL, aws_napi_queue_threadsafe_function(binding->on_connection_failure, args)); } napi_value aws_napi_mqtt_client_connection_new(napi_env env, napi_callback_info cb_info) { struct aws_allocator *allocator = aws_napi_get_allocator(); napi_value node_args[14]; size_t num_args = AWS_ARRAY_SIZE(node_args); napi_value *arg = &node_args[0]; AWS_NAPI_CALL(env, napi_get_cb_info(env, cb_info, &num_args, node_args, NULL, NULL), { napi_throw_error(env, NULL, "Failed to retreive callback information"); return NULL; }); if (num_args != AWS_ARRAY_SIZE(node_args)) { napi_throw_error(env, NULL, "mqtt_client_connection_new received wrong number of arguments"); return NULL; } struct mqtt_connection_binding *binding = aws_mem_calloc(allocator, 1, sizeof(struct mqtt_connection_binding)); AWS_FATAL_ASSERT(binding); binding->env = env; binding->allocator = allocator; binding->first_successfull_connection = false; napi_value node_external; AWS_NAPI_CALL(env, napi_create_external(env, binding, s_mqtt_client_connection_finalize, NULL, &node_external), { napi_throw_error(env, NULL, "Failed create n-api external"); aws_mem_release(allocator, binding); return NULL; }); /* From hereon, we need to clean up if errors occur. * It's good practice to store long-lived values in the binding, and clean them up from the finalizer. * If this new() function fails partway through, the finalizer will still run and clean them up. */ napi_value result = NULL; /* Allocations that should not outlive this function */ struct aws_byte_buf will_topic; AWS_ZERO_STRUCT(will_topic); struct aws_byte_buf will_payload; AWS_ZERO_STRUCT(will_payload); struct aws_byte_buf username; AWS_ZERO_STRUCT(username); struct aws_byte_buf password; AWS_ZERO_STRUCT(password); napi_value node_client_external = *arg++; struct mqtt_nodejs_client *node_client; AWS_NAPI_CALL(env, napi_get_value_external(env, node_client_external, (void **)&node_client), { napi_throw_error(env, NULL, "Failed to extract client from external"); goto cleanup; }); napi_value node_on_interrupted = *arg++; if (!aws_napi_is_null_or_undefined(env, node_on_interrupted)) { AWS_NAPI_CALL( env, aws_napi_create_threadsafe_function( env, node_on_interrupted, "aws_mqtt_client_connection_on_connection_interrupted", s_on_connection_interrupted_call, binding, &binding->on_connection_interrupted), { goto cleanup; }); } napi_value node_on_resumed = *arg++; if (!aws_napi_is_null_or_undefined(env, node_on_resumed)) { AWS_NAPI_CALL( env, aws_napi_create_threadsafe_function( env, node_on_resumed, "aws_mqtt_client_connection_on_connection_resumed", s_on_connection_resumed_call, binding, &binding->on_connection_resumed), { goto cleanup; }); } napi_value node_on_success = *arg++; if (!aws_napi_is_null_or_undefined(env, node_on_success)) { AWS_NAPI_CALL( env, aws_napi_create_threadsafe_function( env, node_on_success, "aws_mqtt_client_connection_on_connection_success", s_on_connection_success_call, binding, &binding->on_connection_success), { goto cleanup; }); } napi_value node_on_failure = *arg++; if (!aws_napi_is_null_or_undefined(env, node_on_failure)) { AWS_NAPI_CALL( env, aws_napi_create_threadsafe_function( env, node_on_failure, "aws_mqtt_client_connection_on_connection_failure", s_on_connection_failure_call, binding, &binding->on_connection_failure), { goto cleanup; }); } /* CREATE THE THING */ binding->connection = aws_mqtt_client_connection_new(node_client->native_client); if (!binding->connection) { napi_throw_error(env, NULL, "Failed create native connection object"); goto cleanup; } if (binding->on_connection_interrupted || binding->on_connection_resumed) { aws_mqtt_client_connection_set_connection_interruption_handlers( binding->connection, s_on_connection_interrupted, binding, s_on_connection_resumed, binding); } if (binding->on_connection_failure || binding->on_connection_success) { if (aws_mqtt_client_connection_set_connection_result_handlers( binding->connection, s_on_connection_success, binding, s_on_connection_failure, binding) != AWS_OP_SUCCESS) { goto cleanup; } } napi_value node_tls = *arg++; if (!aws_napi_is_null_or_undefined(env, node_tls)) { struct aws_tls_ctx *tls_ctx; AWS_NAPI_CALL(env, napi_get_value_external(env, node_tls, (void **)&tls_ctx), { napi_throw_error(env, NULL, "Failed to extract tls_ctx from external"); goto cleanup; }); aws_tls_connection_options_init_from_ctx(&binding->tls_options, tls_ctx); binding->use_tls_options = true; } napi_value node_will = *arg++; if (!aws_napi_is_null_or_undefined(env, node_will)) { napi_value node_topic = NULL; AWS_NAPI_CALL(env, napi_get_named_property(env, node_will, "topic", &node_topic), { napi_throw_type_error(env, NULL, "will must contain a topic string"); goto cleanup; }); AWS_NAPI_CALL(env, aws_byte_buf_init_from_napi(&will_topic, env, node_topic), { aws_napi_throw_last_error(env); goto cleanup; }); napi_value node_payload; AWS_NAPI_CALL(env, napi_get_named_property(env, node_will, "payload", &node_payload), { napi_throw_type_error(env, NULL, "will must contain a payload DataView"); goto cleanup; }); AWS_NAPI_CALL(env, aws_byte_buf_init_from_napi(&will_payload, env, node_payload), { aws_napi_throw_last_error(env); goto cleanup; }); napi_value node_qos; AWS_NAPI_CALL(env, napi_get_named_property(env, node_will, "qos", &node_qos), { napi_throw_type_error(env, NULL, "will must contain a qos member"); goto cleanup; }); enum aws_mqtt_qos will_qos; AWS_NAPI_CALL(env, napi_get_value_int32(env, node_qos, (int32_t *)&will_qos), { napi_throw_type_error(env, NULL, "will.qos must be a number"); goto cleanup; }); napi_value node_retain; AWS_NAPI_CALL(env, napi_get_named_property(env, node_will, "retain", &node_retain), { napi_throw_type_error(env, NULL, "will must contain a retain member"); goto cleanup; }); bool will_retain; AWS_NAPI_CALL(env, napi_get_value_bool(env, node_retain, &will_retain), { napi_throw_type_error(env, NULL, "will.retain must be a boolean"); goto cleanup; }); struct aws_byte_cursor topic_cur = aws_byte_cursor_from_buf(&will_topic); struct aws_byte_cursor payload_cur = aws_byte_cursor_from_buf(&will_payload); if (aws_mqtt_client_connection_set_will(binding->connection, &topic_cur, will_qos, will_retain, &payload_cur)) { aws_napi_throw_last_error(env); goto cleanup; } } napi_value node_username = *arg++; if (!aws_napi_is_null_or_undefined(env, node_username)) { AWS_NAPI_CALL(env, aws_byte_buf_init_from_napi(&username, env, node_username), { napi_throw_type_error(env, NULL, "username must be a String"); goto cleanup; }); } napi_value node_password = *arg++; if (!aws_napi_is_null_or_undefined(env, node_password)) { AWS_NAPI_CALL(env, aws_byte_buf_init_from_napi(&password, env, node_password), { napi_throw_type_error(env, NULL, "password must be a String"); goto cleanup; }); } if (username.buffer || password.buffer) { struct aws_byte_cursor username_cur = aws_byte_cursor_from_buf(&username); struct aws_byte_cursor password_cur = aws_byte_cursor_from_buf(&password); if (aws_mqtt_client_connection_set_login(binding->connection, &username_cur, &password_cur)) { aws_napi_throw_last_error(env); goto cleanup; } } napi_value node_use_websocket = *arg++; bool use_websocket = false; if (!aws_napi_is_null_or_undefined(env, node_use_websocket)) { AWS_NAPI_CALL(env, napi_get_value_bool(env, node_use_websocket, &use_websocket), { napi_throw_type_error(env, NULL, "use_websocket must be a boolean"); goto cleanup; }); } napi_value node_proxy_options = *arg++; struct aws_http_proxy_options *proxy_options = NULL; if (!aws_napi_is_null_or_undefined(env, node_proxy_options)) { struct http_proxy_options_binding *proxy_binding = NULL; AWS_NAPI_CALL(env, napi_get_value_external(env, node_proxy_options, (void **)&proxy_binding), { napi_throw_type_error(env, NULL, "proxy_options must be an external"); goto cleanup; }); /* proxy_options are copied internally, no need to go nuts on copies */ proxy_options = aws_napi_get_http_proxy_options(proxy_binding); aws_mqtt_client_connection_set_http_proxy_options(binding->connection, proxy_options); } napi_value node_transform_websocket = *arg++; if (use_websocket) { if (!aws_napi_is_null_or_undefined(env, node_transform_websocket)) { AWS_NAPI_CALL( env, aws_napi_create_threadsafe_function( env, node_transform_websocket, "aws_mqtt_client_connection_transform_websocket", s_transform_websocket_call, binding, &binding->transform_websocket), { napi_throw_error(env, NULL, "Failed to bind transform_websocket callback"); goto cleanup; }); aws_mqtt_client_connection_use_websockets(binding->connection, s_transform_websocket, binding, NULL, NULL); } else { aws_mqtt_client_connection_use_websockets(binding->connection, NULL, NULL, NULL, NULL); } } /* set reconnect min/max times. beware that user that might have passed only one of these values */ napi_value node_reconnect_min_sec = *arg++; napi_value node_reconnect_max_sec = *arg++; if (!aws_napi_is_null_or_undefined(env, node_reconnect_min_sec) && !aws_napi_is_null_or_undefined(env, node_reconnect_max_sec)) { int64_t reconnect_min_sec = -1; int64_t reconnect_max_sec = -1; if (!aws_napi_is_null_or_undefined(env, node_reconnect_min_sec)) { AWS_NAPI_CALL(env, napi_get_value_int64(env, node_reconnect_min_sec, &reconnect_min_sec), { napi_throw_type_error(env, NULL, "reconnect_min_sec must be a Number"); goto cleanup; }); if (reconnect_min_sec < 0) { napi_throw_range_error(env, NULL, "reconnect_min_sec cannot be negative"); goto cleanup; } } if (!aws_napi_is_null_or_undefined(env, node_reconnect_max_sec)) { AWS_NAPI_CALL(env, napi_get_value_int64(env, node_reconnect_max_sec, &reconnect_max_sec), { napi_throw_type_error(env, NULL, "reconnect_max_sec must be a Number"); goto cleanup; }); if (reconnect_max_sec < 0) { napi_throw_range_error(env, NULL, "reconnect_max_sec cannot be negative"); goto cleanup; } } if (aws_mqtt_client_connection_set_reconnect_timeout( binding->connection, (uint64_t)reconnect_min_sec, (uint64_t)reconnect_max_sec)) { napi_throw_error(env, NULL, "failed to set reconnect min/max timeout"); goto cleanup; } } else { napi_throw_error(env, NULL, "reconnect min/max timeout is missing."); goto cleanup; } /* napi_create_reference() must be the last thing called by this function. * Once this succeeds, the external will not be cleaned up automatically */ AWS_NAPI_CALL(env, napi_create_reference(env, node_external, 1, &binding->node_external), { napi_throw_error(env, NULL, "Failed to reference node external"); goto cleanup; }); result = node_external; cleanup: aws_byte_buf_clean_up(&will_topic); aws_byte_buf_clean_up(&will_payload); aws_byte_buf_clean_up(&username); aws_byte_buf_clean_up(&password); return result; } /******************************************************************************* * Connect ******************************************************************************/ struct connect_args { struct aws_allocator *allocator; struct mqtt_connection_binding *binding; enum aws_mqtt_connect_return_code return_code; int error_code; bool session_present; napi_threadsafe_function on_connect; }; static void s_destroy_connect_args(struct connect_args *args) { if (args == NULL) { return; } AWS_FATAL_ASSERT(args->allocator != NULL); if (args->on_connect != 0) { AWS_FATAL_ASSERT(args->binding != NULL); AWS_NAPI_ENSURE(args->binding->env, aws_napi_release_threadsafe_function(args->on_connect, napi_tsfn_abort)); } aws_mem_release(args->allocator, args); } static void s_on_connect_call(napi_env env, napi_value on_connect, void *context, void *user_data) { struct mqtt_connection_binding *binding = context; struct connect_args *args = user_data; if (env) { napi_value params[3]; const size_t num_params = AWS_ARRAY_SIZE(params); AWS_NAPI_ENSURE(env, napi_create_int32(env, args->error_code, ¶ms[0])); AWS_NAPI_ENSURE(env, napi_create_int32(env, args->return_code, ¶ms[1])); AWS_NAPI_ENSURE(env, napi_get_boolean(env, args->session_present, ¶ms[2])); AWS_NAPI_ENSURE( env, aws_napi_dispatch_threadsafe_function(env, args->on_connect, NULL, on_connect, num_params, params)); } /* * This is terrible and we need to make backwards incompatible changes to the API to correct it, but for now * clean up the connection function bindings if the initial connect failed because that's the contract * that we fulfilled in the past. If we don't clean these up here then current programs written against * the current contract will leak their resumed/interrupted/websocket callbacks. */ if (args->return_code || args->error_code) { /* Failed to create a connection, none of the callbacks will be invoked again */ s_mqtt_client_connection_release_threadsafe_function(binding); if (binding->first_successfull_connection == true) { /* separating the cleanup to on_connection_failure, to make it * possible for the callback to be sent later and not free the * callback prematurely. It will be freed when we send the callback * itself later on. if it is not the first successfull connection, * then re-connect will handle this scenario */ s_mqtt_client_connection_release_threadsafe_function_on_failure(binding); } } s_destroy_connect_args(args); } static void s_on_connected( struct aws_mqtt_client_connection *connection, int error_code, enum aws_mqtt_connect_return_code return_code, bool session_present, void *user_data) { (void)connection; struct connect_args *args = user_data; if (!args->on_connect) { s_destroy_connect_args(args); return; } args->error_code = error_code; args->return_code = return_code; args->session_present = session_present; AWS_NAPI_ENSURE(NULL, aws_napi_queue_threadsafe_function(args->on_connect, args)); } struct transform_websocket_args { struct mqtt_connection_binding *binding; struct aws_http_message *request; aws_mqtt_transform_websocket_handshake_complete_fn *complete_fn; void *complete_ctx; }; static napi_value s_napi_transform_websocket_complete(napi_env env, napi_callback_info cb_info) { struct transform_websocket_args *args = NULL; int error_code = AWS_ERROR_SUCCESS; napi_value node_args[1]; size_t num_args = AWS_ARRAY_SIZE(node_args); napi_value *arg = &node_args[0]; AWS_NAPI_CALL(env, napi_get_cb_info(env, cb_info, &num_args, node_args, NULL, (void **)&args), { napi_throw_error(env, NULL, "Failed to retreive callback information"); goto cleanup; }); if (num_args > 1) { napi_throw_error(env, NULL, "transform_websocket_complete needs exactly 0 or 1 arguments"); goto cleanup; } napi_value node_error_code = *arg++; /* If the user didn't provide an error_code, the napi_value will be undefined, so we can ignore it */ if (!aws_napi_is_null_or_undefined(env, node_error_code)) { AWS_NAPI_CALL(env, napi_get_value_int32(env, node_error_code, &error_code), { napi_throw_type_error(env, NULL, "error_code must be a number or undefined"); goto cleanup; }); } args->complete_fn(args->request, error_code, args->complete_ctx); cleanup: if (args != NULL) { aws_mem_release(args->binding->allocator, args); } return NULL; } static void s_transform_websocket_call(napi_env env, napi_value transform_websocket, void *context, void *user_data) { // struct mqtt_connection_binding *binding = context; (void)context; struct transform_websocket_args *args = user_data; if (env) { napi_value params[2]; const size_t num_params = AWS_ARRAY_SIZE(params); AWS_NAPI_ENSURE(env, aws_napi_http_message_wrap(env, args->request, ¶ms[0])); AWS_NAPI_ENSURE( env, napi_create_function( env, "transform_websocket_complete", NAPI_AUTO_LENGTH, &s_napi_transform_websocket_complete, args, ¶ms[1])); AWS_NAPI_ENSURE( env, aws_napi_dispatch_threadsafe_function( env, args->binding->transform_websocket, NULL, transform_websocket, num_params, params)); } else { args->complete_fn(args->request, AWS_CRT_NODEJS_ERROR_THREADSAFE_FUNCTION_NULL_NAPI_ENV, args->complete_ctx); aws_mem_release(args->binding->allocator, args); } } void s_transform_websocket( struct aws_http_message *request, void *user_data, aws_mqtt_transform_websocket_handshake_complete_fn *complete_fn, void *complete_ctx) { struct mqtt_connection_binding *binding = user_data; struct transform_websocket_args *args = aws_mem_calloc(binding->allocator, 1, sizeof(struct transform_websocket_args)); AWS_FATAL_ASSERT(args); args->binding = binding; args->request = request; args->complete_fn = complete_fn; args->complete_ctx = complete_ctx; AWS_NAPI_ENSURE(NULL, aws_napi_queue_threadsafe_function(binding->transform_websocket, args)); } napi_value aws_napi_mqtt_client_connection_connect(napi_env env, napi_callback_info cb_info) { bool success = false; struct aws_socket_options *socket_options = NULL; struct mqtt_connection_binding *binding = NULL; struct aws_byte_buf client_id; AWS_ZERO_STRUCT(client_id); struct aws_byte_buf server_name; AWS_ZERO_STRUCT(server_name); struct connect_args *on_connect_args = NULL; napi_value node_args[10]; size_t num_args = AWS_ARRAY_SIZE(node_args); napi_value *arg = &node_args[0]; AWS_NAPI_CALL(env, napi_get_cb_info(env, cb_info, &num_args, node_args, NULL, NULL), { napi_throw_error(env, NULL, "Failed to retrieve callback information"); goto cleanup; }); if (num_args != AWS_ARRAY_SIZE(node_args)) { napi_throw_error(env, NULL, "mqtt_client_connection_connect received wrong number of arguments"); goto cleanup; } napi_value node_binding = *arg++; AWS_NAPI_CALL(env, napi_get_value_external(env, node_binding, (void **)&binding), { napi_throw_error(env, NULL, "Failed to extract connection from first argument"); goto cleanup; }); if (binding->connection == NULL) { napi_throw_error(env, NULL, "Connection has been closed and can no longer be used"); goto cleanup; } struct aws_allocator *allocator = binding->allocator; napi_value node_client_id = *arg++; AWS_NAPI_CALL(env, aws_byte_buf_init_from_napi(&client_id, env, node_client_id), { napi_throw_type_error(env, NULL, "client_id must be a String"); goto cleanup; }); napi_value node_server_name = *arg++; AWS_NAPI_CALL(env, aws_byte_buf_init_from_napi(&server_name, env, node_server_name), { napi_throw_type_error(env, NULL, "server_name must be a String"); goto cleanup; }); napi_value node_port = *arg++; uint32_t port_number = 0; AWS_NAPI_CALL(env, napi_get_value_uint32(env, node_port, &port_number), { napi_throw_type_error(env, NULL, "port must be a Number"); goto cleanup; }); napi_value node_socket_options = *arg++; if (!aws_napi_is_null_or_undefined(env, node_socket_options)) { AWS_NAPI_CALL(env, napi_get_value_external(env, node_socket_options, (void **)&socket_options), { napi_throw_type_error(env, NULL, "connect_timeout must be a Number"); goto cleanup; }); } napi_value node_keep_alive_time = *arg++; uint32_t keep_alive_time = 0; if (!aws_napi_is_null_or_undefined(env, node_keep_alive_time)) { AWS_NAPI_CALL(env, napi_get_value_uint32(env, node_keep_alive_time, &keep_alive_time), { napi_throw_type_error(env, NULL, "keep_alive must be a Number"); goto cleanup; }); } napi_value node_ping_timeout = *arg++; uint32_t ping_timeout = 0; if (!aws_napi_is_null_or_undefined(env, node_ping_timeout)) { AWS_NAPI_CALL(env, napi_get_value_uint32(env, node_ping_timeout, &ping_timeout), { napi_throw_type_error(env, NULL, "ping_timeout must be a Number"); goto cleanup; }); } napi_value node_protocol_operation_timeout = *arg++; uint32_t protocol_operation_timeout = 0; if (!aws_napi_is_null_or_undefined(env, node_protocol_operation_timeout)) { AWS_NAPI_CALL(env, napi_get_value_uint32(env, node_protocol_operation_timeout, &protocol_operation_timeout), { napi_throw_type_error(env, NULL, "protocol_operation_timeout must be a Number"); goto cleanup; }); } napi_value node_clean_session = *arg++; bool clean_session = false; if (!aws_napi_is_null_or_undefined(env, node_clean_session)) { AWS_NAPI_CALL(env, napi_get_value_bool(env, node_clean_session, &clean_session), { napi_throw_type_error(env, NULL, "clean_session must be a boolean"); goto cleanup; }); } napi_value node_on_connect = *arg++; if (!aws_napi_is_null_or_undefined(env, node_on_connect)) { on_connect_args = aws_mem_calloc(allocator, 1, sizeof(struct connect_args)); AWS_FATAL_ASSERT(on_connect_args); on_connect_args->allocator = allocator; on_connect_args->binding = binding; AWS_NAPI_CALL( env, aws_napi_create_threadsafe_function( env, node_on_connect, "aws_mqtt_client_connection_on_connect", s_on_connect_call, binding, &on_connect_args->on_connect), { napi_throw_error(env, NULL, "Failed to bind on_connect callback"); goto cleanup; }); } struct aws_byte_cursor client_id_cur = aws_byte_cursor_from_buf(&client_id); struct aws_byte_cursor server_name_cur = aws_byte_cursor_from_buf(&server_name); struct aws_mqtt_connection_options options; options.clean_session = clean_session; options.client_id = client_id_cur; options.host_name = server_name_cur; options.keep_alive_time_secs = (uint16_t)keep_alive_time; options.on_connection_complete = s_on_connected; options.ping_timeout_ms = ping_timeout; options.protocol_operation_timeout_ms = protocol_operation_timeout; options.port = port_number; options.socket_options = socket_options; options.tls_options = binding->use_tls_options ? &binding->tls_options : NULL; options.user_data = on_connect_args; /* on_connect user_data */ if (aws_mqtt_client_connection_connect(binding->connection, &options)) { aws_napi_throw_last_error(env); goto cleanup; } success = true; cleanup: aws_byte_buf_clean_up(&client_id); aws_byte_buf_clean_up(&server_name); if (!success && on_connect_args) { s_destroy_connect_args(on_connect_args); } return NULL; } /******************************************************************************* * Reconnect Deprecated ******************************************************************************/ napi_value aws_napi_mqtt_client_connection_reconnect(napi_env env, napi_callback_info cb_info) { struct mqtt_connection_binding *binding = NULL; napi_value node_args[2]; size_t num_args = AWS_ARRAY_SIZE(node_args); napi_value *arg = &node_args[0]; AWS_NAPI_CALL(env, napi_get_cb_info(env, cb_info, &num_args, node_args, NULL, NULL), { napi_throw_error(env, NULL, "Failed to retreive callback information"); return NULL; }); if (num_args != AWS_ARRAY_SIZE(node_args)) { napi_throw_error(env, NULL, "mqtt_client_connection_reconnect needs exactly 2 arguments"); return NULL; } napi_value node_binding = *arg++; AWS_NAPI_CALL(env, napi_get_value_external(env, node_binding, (void **)&binding), { napi_throw_error(env, NULL, "Failed to extract binding from external"); return NULL; }); if (binding->connection == NULL) { napi_throw_error(env, NULL, "Connection has been closed and can no longer be used"); return NULL; } struct connect_args *args = aws_mem_calloc(binding->allocator, 1, sizeof(struct connect_args)); AWS_FATAL_ASSERT(args); args->allocator = binding->allocator; args->binding = binding; napi_value node_on_connect = *arg++; if (!aws_napi_is_null_or_undefined(env, node_on_connect)) { AWS_NAPI_CALL( env, aws_napi_create_threadsafe_function( env, node_on_connect, "aws_mqtt_client_connection_on_reconnect", s_on_connect_call, binding, &args->on_connect), { goto on_error; }); } if (aws_mqtt_client_connection_reconnect(binding->connection, s_on_connected, binding)) { aws_napi_throw_last_error(env); goto on_error; } return NULL; on_error: s_destroy_connect_args(args); return NULL; } /******************************************************************************* * Publish ******************************************************************************/ struct puback_args { struct aws_allocator *allocator; struct mqtt_connection_binding *binding; uint16_t packet_id; int error_code; napi_threadsafe_function on_puback; }; static void s_destroy_puback_args(struct puback_args *args) { if (args == NULL) { return; } AWS_FATAL_ASSERT(args->allocator != NULL); if (args->on_puback != 0) { AWS_FATAL_ASSERT(args->binding != NULL); AWS_NAPI_ENSURE(args->binding->env, aws_napi_release_threadsafe_function(args->on_puback, napi_tsfn_abort)); } aws_mem_release(args->allocator, args); } static void s_on_publish_complete_call(napi_env env, napi_value on_puback, void *context, void *user_data) { (void)context; struct puback_args *args = user_data; if (env) { napi_value params[2]; const size_t num_params = AWS_ARRAY_SIZE(params); AWS_NAPI_ENSURE(env, napi_create_uint32(env, args->packet_id, ¶ms[0])); AWS_NAPI_ENSURE(env, napi_create_int32(env, args->error_code, ¶ms[1])); AWS_NAPI_ENSURE( env, aws_napi_dispatch_threadsafe_function(env, args->on_puback, NULL, on_puback, num_params, params)); } s_destroy_puback_args(args); } static void s_on_publish_complete( struct aws_mqtt_client_connection *connection, uint16_t packet_id, int error_code, void *user_data) { (void)connection; struct puback_args *args = user_data; args->packet_id = packet_id; args->error_code = error_code; AWS_NAPI_ENSURE(NULL, aws_napi_queue_threadsafe_function(args->on_puback, args)); } napi_value aws_napi_mqtt_client_connection_publish(napi_env env, napi_callback_info info) { struct aws_allocator *allocator = aws_napi_get_allocator(); struct puback_args *args = aws_mem_calloc(allocator, 1, sizeof(struct puback_args)); AWS_FATAL_ASSERT(args); args->allocator = allocator; struct aws_byte_buf topic_buf; struct aws_byte_buf payload_buf; AWS_ZERO_STRUCT(topic_buf); AWS_ZERO_STRUCT(payload_buf); napi_value node_args[6]; size_t num_args = AWS_ARRAY_SIZE(node_args); napi_value *arg = &node_args[0]; AWS_NAPI_CALL(env, napi_get_cb_info(env, info, &num_args, node_args, NULL, NULL), { napi_throw_error(env, NULL, "Failed to retreive callback information"); goto cleanup; }); if (num_args != AWS_ARRAY_SIZE(node_args)) { napi_throw_error(env, NULL, "mqtt_client_connection_publish needs exactly 6 arguments"); goto cleanup; } napi_value node_binding = *arg++; struct mqtt_connection_binding *binding = NULL; AWS_NAPI_CALL(env, napi_get_value_external(env, node_binding, (void **)&binding), { napi_throw_error(env, NULL, "Failed to extract binding from external"); goto cleanup; }); args->binding = binding; if (binding->connection == NULL) { napi_throw_error(env, NULL, "Connection has been closed and can no longer be used"); goto cleanup; } napi_value node_topic = *arg++; AWS_NAPI_CALL(env, aws_byte_buf_init_from_napi(&topic_buf, env, node_topic), { napi_throw_type_error(env, NULL, "topic must be a String"); goto cleanup; }); napi_value node_payload = *arg++; AWS_NAPI_CALL(env, aws_byte_buf_init_from_napi(&payload_buf, env, node_payload), { napi_throw_type_error(env, NULL, "payload is invalid type"); goto cleanup; }); napi_value node_qos = *arg++; uint32_t qos_uint = 0; AWS_NAPI_CALL(env, napi_get_value_uint32(env, node_qos, &qos_uint), { napi_throw_type_error(env, NULL, "qos must be a number"); goto cleanup; }); enum aws_mqtt_qos qos = (enum aws_mqtt_qos)qos_uint; napi_value node_retain = *arg++; bool retain = false; AWS_NAPI_CALL(env, napi_get_value_bool(env, node_retain, &retain), { napi_throw_type_error(env, NULL, "retain must be a bool"); goto cleanup; }); napi_value node_on_puback = *arg++; if (!aws_napi_is_null_or_undefined(env, node_on_puback)) { AWS_NAPI_CALL( env, aws_napi_create_threadsafe_function( env, node_on_puback, "aws_mqtt_client_connection_on_puback", s_on_publish_complete_call, binding, &args->on_puback), { goto cleanup; }); } const struct aws_byte_cursor topic_cur = aws_byte_cursor_from_buf(&topic_buf); const struct aws_byte_cursor payload_cur = aws_byte_cursor_from_buf(&payload_buf); uint16_t pub_id = aws_mqtt_client_connection_publish( binding->connection, &topic_cur, qos, retain, &payload_cur, s_on_publish_complete, args); if (!pub_id) { aws_napi_throw_last_error(env); goto cleanup; } aws_byte_buf_clean_up(&payload_buf); aws_byte_buf_clean_up(&topic_buf); return NULL; cleanup: aws_byte_buf_clean_up(&payload_buf); aws_byte_buf_clean_up(&topic_buf); s_destroy_puback_args(args); return NULL; } /******************************************************************************* * Subscribe ******************************************************************************/ struct suback_args { struct aws_allocator *allocator; struct mqtt_connection_binding *binding; uint16_t packet_id; enum aws_mqtt_qos qos; int error_code; struct aws_byte_buf topic; /* not confident that we can guarantee a cursor ref will always be valid */ napi_threadsafe_function on_suback; }; static void s_destroy_suback_args(struct suback_args *args) { if (args == NULL) { return; } AWS_FATAL_ASSERT(args->allocator != NULL); aws_byte_buf_clean_up(&args->topic); if (args->on_suback != 0) { AWS_FATAL_ASSERT(args->binding != NULL); AWS_NAPI_ENSURE(args->binding->env, aws_napi_release_threadsafe_function(args->on_suback, napi_tsfn_abort)); } aws_mem_release(args->allocator, args); } static void s_on_suback_call(napi_env env, napi_value on_suback, void *context, void *user_data) { (void)context; struct suback_args *args = user_data; if (env) { napi_value params[4]; const size_t num_params = AWS_ARRAY_SIZE(params); AWS_NAPI_ENSURE(env, napi_create_int32(env, args->packet_id, ¶ms[0])); AWS_NAPI_ENSURE( env, napi_create_string_utf8(env, (const char *)args->topic.buffer, args->topic.len, ¶ms[1])); AWS_NAPI_ENSURE(env, napi_create_int32(env, args->qos, ¶ms[2])); AWS_NAPI_ENSURE(env, napi_create_int32(env, args->error_code, ¶ms[3])); AWS_NAPI_ENSURE( env, aws_napi_dispatch_threadsafe_function(env, args->on_suback, NULL, on_suback, num_params, params)); } s_destroy_suback_args(args); } static void s_on_suback( struct aws_mqtt_client_connection *connection, uint16_t packet_id, const struct aws_byte_cursor *topic, enum aws_mqtt_qos qos, int error_code, void *user_data) { (void)connection; (void)topic; struct suback_args *args = user_data; if (args == NULL) { return; } if (!args->on_suback) { s_destroy_suback_args(args); return; } args->error_code = error_code; args->qos = qos; args->packet_id = packet_id; AWS_NAPI_ENSURE(args->binding->env, aws_napi_queue_threadsafe_function(args->on_suback, args)); } /* user data which describes a subscription, passed to aws_mqtt_connection_subscribe */ struct subscription { struct aws_allocator *allocator; struct aws_byte_buf topic; /* stored here as long as the sub is active, referenced by callbacks */ napi_threadsafe_function on_publish; }; static void s_destroy_subscription(struct subscription *sub) { if (sub == NULL) { return; } AWS_FATAL_ASSERT(sub->allocator != NULL); if (sub->on_publish != 0) { AWS_NAPI_ENSURE(NULL, aws_napi_release_threadsafe_function(sub->on_publish, napi_tsfn_release)); } aws_byte_buf_clean_up(&sub->topic); aws_mem_release(sub->allocator, sub); } static void s_on_publish_user_data_clean_up(void *user_data) { s_destroy_subscription(user_data); } /* arguments for publish callbacks */ struct on_publish_args { struct aws_allocator *allocator; struct aws_byte_buf topic; /* owned by this */ struct aws_byte_buf *payload; /* owned by this until the external array buffer in the direct callback is created */ bool dup; enum aws_mqtt_qos qos; bool retain; /* created by subscription, but we add/dec ref on our copy of the pointer too */ napi_threadsafe_function on_publish; }; static void s_destroy_on_publish_args(struct on_publish_args *args) { if (args == NULL) { return; } AWS_FATAL_ASSERT(args->allocator != NULL); if (args->on_publish != NULL) { AWS_NAPI_ENSURE(NULL, aws_napi_release_threadsafe_function(args->on_publish, napi_tsfn_release)); } if (args->payload != NULL) { aws_byte_buf_clean_up(args->payload); aws_mem_release(args->allocator, args->payload); } aws_byte_buf_clean_up(&args->topic); aws_mem_release(args->allocator, args); } static void s_publish_external_arraybuffer_finalizer(napi_env env, void *finalize_data, void *finalize_hint) { (void)env; (void)finalize_data; struct aws_byte_buf *buf = finalize_hint; struct aws_allocator *allocator = buf->allocator; AWS_FATAL_ASSERT(allocator != NULL); aws_byte_buf_clean_up(buf); aws_mem_release(allocator, buf); } static void s_on_publish_call(napi_env env, napi_value on_publish, void *context, void *user_data) { (void)context; struct on_publish_args *args = user_data; if (env) { napi_value params[5]; const size_t num_params = AWS_ARRAY_SIZE(params); AWS_NAPI_ENSURE( env, napi_create_string_utf8(env, (const char *)args->topic.buffer, args->topic.len, ¶ms[0])); AWS_NAPI_ENSURE( env, aws_napi_create_external_arraybuffer( env, args->payload->buffer, args->payload->len, s_publish_external_arraybuffer_finalizer, args->payload, ¶ms[1])); AWS_NAPI_ENSURE(env, napi_get_boolean(env, args->dup, ¶ms[2])); AWS_NAPI_ENSURE(env, napi_create_int32(env, args->qos, ¶ms[3])); AWS_NAPI_ENSURE(env, napi_get_boolean(env, args->retain, ¶ms[4])); /* * We've successfully created the external array buffer whose finalizer will clean this byte_buf up. * It's now safe and correct to set this to NULL in the args so that the args destructor does not clean it up. */ args->payload = NULL; AWS_NAPI_ENSURE( env, aws_napi_dispatch_threadsafe_function(env, args->on_publish, NULL, on_publish, num_params, params)); } s_destroy_on_publish_args(args); } /* called in response to a message being published to an active subscription */ static void s_on_publish( struct aws_mqtt_client_connection *connection, const struct aws_byte_cursor *topic, const struct aws_byte_cursor *payload, bool dup, enum aws_mqtt_qos qos, bool retain, void *user_data) { (void)connection; struct subscription *sub = user_data; /* users can use a null handler to sub to a topic, and then handle it with the any handler */ if (!sub->on_publish) { return; } struct mqtt_connection_binding *binding = NULL; AWS_NAPI_ENSURE(NULL, napi_get_threadsafe_function_context(sub->on_publish, (void **)&binding)); struct aws_allocator *allocator = binding->allocator; struct on_publish_args *args = aws_mem_calloc(allocator, 1, sizeof(struct on_publish_args)); AWS_FATAL_ASSERT(args); args->allocator = allocator; args->dup = dup; args->qos = qos; args->retain = retain; args->on_publish = sub->on_publish; /* * We share this threadsafe function with the subscription structure. Each applies a inc/dec ref because * it isn't clear that we can guarantee the order of destruction because we don't really have any * guarantees about V8's internal scheduling/ordering invariants for queued functions. */ if (args->on_publish != 0) { AWS_NAPI_ENSURE(NULL, aws_napi_acquire_threadsafe_function(args->on_publish)); } if (aws_byte_buf_init_copy_from_cursor(&args->topic, allocator, *topic)) { AWS_LOGF_ERROR(AWS_LS_NODEJS_CRT_GENERAL, "Failed to copy MQTT topic, message will not be delivered"); goto on_error; } /* * Create the payload as a pointer-to-buf so cleanup responsibilities can be transferred to the payload's * finalizer. */ args->payload = aws_mem_calloc(allocator, 1, sizeof(struct aws_byte_buf)); if (args->payload == NULL) { goto on_error; } /* this is freed after being delivered to node in s_on_publish_call */ if (aws_byte_buf_init_copy_from_cursor(args->payload, allocator, *payload)) { AWS_LOGF_ERROR(AWS_LS_NODEJS_CRT_GENERAL, "Failed to copy MQTT payload buffer, message will not be delivered"); goto on_error; } AWS_NAPI_ENSURE(NULL, aws_napi_queue_threadsafe_function(args->on_publish, args)); return; on_error: s_destroy_on_publish_args(args); } napi_value aws_napi_mqtt_client_connection_subscribe(napi_env env, napi_callback_info cb_info) { napi_value node_args[5]; size_t num_args = AWS_ARRAY_SIZE(node_args); napi_value *arg = &node_args[0]; AWS_NAPI_CALL(env, napi_get_cb_info(env, cb_info, &num_args, node_args, NULL, NULL), { napi_throw_error(env, NULL, "Failed to retreive callback information"); return NULL; }); if (num_args != AWS_ARRAY_SIZE(node_args)) { napi_throw_error(env, NULL, "mqtt_client_connection_subscribe needs exactly 5 arguments"); return NULL; } napi_value node_binding = *arg++; struct mqtt_connection_binding *binding = NULL; AWS_NAPI_CALL(env, napi_get_value_external(env, node_binding, (void **)&binding), { napi_throw_error(env, NULL, "Failed to extract binding from external"); return NULL; }); struct aws_allocator *allocator = binding->allocator; struct suback_args *suback = NULL; struct subscription *sub = aws_mem_calloc(allocator, 1, sizeof(struct subscription)); AWS_FATAL_ASSERT(sub); sub->allocator = allocator; if (binding->connection == NULL) { napi_throw_error(env, NULL, "Connection has been closed and can no longer be used"); goto cleanup; } napi_value node_topic = *arg++; AWS_NAPI_CALL(env, aws_byte_buf_init_from_napi(&sub->topic, env, node_topic), { napi_throw_type_error(env, NULL, "topic must be a String"); goto cleanup; }); napi_value node_qos = *arg++; uint32_t qos_uint = 0; AWS_NAPI_CALL(env, napi_get_value_uint32(env, node_qos, &qos_uint), { napi_throw_type_error(env, NULL, "qos must be a number"); goto cleanup; }); enum aws_mqtt_qos qos = (enum aws_mqtt_qos)qos_uint; napi_value node_on_publish = *arg++; if (!aws_napi_is_null_or_undefined(env, node_on_publish)) { AWS_NAPI_CALL( env, aws_napi_create_threadsafe_function( env, node_on_publish, "aws_mqtt_client_connection_on_publish", s_on_publish_call, binding, &sub->on_publish), { goto cleanup; }); } napi_value node_on_suback = *arg++; if (!aws_napi_is_null_or_undefined(env, node_on_suback)) { suback = aws_mem_calloc(allocator, 1, sizeof(struct suback_args)); AWS_FATAL_ASSERT(suback); suback->allocator = allocator; suback->binding = binding; aws_byte_buf_init_copy_from_cursor(&suback->topic, allocator, aws_byte_cursor_from_buf(&sub->topic)); AWS_NAPI_CALL( env, aws_napi_create_threadsafe_function( env, node_on_suback, "aws_mqtt_client_connection_on_suback", s_on_suback_call, binding, &suback->on_suback), { goto cleanup; }); } struct aws_byte_cursor topic_cur = aws_byte_cursor_from_buf(&sub->topic); uint16_t sub_id = aws_mqtt_client_connection_subscribe( binding->connection, &topic_cur, qos, s_on_publish, sub, s_on_publish_user_data_clean_up, s_on_suback, suback); if (!sub_id) { aws_napi_throw_last_error(env); goto cleanup; } return NULL; cleanup: s_destroy_subscription(sub); s_destroy_suback_args(suback); return NULL; } /* * on-any publish */ struct on_any_publish_args { struct aws_allocator *allocator; struct aws_string *topic; struct aws_byte_buf *payload; bool dup; enum aws_mqtt_qos qos; bool retain; }; static void s_destroy_on_any_publish_args(struct on_any_publish_args *args) { if (args == NULL) { return; } aws_string_destroy(args->topic); /* * Between payload construction and transfer to the external array the only failure options are fatal, but * as a pattern I believe it to be better (future-proofing, consistency) to properly handle the possibility * of the payload getting created but not transferred to the external array's finalizer */ if (args->payload) { aws_byte_buf_clean_up(args->payload); aws_mem_release(args->allocator, args->payload); } aws_mem_release(args->allocator, args); } static void s_any_publish_external_arraybuffer_finalizer(napi_env env, void *finalize_data, void *finalize_hint) { (void)env; (void)finalize_data; struct aws_byte_buf *payload = finalize_hint; struct aws_allocator *allocator = payload->allocator; AWS_FATAL_ASSERT(allocator != NULL); aws_byte_buf_clean_up(payload); aws_mem_release(allocator, payload); } static void s_on_any_publish_call(napi_env env, napi_value on_publish, void *context, void *user_data) { struct mqtt_connection_binding *binding = context; struct on_any_publish_args *args = user_data; if (env) { if (binding->on_any_publish) { napi_value params[5]; const size_t num_params = AWS_ARRAY_SIZE(params); AWS_NAPI_ENSURE( env, napi_create_string_utf8(env, aws_string_c_str(args->topic), args->topic->len, ¶ms[0])); AWS_NAPI_ENSURE( env, aws_napi_create_external_arraybuffer( env, args->payload->buffer, args->payload->len, s_any_publish_external_arraybuffer_finalizer, args->payload, ¶ms[1])); AWS_NAPI_ENSURE(env, napi_get_boolean(env, args->dup, ¶ms[2])); AWS_NAPI_ENSURE(env, napi_create_int32(env, args->qos, ¶ms[3])); AWS_NAPI_ENSURE(env, napi_get_boolean(env, args->retain, ¶ms[4])); /* * We've successfully created the external array buffer whose finalizer will clean this byte_buf up. * It's now safe and correct to set this to NULL in the args so that the args destructor does not clean it * up. */ args->payload = NULL; AWS_NAPI_ENSURE( env, aws_napi_dispatch_threadsafe_function( env, binding->on_any_publish, NULL, on_publish, num_params, params)); } else { AWS_LOGF_INFO( AWS_LS_NODEJS_CRT_GENERAL, "Any publish callback invoked for connection binding %p but callback is null", (void *)binding); } } s_destroy_on_any_publish_args(args); } static void s_on_any_publish( struct aws_mqtt_client_connection *connection, const struct aws_byte_cursor *topic, const struct aws_byte_cursor *payload, bool dup, enum aws_mqtt_qos qos, bool retain, void *user_data) { (void)connection; struct mqtt_connection_binding *binding = user_data; if (binding->on_any_publish == NULL) { return; } struct aws_allocator *allocator = binding->allocator; struct on_any_publish_args *args = aws_mem_calloc(allocator, 1, sizeof(struct on_publish_args)); AWS_FATAL_ASSERT(args); args->allocator = allocator; args->topic = aws_string_new_from_array(allocator, topic->ptr, topic->len); args->dup = dup; args->qos = qos; args->retain = retain; args->payload = aws_mem_calloc(allocator, 1, sizeof(struct aws_byte_buf)); /* this is freed after being delivered to node in s_on_any_publish_call */ if (aws_byte_buf_init_copy_from_cursor(args->payload, allocator, *payload)) { AWS_LOGF_ERROR(AWS_LS_NODEJS_CRT_GENERAL, "Failed to copy MQTT payload buffer, payload will not be delivered"); goto on_error; } AWS_NAPI_ENSURE(NULL, aws_napi_queue_threadsafe_function(binding->on_any_publish, args)); return; on_error: s_destroy_on_any_publish_args(args); } napi_value aws_napi_mqtt_client_connection_on_message(napi_env env, napi_callback_info cb_info) { napi_value node_args[2]; size_t num_args = AWS_ARRAY_SIZE(node_args); napi_value *arg = &node_args[0]; AWS_NAPI_CALL(env, napi_get_cb_info(env, cb_info, &num_args, node_args, NULL, NULL), { napi_throw_error(env, NULL, "Failed to retreive callback information"); return NULL; }); if (num_args != AWS_ARRAY_SIZE(node_args)) { napi_throw_error(env, NULL, "mqtt_client_connection_on_message needs exactly 2 arguments"); return NULL; } napi_value node_binding = *arg++; struct mqtt_connection_binding *binding = NULL; AWS_NAPI_CALL(env, napi_get_value_external(env, node_binding, (void **)&binding), { napi_throw_error(env, NULL, "Unable to extract external"); return NULL; }); napi_value node_handler = *arg++; if (aws_napi_is_null_or_undefined(env, node_handler)) { napi_throw_error(env, NULL, "handler must not be null or undefined"); return NULL; } /* * There's no reasonable way of making this safe for multiple calls. We have to assume this is pre-connect * otherwise the callback could be getting used in another thread as we try and change it here. */ if (binding->on_any_publish != NULL) { napi_throw_error(env, NULL, "on_any_publish handler cannot be set more than once"); return NULL; } AWS_NAPI_CALL( env, aws_napi_create_threadsafe_function( env, node_handler, "on_any_publish", s_on_any_publish_call, binding, &binding->on_any_publish), { return NULL; }); if (aws_mqtt_client_connection_set_on_any_publish_handler(binding->connection, s_on_any_publish, binding)) { napi_throw_error(env, NULL, "Unable to set on_any_publish handler"); return NULL; } return NULL; } /******************************************************************************* * Unsubscribe ******************************************************************************/ struct unsuback_args { struct aws_allocator *allocator; struct mqtt_connection_binding *binding; struct aws_byte_buf topic; /* stored here until unsub completes */ uint16_t packet_id; int error_code; napi_threadsafe_function on_unsuback; }; static void s_destroy_unsuback_args(struct unsuback_args *args) { if (args == NULL) { return; } AWS_FATAL_ASSERT(args->allocator != NULL); aws_byte_buf_clean_up(&args->topic); if (args->on_unsuback != 0) { AWS_FATAL_ASSERT(args->binding != NULL); AWS_NAPI_ENSURE(args->binding->env, aws_napi_release_threadsafe_function(args->on_unsuback, napi_tsfn_abort)); } aws_mem_release(args->allocator, args); } static void s_on_unsub_ack_call(napi_env env, napi_value on_unsuback, void *context, void *user_data) { (void)context; struct unsuback_args *args = user_data; if (env) { napi_value params[2]; const size_t num_params = AWS_ARRAY_SIZE(params); AWS_NAPI_ENSURE(env, napi_create_uint32(env, args->packet_id, ¶ms[0])); AWS_NAPI_ENSURE(env, napi_create_int32(env, args->error_code, ¶ms[1])); AWS_NAPI_ENSURE( env, aws_napi_dispatch_threadsafe_function(env, args->on_unsuback, NULL, on_unsuback, num_params, params)); } s_destroy_unsuback_args(args); } static void s_on_unsubscribe_complete( struct aws_mqtt_client_connection *connection, uint16_t packet_id, int error_code, void *user_data) { (void)connection; struct unsuback_args *args = user_data; if (!args->on_unsuback) { s_destroy_unsuback_args(args); return; } args->packet_id = packet_id; args->error_code = error_code; AWS_NAPI_ENSURE(NULL, aws_napi_queue_threadsafe_function(args->on_unsuback, args)); } napi_value aws_napi_mqtt_client_connection_unsubscribe(napi_env env, napi_callback_info cb_info) { napi_value node_args[3]; size_t num_args = AWS_ARRAY_SIZE(node_args); napi_value *arg = &node_args[0]; AWS_NAPI_CALL(env, napi_get_cb_info(env, cb_info, &num_args, node_args, NULL, NULL), { napi_throw_error(env, NULL, "Failed to retreive callback information"); return NULL; }); if (num_args != AWS_ARRAY_SIZE(node_args)) { napi_throw_error(env, NULL, "mqtt_client_connection_unsubscribe needs exactly 3 arguments"); return NULL; } napi_value node_binding = *arg++; struct mqtt_connection_binding *binding = NULL; AWS_NAPI_CALL(env, napi_get_value_external(env, node_binding, (void **)&binding), { napi_throw_error(env, NULL, "Failed to extract binding from external"); return NULL; }); struct aws_allocator *allocator = binding->allocator; struct unsuback_args *args = aws_mem_calloc(allocator, 1, sizeof(struct unsuback_args)); AWS_FATAL_ASSERT(args); args->allocator = allocator; args->binding = binding; if (binding->connection == NULL) { napi_throw_error(env, NULL, "Connection has been closed and can no longer be used"); goto cleanup; } napi_value node_topic = *arg++; if (aws_byte_buf_init_from_napi(&args->topic, env, node_topic)) { napi_throw_type_error(env, NULL, "topic must be a String"); goto cleanup; } napi_value node_on_unsuback = *arg++; if (!aws_napi_is_null_or_undefined(env, node_on_unsuback)) { AWS_NAPI_CALL( env, aws_napi_create_threadsafe_function( env, node_on_unsuback, "aws_mqtt_client_connection_on_unsuback", s_on_unsub_ack_call, binding, &args->on_unsuback), { goto cleanup; }); } const struct aws_byte_cursor topic_cur = aws_byte_cursor_from_buf(&args->topic); uint16_t unsub_id = aws_mqtt_client_connection_unsubscribe(binding->connection, &topic_cur, s_on_unsubscribe_complete, args); if (!unsub_id) { napi_throw_error(env, NULL, "Failed to initiate subscribe request"); goto cleanup; } args->packet_id = unsub_id; return NULL; cleanup: s_destroy_unsuback_args(args); return NULL; } /******************************************************************************* * Disconnect ******************************************************************************/ struct disconnect_args { struct aws_allocator *allocator; struct mqtt_connection_binding *binding; napi_threadsafe_function on_disconnect; }; static void s_destroy_disconnect_args(struct disconnect_args *args) { if (args == NULL) { return; } AWS_FATAL_ASSERT(args->allocator != NULL); if (args->on_disconnect != 0) { AWS_FATAL_ASSERT(args->binding != NULL); AWS_NAPI_ENSURE(args->binding->env, aws_napi_release_threadsafe_function(args->on_disconnect, napi_tsfn_abort)); } aws_mem_release(args->allocator, args); } static void s_on_disconnect_call(napi_env env, napi_value on_disconnect, void *context, void *user_data) { (void)context; struct disconnect_args *args = user_data; if (env) { AWS_NAPI_ENSURE( env, aws_napi_dispatch_threadsafe_function(env, args->on_disconnect, NULL, on_disconnect, 0, NULL)); } s_destroy_disconnect_args(args); } static void s_on_disconnected(struct aws_mqtt_client_connection *connection, void *user_data) { (void)connection; struct disconnect_args *args = user_data; if (!args->on_disconnect) { s_destroy_disconnect_args(args); return; } AWS_NAPI_ENSURE(NULL, aws_napi_queue_threadsafe_function(args->on_disconnect, args)); } napi_value aws_napi_mqtt_client_connection_disconnect(napi_env env, napi_callback_info cb_info) { struct mqtt_connection_binding *binding = NULL; napi_value node_args[2]; size_t num_args = AWS_ARRAY_SIZE(node_args); napi_value *arg = &node_args[0]; AWS_NAPI_CALL(env, napi_get_cb_info(env, cb_info, &num_args, node_args, NULL, NULL), { napi_throw_error(env, NULL, "Failed to retrieve callback information"); return NULL; }); if (num_args != AWS_ARRAY_SIZE(node_args)) { napi_throw_error(env, NULL, "mqtt_client_connection_disconnect needs exactly 2 arguments"); return NULL; } napi_value node_binding = *arg++; AWS_NAPI_CALL(env, napi_get_value_external(env, node_binding, (void **)&binding), { napi_throw_error(env, NULL, "Failed to extract binding from external"); return NULL; }); if (binding->connection == NULL) { napi_throw_error(env, NULL, "Connection has been closed and can no longer be used"); return NULL; } struct aws_allocator *allocator = binding->allocator; struct disconnect_args *args = aws_mem_calloc(allocator, 1, sizeof(struct disconnect_args)); AWS_FATAL_ASSERT(args); args->allocator = allocator; args->binding = binding; napi_value node_on_disconnect = *arg++; if (!aws_napi_is_null_or_undefined(env, node_on_disconnect)) { AWS_NAPI_CALL( env, aws_napi_create_threadsafe_function( env, node_on_disconnect, "aws_mqtt_client_connection_on_disconnect", s_on_disconnect_call, binding, &args->on_disconnect), { goto on_error; }); } if (aws_mqtt_client_connection_disconnect(binding->connection, s_on_disconnected, args)) { aws_napi_throw_last_error(env); goto on_error; } return NULL; on_error: s_destroy_disconnect_args(args); return NULL; } /******************************************************************************* * Operation Statistics ******************************************************************************/ static int s_create_napi_mqtt_connection_statistics( napi_env env, const struct aws_mqtt_connection_operation_statistics *stats, napi_value *stats_out) { if (env == NULL) { return aws_raise_error(AWS_CRT_NODEJS_ERROR_THREADSAFE_FUNCTION_NULL_NAPI_ENV); } napi_value napi_stats = NULL; AWS_NAPI_CALL( env, napi_create_object(env, &napi_stats), { return aws_raise_error(AWS_CRT_NODEJS_ERROR_NAPI_FAILURE); }); if (aws_napi_attach_object_property_u64( napi_stats, env, AWS_NAPI_KEY_INCOMPLETE_OPERATION_COUNT, stats->incomplete_operation_count)) { return AWS_OP_ERR; } if (aws_napi_attach_object_property_u64( napi_stats, env, AWS_NAPI_KEY_INCOMPLETE_OPERATION_SIZE, stats->incomplete_operation_size)) { return AWS_OP_ERR; } if (aws_napi_attach_object_property_u64( napi_stats, env, AWS_NAPI_KEY_UNACKED_OPERATION_COUNT, stats->unacked_operation_count)) { return AWS_OP_ERR; } if (aws_napi_attach_object_property_u64( napi_stats, env, AWS_NAPI_KEY_UNACKED_OPERATION_SIZE, stats->unacked_operation_size)) { return AWS_OP_ERR; }; *stats_out = napi_stats; return AWS_OP_SUCCESS; } napi_value aws_napi_mqtt_client_connection_get_queue_statistics(napi_env env, napi_callback_info info) { napi_value node_args[1]; size_t num_args = AWS_ARRAY_SIZE(node_args); napi_value *arg = &node_args[0]; AWS_NAPI_CALL(env, napi_get_cb_info(env, info, &num_args, node_args, NULL, NULL), { napi_throw_error( env, NULL, "aws_napi_mqtt_client_connection_get_queue_statistics - Failed to extract parameter array"); return NULL; }); if (num_args != AWS_ARRAY_SIZE(node_args)) { napi_throw_error(env, NULL, "aws_napi_mqtt_client_connection_get_queue_statistics - needs exactly 1 argument"); return NULL; } struct mqtt_connection_binding *binding = NULL; napi_value node_binding = *arg++; AWS_NAPI_CALL(env, napi_get_value_external(env, node_binding, (void **)&binding), { napi_throw_error(env, NULL, "Failed to extract binding from external"); return NULL; }); if (binding == NULL) { napi_throw_error(env, NULL, "aws_napi_mqtt_client_connection_get_queue_statistics - binding was null"); return NULL; } if (binding->connection == NULL) { napi_throw_error(env, NULL, "Connection has been closed and can no longer be used"); return NULL; } struct aws_mqtt_connection_operation_statistics stats; AWS_ZERO_STRUCT(stats); aws_mqtt_client_connection_get_stats(binding->connection, &stats); napi_value napi_stats = NULL; if (s_create_napi_mqtt_connection_statistics(env, &stats, &napi_stats)) { napi_throw_error( env, NULL, "aws_napi_mqtt_client_connection_get_queue_statistics - failed to build statistics value"); return NULL; } return napi_stats; } /******************************************************************************* * On Closed ******************************************************************************/ static void s_on_closed_call(napi_env env, napi_value on_closed, void *context, void *user_data) { (void)user_data; struct mqtt_connection_binding *binding = context; if (binding->on_closed == NULL) { return; } if (env) { AWS_NAPI_ENSURE(env, aws_napi_dispatch_threadsafe_function(env, binding->on_closed, NULL, on_closed, 0, NULL)); } } static void s_on_closed( struct aws_mqtt_client_connection *connection, struct on_connection_closed_data *data, void *user_data) { (void)data; (void)connection; struct mqtt_connection_binding *binding = user_data; if (binding->on_closed == NULL) { return; } AWS_NAPI_ENSURE(NULL, aws_napi_queue_threadsafe_function(binding->on_closed, binding)); return; } napi_value aws_napi_mqtt_client_connection_on_closed(napi_env env, napi_callback_info cb_info) { napi_value node_args[2]; size_t num_args = AWS_ARRAY_SIZE(node_args); napi_value *arg = &node_args[0]; AWS_NAPI_CALL(env, napi_get_cb_info(env, cb_info, &num_args, node_args, NULL, NULL), { napi_throw_error(env, NULL, "Failed to retreive callback information"); return NULL; }); if (num_args != AWS_ARRAY_SIZE(node_args)) { napi_throw_error(env, NULL, "mqtt_client_on_closed needs exactly 2 arguments"); return NULL; } napi_value node_binding = *arg++; struct mqtt_connection_binding *binding = NULL; AWS_NAPI_CALL(env, napi_get_value_external(env, node_binding, (void **)&binding), { napi_throw_error(env, NULL, "Unable to extract external"); return NULL; }); napi_value node_handler = *arg++; if (aws_napi_is_null_or_undefined(env, node_handler)) { napi_throw_error(env, NULL, "handler must not be null or undefined"); return NULL; } /* * There's no reasonable way of making this safe for multiple calls. We have to assume this is pre-connect * otherwise the callback could be getting used in another thread as we try and change it here. */ if (binding->on_closed != NULL) { napi_throw_error(env, NULL, "on_closed handler cannot be set more than once"); return NULL; } AWS_NAPI_CALL( env, aws_napi_create_threadsafe_function( env, node_handler, "on_closed", s_on_closed_call, binding, &binding->on_closed), { return NULL; }); if (aws_mqtt_client_connection_set_connection_closed_handler(binding->connection, s_on_closed, binding)) { napi_throw_error(env, NULL, "Unable to set on_closed handler"); return NULL; } return NULL; }