diff --git a/.github/workflows/commands-handler.yml b/.github/workflows/commands-handler.yml index 51f8668f..3bcc0f83 100644 --- a/.github/workflows/commands-handler.yml +++ b/.github/workflows/commands-handler.yml @@ -24,12 +24,12 @@ jobs: run: echo -e "\033[38;2;19;181;255mThis is regular commit which should be ignored.\033[0m" - name: Checkout repository if: steps.user-check.outputs.expected-user == 'true' - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: token: ${{ secrets.GH_TOKEN }} - name: Checkout release actions if: steps.user-check.outputs.expected-user == 'true' - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: repository: pubnub/client-engineering-deployment-tools ref: v1 diff --git a/.github/workflows/composite/unit-test-framework/action.yaml b/.github/workflows/composite/unit-test-framework/action.yaml index 70b3fd18..1f2461cd 100644 --- a/.github/workflows/composite/unit-test-framework/action.yaml +++ b/.github/workflows/composite/unit-test-framework/action.yaml @@ -27,7 +27,7 @@ runs: ${{ inputs.os }}-cgreen-${{ inputs.version }}- - name: Checkout Unit Test framework if: steps.unit-test-framework.outputs.cache-hit != 'true' - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: repository: cgreen-devs/cgreen ref: ${{ matrix.cgreen }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 57643b37..6d40c76f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,7 @@ jobs: release: ${{ steps.check.outputs.ready }} steps: - name: Checkout actions - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: repository: pubnub/client-engineering-deployment-tools ref: v1 @@ -34,13 +34,13 @@ jobs: group: organization/Default steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: # This should be the same as the one specified for on.pull_request.branches ref: master token: ${{ secrets.GH_TOKEN }} - name: Checkout actions - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: repository: pubnub/client-engineering-deployment-tools ref: v1 diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 92878f8b..afd2fc91 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -25,11 +25,11 @@ jobs: # group: organization/macos-gh steps: - name: Checkout project - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: token: ${{ secrets.GH_TOKEN }} - name: Checkout actions - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: repository: pubnub/client-engineering-deployment-tools ref: v1 @@ -59,11 +59,11 @@ jobs: sudo apt-get install -y ninja-build libboost-all-dev libssl-dev libgtest-dev nlohmann-json3-dev libasio-dev libtclap-dev g++ cmake sudo gem install cucumber - name: Checkout project - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: token: ${{ secrets.GH_TOKEN }} - name: Checkout mock-server action - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: repository: pubnub/client-engineering-deployment-tools ref: v1 @@ -84,7 +84,7 @@ jobs: ${{ runner.os }}-cucumber-cpp- - name: Checkout Cucumber if: steps.cucumber-cpp.outputs.cache-hit != 'true' - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: repository: cucumber/cucumber-cpp ref: c79100eb70fbb34f6ea10030cec051c2cc9f7961 diff --git a/.github/workflows/run-validations.yml b/.github/workflows/run-validations.yml index bdba8ea1..2076408b 100644 --- a/.github/workflows/run-validations.yml +++ b/.github/workflows/run-validations.yml @@ -9,9 +9,9 @@ jobs: group: organization/Default steps: - name: Checkout project - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Checkout validator action - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: repository: pubnub/client-engineering-deployment-tools ref: v1 diff --git a/.pubnub.yml b/.pubnub.yml index acd61929..1a1025a6 100644 --- a/.pubnub.yml +++ b/.pubnub.yml @@ -1,8 +1,17 @@ name: c-core schema: 1 -version: "5.3.2" +version: "5.4.0" scm: github.com/pubnub/c-core changelog: + - date: 2025-10-21 + version: v5.4.0 + changes: + - type: feature + text: "Provide interfaces (`pubnub_use_tcp_keep_alive`, `pubnub_dont_use_tcp_keep_alive`, `use_tcp_keep_alive`, and `dont_use_tcp_keep_alive`) to set up active TCP connection keep-alive packet sending." + - type: bug + text: "Replace the Windows API, which in a multithreaded environment became a reason for crashes during the DNS resolution process." + - type: bug + text: "Fix the issue because of which client created a secondary connection when built with `PUBNUB_USE_IPV6` support." - date: 2025-09-23 version: v5.3.2 changes: @@ -1036,7 +1045,7 @@ sdks: distribution-type: source code distribution-repository: GitHub release package-name: C-Core - location: https://github.com/pubnub/c-core/releases/tag/v5.3.2 + location: https://github.com/pubnub/c-core/releases/tag/v5.4.0 requires: - name: "miniz" @@ -1102,7 +1111,7 @@ sdks: distribution-type: source code distribution-repository: GitHub release package-name: C-Core - location: https://github.com/pubnub/c-core/releases/tag/v5.3.2 + location: https://github.com/pubnub/c-core/releases/tag/v5.4.0 requires: - name: "miniz" @@ -1168,7 +1177,7 @@ sdks: distribution-type: source code distribution-repository: GitHub release package-name: C-Core - location: https://github.com/pubnub/c-core/releases/tag/v5.3.2 + location: https://github.com/pubnub/c-core/releases/tag/v5.4.0 requires: - name: "miniz" @@ -1230,7 +1239,7 @@ sdks: distribution-type: source code distribution-repository: GitHub release package-name: C-Core - location: https://github.com/pubnub/c-core/releases/tag/v5.3.2 + location: https://github.com/pubnub/c-core/releases/tag/v5.4.0 requires: - name: "miniz" @@ -1291,7 +1300,7 @@ sdks: distribution-type: source code distribution-repository: GitHub release package-name: C-Core - location: https://github.com/pubnub/c-core/releases/tag/v5.3.2 + location: https://github.com/pubnub/c-core/releases/tag/v5.4.0 requires: - name: "miniz" @@ -1347,7 +1356,7 @@ sdks: distribution-type: source code distribution-repository: GitHub release package-name: C-Core - location: https://github.com/pubnub/c-core/releases/tag/v5.3.2 + location: https://github.com/pubnub/c-core/releases/tag/v5.4.0 requires: - name: "miniz" @@ -1400,7 +1409,7 @@ sdks: distribution-type: source code distribution-repository: GitHub release package-name: C-Core - location: https://github.com/pubnub/c-core/releases/tag/v5.3.2 + location: https://github.com/pubnub/c-core/releases/tag/v5.4.0 requires: - name: "miniz" diff --git a/CHANGELOG.md b/CHANGELOG.md index f6421db1..731f9516 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## v5.4.0 +October 21 2025 + +#### Added +- Provide interfaces (`pubnub_use_tcp_keep_alive`, `pubnub_dont_use_tcp_keep_alive`, `use_tcp_keep_alive`, and `dont_use_tcp_keep_alive`) to set up active TCP connection keep-alive packet sending. + +#### Fixed +- Replace the Windows API, which in a multithreaded environment became a reason for crashes during the DNS resolution process. +- Fix the issue because of which client created a secondary connection when built with `PUBNUB_USE_IPV6` support. + ## v5.3.2 September 23 2025 diff --git a/core/pbauto_heartbeat.c b/core/pbauto_heartbeat.c index 74883290..116eb661 100644 --- a/core/pbauto_heartbeat.c +++ b/core/pbauto_heartbeat.c @@ -98,6 +98,10 @@ static int copy_context_settings(pubnub_t* pb_clone, pubnub_t const* pb) pb_clone->origin = pb->origin; } pb_clone->options.use_http_keep_alive = pb->options.use_http_keep_alive; + pb_clone->options.tcp_keepalive.enabled = pb->options.tcp_keepalive.enabled; + pb_clone->options.tcp_keepalive.time = pb->options.tcp_keepalive.time; + pb_clone->options.tcp_keepalive.interval = pb->options.tcp_keepalive.interval; + pb_clone->options.tcp_keepalive.probes = pb->options.tcp_keepalive.probes; #if PUBNUB_USE_IPV6 && defined(PUBNUB_CALLBACK_API) pb_clone->options.ipv6_connectivity = pb->options.ipv6_connectivity; #endif @@ -252,6 +256,14 @@ static void auto_heartbeat_callback(pubnub_t* heartbeat_pb, if (pubsub_keys_changed(heartbeat_pb, pb)) { pubnub_init( heartbeat_pb, pb->core.publish_key, pb->core.subscribe_key); + if (pbccTrue == pb->options.tcp_keepalive.enabled) { + pubnub_use_tcp_keep_alive( + heartbeat_pb, + pb->options.tcp_keepalive.time, + pb->options.tcp_keepalive.interval, + pb->options.tcp_keepalive.probes); + } else if (pbccFalse == pb->options.tcp_keepalive.enabled) + pubnub_dont_use_tcp_keep_alive(heartbeat_pb); heartbeat_thump(pb, heartbeat_pb); } @@ -275,6 +287,14 @@ static pubnub_t* init_new_thumper_pb(pubnub_t* pb, unsigned i) } pubnub_mutex_lock(pb->monitor); pubnub_init(pb_new, pb->core.publish_key, pb->core.subscribe_key); + if (pbccTrue == pb->options.tcp_keepalive.enabled) { + pubnub_use_tcp_keep_alive( + pb_new, + pb->options.tcp_keepalive.time, + pb->options.tcp_keepalive.interval, + pb->options.tcp_keepalive.probes); + } else if (pbccFalse == pb->options.tcp_keepalive.enabled) + pubnub_dont_use_tcp_keep_alive(pb_new); pubnub_mutex_unlock(pb->monitor); pubnub_mutex_lock(pb_new->monitor); diff --git a/core/pbpal.h b/core/pbpal.h index 8e2247fa..6ee984f3 100644 --- a/core/pbpal.h +++ b/core/pbpal.h @@ -251,6 +251,9 @@ int pbpal_close(pubnub_t *pb); /** Sets blocking I/O option on the context for the communication */ int pbpal_set_blocking_io(pubnub_t *pb); +/** Sets user-provided TCP Keep-Alive configuration for active connection. */ +void pbpal_set_tcp_keepalive(const pubnub_t *pb); + /** Frees-up any resources allocated by the PAL for the given context. After this call, context is not safe for use by PAL any more (it is assumed it will be freed-up by the caller). diff --git a/core/pubnub_internal_common.h b/core/pubnub_internal_common.h index 3086a733..7288bc12 100644 --- a/core/pubnub_internal_common.h +++ b/core/pubnub_internal_common.h @@ -197,6 +197,27 @@ struct pbntlm_context { typedef struct pbntlm_context pbntlm_ctx_t; +/** TCP Keep-Alive configuration object. */ +typedef struct pubnub_tcp_keepalive_ { + /** Whether TCP Keep-Alive should be used or not. */ + enum pubnub_tribool enabled; + + /** The time in seconds a socket needs to be @c idle before the first + keep-alive probe is sent. + */ + uint8_t time; + + /** The number of seconds that should pass between sends of + keep-alive probes if the last one wasn't acknowledged. + */ + uint8_t interval; + + /** The number of times a probe will be sent and not acknowledged + before the connection is deemed broken. + */ + uint8_t probes; +} pubnub_tcp_keepalive; + struct pubnub_options { #if PUBNUB_BLOCKING_IO_SETTABLE /** Indicates whether to use blocking I/O. Not implemented if @@ -214,6 +235,9 @@ struct pubnub_options { */ bool use_http_keep_alive : 1; + /** Per-context (because of one request per-context) TCP Keep-Alive + configuration. */ + pubnub_tcp_keepalive tcp_keepalive; #if PUBNUB_USE_IPV6 /* Connectivity type(true-Ipv6/false-Ipv4) chosen on a given context */ bool ipv6_connectivity : 1; diff --git a/core/pubnub_netcore.c b/core/pubnub_netcore.c index 098f9828..6822c208 100644 --- a/core/pubnub_netcore.c +++ b/core/pubnub_netcore.c @@ -226,9 +226,7 @@ static enum pubnub_state close_kept_alive_connection(struct pubnub_* pb) pbpal_forget(pb); return PBS_IDLE; } - else { - return PBS_KEEP_ALIVE_WAIT_CLOSE; - } + return PBS_KEEP_ALIVE_WAIT_CLOSE; } @@ -1085,7 +1083,6 @@ int pbnc_fsm(struct pubnub_* pb) pb->state = PBS_RX_HEADERS; goto next_state; case PNR_CONNECTION_TIMEOUT: - case PNR_TIMEOUT: case PNR_IO_ERROR: if (pb->flags.started_while_kept_alive) { pb->state = close_kept_alive_connection(pb); diff --git a/core/pubnub_pubsubapi.c b/core/pubnub_pubsubapi.c index 8df80ce7..b1bdfd1c 100644 --- a/core/pubnub_pubsubapi.c +++ b/core/pubnub_pubsubapi.c @@ -84,6 +84,12 @@ pubnub_t* pubnub_init(pubnub_t* p, const char* publish_key, const char* subscrib p->state = PBS_IDLE; p->trans = PBTT_NONE; p->options.use_http_keep_alive = true; + + // Setting default TCP keep-alive options. + p->options.tcp_keepalive.enabled = pbccTrue; + p->options.tcp_keepalive.time = 60; + p->options.tcp_keepalive.interval = 20; + p->options.tcp_keepalive.probes = 3; #if !defined(PUBNUB_CALLBACK_API) || defined(PUBNUB_NTF_RUNTIME_SELECTION) p->should_stop_await = false; #endif @@ -540,3 +546,20 @@ void pubnub_dont_use_http_keep_alive(pubnub_t* p) { p->options.use_http_keep_alive = 0; } + +void pubnub_use_tcp_keep_alive( + pubnub_t* pb, + const uint8_t time, + const uint8_t interval, + const uint8_t probes) +{ + pb->options.tcp_keepalive.enabled = pbccTrue; + pb->options.tcp_keepalive.time = time; + pb->options.tcp_keepalive.interval = interval; + pb->options.tcp_keepalive.probes = probes; +} + +void pubnub_dont_use_tcp_keep_alive(pubnub_t* pb) +{ + pb->options.tcp_keepalive.enabled = pbccFalse; +} \ No newline at end of file diff --git a/core/pubnub_pubsubapi.h b/core/pubnub_pubsubapi.h index c435af1f..b31eb0a2 100644 --- a/core/pubnub_pubsubapi.h +++ b/core/pubnub_pubsubapi.h @@ -394,5 +394,39 @@ PUBNUB_EXTERN void pubnub_use_http_keep_alive(pubnub_t* p); */ PUBNUB_EXTERN void pubnub_dont_use_http_keep_alive(pubnub_t* p); +/** Enable the use of TCP Keep-Alive ("probes") on the context @p pb . + * + * @b Defaults: + * - @c time: @b 60 seconds + * - @c interval: @b 20 seconds + * - @c probes: @b 3 + * + * @b Important: this option works well @b only together with HTTP Keep-Alive, which + * is managed by @c pubnub_use_http_keep_alive and + * @c pubnub_dont_use_http_keep_alive. + * + * @param pb Pointer to the PubNub context which TCP KA should be enabled. + * @param time The time in seconds a socket needs to be @c idle before the + * first keep-alive probe is sent. + * @param interval The number of seconds that should pass between sends of + * keep-alive probes if the last one wasn't acknowledged. + * @param probes The number of times a probe will be sent and not acknowledged + * before the connection is deemed broken. + */ +PUBNUB_EXTERN void pubnub_use_tcp_keep_alive( + pubnub_t* pb, + uint8_t time, + uint8_t interval, + uint8_t probes); + +/** Disables the use of TCP Keep-Alive ("probes") on the context @p pb . + * + * @b Important: this option works @b only together with HTTP Keep-Alive, which + * is managed by @c pubnub_use_http_keep_alive and + * @c pubnub_dont_use_http_keep_alive. + * + * @param pb Pointer to the PubNub context which TCP KA should be disabled. + */ +PUBNUB_EXTERN void pubnub_dont_use_tcp_keep_alive(pubnub_t* pb); #endif /* !defined INC_PUBNUB_PUBSUBAPI */ diff --git a/core/pubnub_version_internal.h b/core/pubnub_version_internal.h index ee08a47c..4987790f 100644 --- a/core/pubnub_version_internal.h +++ b/core/pubnub_version_internal.h @@ -3,7 +3,7 @@ #define INC_PUBNUB_VERSION_INTERNAL -#define PUBNUB_SDK_VERSION "5.3.2" +#define PUBNUB_SDK_VERSION "5.4.0" #endif /* !defined INC_PUBNUB_VERSION_INTERNAL */ diff --git a/cpp/pubnub_common.hpp b/cpp/pubnub_common.hpp index fe0d2f8e..fc7d83be 100644 --- a/cpp/pubnub_common.hpp +++ b/cpp/pubnub_common.hpp @@ -1933,6 +1933,23 @@ class context { pubnub_dont_use_http_keep_alive(d_pb); } + /// Use of TCP Keep-Alive ("probes") on the context. + /// @see pubnub_use_tcp_keep_alive + void use_tcp_keep_alive( + uint8_t time, + uint8_t interval, + uint8_t probes) + { + pubnub_use_tcp_keep_alive(d_pb, time, interval, probes); + } + + /// Don't use of TCP Keep-Alive ("probes") on the context. + /// @see pubnub_dont_use_tcp_keep_alive + void dont_use_tcp_keep_alive() + { + pubnub_dont_use_tcp_keep_alive(d_pb); + } + #if PUBNUB_PROXY_API /// Manually set a proxy to use /// @see pubnub_set_proxy_manual diff --git a/freertos/pbpal_resolv_and_connect_freertos_tcp.c b/freertos/pbpal_resolv_and_connect_freertos_tcp.c index 6dcd8d92..5ca0c12d 100644 --- a/freertos/pbpal_resolv_and_connect_freertos_tcp.c +++ b/freertos/pbpal_resolv_and_connect_freertos_tcp.c @@ -1,8 +1,9 @@ /* -*- c-file-style:"stroustrup"; indent-tabs-mode: nil -*- */ -#include "lwip/dns.h" -#include "lwip/inet.h" +#include "lwip/sockets.h" #include "lwip/ip_addr.h" #include "lwip/netdb.h" +#include "lwip/inet.h" +#include "lwip/dns.h" #include "pbpal.h" #include "pubnub_internal.h" @@ -44,6 +45,7 @@ enum pbpal_resolv_n_connect_result pbpal_resolv_and_connect(pubnub_t *pb) if (pb->pal.socket == SOCKET_INVALID) { return pbpal_connect_resource_failure; } + pbpal_set_tcp_keepalive(pb); if (connect(pb->pal.socket, (const struct sockaddr*) &addr, sizeof addr) != 0) { closesocket(pb->pal.socket); pb->pal.socket = SOCKET_INVALID; @@ -61,6 +63,7 @@ enum pbpal_resolv_n_connect_result pbpal_resolv_and_connect(pubnub_t *pb) if (pb->pal.socket == SOCKET_INVALID) { return pbpal_connect_resource_failure; } + pbpal_set_tcp_keepalive(pb); if (connect(pb->pal.socket, &addr, sizeof addr) != 0) { closesocket(pb->pal.socket); pb->pal.socket = SOCKET_INVALID; @@ -101,3 +104,38 @@ int pbpal_dns_rotate_server(pubnub_t *pb) } #endif /* PUBNUB_CHANGE_DNS_SERVERS */ #endif /* defined(PUBNUB_CALLBACK_API) */ + +void pbpal_set_tcp_keepalive(const pubnub_t *pb) +{ + if (pb->pal.socket == SOCKET_INVALID) return; + const pubnub_tcp_keepalive keepalive = pb->options.tcp_keepalive; + const pb_socket_t skt = pb->pal.socket; + + const int enabled = pbccTrue == keepalive.enabled ? 1 : 0; + (void)setsockopt(skt, SOL_SOCKET, SO_KEEPALIVE, &enabled, sizeof(enabled)); + + if (pbccTrue != keepalive.enabled || + (0 == keepalive.time && 0 == keepalive.interval)) return; + + const int time = keepalive.time; + + if (time > 0) { +#if defined(TCP_KEEPIDLE) + (void)setsockopt(skt, IPPROTO_TCP, TCP_KEEPIDLE, &time, sizeof(time)); +#elif defined(TCP_KEEPALIVE) + (void)setsockopt(skt, IPPROTO_TCP, TCP_KEEPALIVE, &time, sizeof(time)); +#endif + } + +#if defined(TCP_KEEPINTVL) + const int interval = keepalive.interval; + if (interval > 0) + (void)setsockopt(skt, IPPROTO_TCP, TCP_KEEPINTVL, &interval, sizeof(interval)); +#endif + +#if defined(TCP_KEEPCNT) + const int probes = keepalive.probes; + if (probes > 0) + (void)setsockopt(skt, IPPROTO_TCP, TCP_KEEPCNT, &probes, sizeof(probes)); +#endif +} diff --git a/lib/sockets/pbpal_handle_socket_error.c b/lib/sockets/pbpal_handle_socket_error.c index e3ed7396..bf6dea81 100644 --- a/lib/sockets/pbpal_handle_socket_error.c +++ b/lib/sockets/pbpal_handle_socket_error.c @@ -72,9 +72,20 @@ enum pubnub_res pbpal_handle_socket_error(int socket_result, return PNR_IN_PROGRESS; } else { + // Whether socket already in use for data sending / receiving or not. + const bool handles_data = STATE_READ == pb->sock_state || + STATE_NEWDATA_EXHAUSTED == pb->sock_state || + STATE_READ_LINE == pb->sock_state || + STATE_SENDING_DATA == pb->sock_state; + const bool timed_out = socket_timed_out(); pb->sock_state = STATE_NONE; pbpal_report_error_from_environment(pb, file, line); - return socket_timed_out() ? PNR_CONNECTION_TIMEOUT : PNR_IO_ERROR; + + // Report data sending / receive timeout, which happened in the + // middle of the operation. + if (timed_out && handles_data) return PNR_TIMEOUT; + + return timed_out ? PNR_CONNECTION_TIMEOUT : PNR_IO_ERROR; } } else if (0 == socket_result) { diff --git a/lib/sockets/pbpal_resolv_and_connect_sockets.c b/lib/sockets/pbpal_resolv_and_connect_sockets.c index 97a89f83..cb170c13 100644 --- a/lib/sockets/pbpal_resolv_and_connect_sockets.c +++ b/lib/sockets/pbpal_resolv_and_connect_sockets.c @@ -15,8 +15,10 @@ #if defined(_WIN32) #include "windows/pubnub_get_native_socket.h" +#include #else #include "posix/pubnub_get_native_socket.h" +#include #endif #define HTTP_PORT 80 @@ -51,7 +53,6 @@ static void prepare_port_and_hostname(pubnub_t* pb, { PUBNUB_ASSERT(pb_valid_ctx_ptr(pb)); PUBNUB_ASSERT_OPT((pb->state == PBS_READY) || (pb->state == PBS_WAIT_DNS_SEND)); - *p_origin = PUBNUB_ORIGIN; *p_port = HTTP_PORT; #if PUBNUB_USE_SSL if (pb->flags.trySSL) { @@ -63,7 +64,7 @@ static void prepare_port_and_hostname(pubnub_t* pb, if (pb->port != INITIAL_PORT_VALUE) { *p_port = pb->port; } - *p_origin = pb->origin; + *p_origin = pb->origin ? pb->origin : PUBNUB_ORIGIN; #endif #if PUBNUB_PROXY_API switch (pb->proxy_type) { @@ -206,7 +207,7 @@ connect_TCP_socket(pb_socket_t* skt, #endif default: PUBNUB_LOG_ERROR( - "connect_TCP_socket(socket=%ld): invalid internet protokol " + "connect_TCP_socket(socket=%ld): invalid internet protocol " "dest->sa_family =%uh\n", (long)*skt, dest->sa_family); @@ -223,6 +224,7 @@ connect_TCP_socket(pb_socket_t* skt, return socket_would_block() ? pbpal_connect_wouldblock : pbpal_connect_failed; } + return pbpal_connect_success; } @@ -440,8 +442,14 @@ enum pbpal_resolv_n_connect_result pbpal_resolv_and_connect(pubnub_t* pb) pb->proxy_ipv4_address.ipv4, sizeof dest.sin_addr.s_addr); dest.sin_family = AF_INET; +#if defined(_WIN32) + enum pbpal_resolv_n_connect_result rslt = connect_TCP_socket( + &pb->pal.socket, &pb->options, (struct sockaddr*)&dest, port); + if (pbpal_connect_success == rslt) pbpal_set_tcp_keepalive(pb); +#else return connect_TCP_socket( &pb->pal.socket, &pb->options, (struct sockaddr*)&dest, port); +#endif } #if PUBNUB_USE_IPV6 if (has_ipv6_proxy) { @@ -452,8 +460,14 @@ enum pbpal_resolv_n_connect_result pbpal_resolv_and_connect(pubnub_t* pb) pb->proxy_ipv6_address.ipv6, sizeof dest.sin6_addr.s6_addr); dest.sin6_family = AF_INET6; +#if defined(_WIN32) + enum pbpal_resolv_n_connect_result rslt = connect_TCP_socket( + &pb->pal.socket, &pb->options, (struct sockaddr*)&dest, port); + if (pbpal_connect_success == rslt) pbpal_set_tcp_keepalive(pb); +#else return connect_TCP_socket( &pb->pal.socket, &pb->options, (struct sockaddr*)&dest, port); +#endif } #endif /* PUBNUB_USE_IPV6 */ #endif /* PUBNUB_PROXY_API */ @@ -462,6 +476,9 @@ enum pbpal_resolv_n_connect_result pbpal_resolv_and_connect(pubnub_t* pb) enum pbpal_resolv_n_connect_result rslt; rslt = try_TCP_connect_spare_address( &pb->pal.socket, &pb->spare_addresses, &pb->options, &pb->flags, port); +#if defined(_WIN32) + if (pbpal_connect_success == rslt) pbpal_set_tcp_keepalive(pb); +#endif if (rslt != pbpal_resolv_resource_failure) { return rslt; } @@ -536,7 +553,13 @@ enum pbpal_resolv_n_connect_result pbpal_resolv_and_connect(pubnub_t* pb) if (pb->pal.socket == SOCKET_INVALID) { continue; } + pbpal_set_blocking_io(pb); +#ifndef _WIN32 + const int enabled = pbccTrue == pb->options.tcp_keepalive.enabled ? 1 : 0; + (void)setsockopt(pb->pal.socket, SOL_SOCKET, SO_KEEPALIVE, &enabled, sizeof(enabled)); + pbpal_set_tcp_keepalive(pb); +#endif if (connect(pb->pal.socket, it->ai_addr, it->ai_addrlen) == SOCKET_ERROR) { if (socket_would_block()) { error = 1; @@ -553,6 +576,7 @@ enum pbpal_resolv_n_connect_result pbpal_resolv_and_connect(pubnub_t* pb) break; } #if PUBNUB_USE_IPV6 + if (1 == error) break; } #endif /* PUBNUB_USE_IPV6 */ freeaddrinfo(result); @@ -564,6 +588,10 @@ enum pbpal_resolv_n_connect_result pbpal_resolv_and_connect(pubnub_t* pb) socket_set_rcv_timeout(pb->pal.socket, pb->transaction_timeout_ms); socket_disable_SIGPIPE(pb->pal.socket); +#if defined(_WIN32) + if (!error) pbpal_set_tcp_keepalive(pb); +#endif + return error ? pbpal_connect_wouldblock : pbpal_connect_success; #endif /* !defined PUBNUB_CALLBACK_API || defined PUBNUB_NTF_RUNTIME_SELECTION */ #ifdef PUBNUB_NTF_RUNTIME_SELECTION @@ -644,6 +672,9 @@ enum pbpal_resolv_n_connect_result pbpal_check_resolv_and_connect(pubnub_t* pb) #endif } #endif /* PUBNUB_USE_MULTIPLE_ADDRESSES */ +#if defined(_WIN32) + if (pbpal_connect_success == rslt) pbpal_set_tcp_keepalive(pb); +#endif return rslt; #endif /* PUBNUB_CALLBACK_API */ #ifdef PUBNUB_NTF_RUNTIME_SELECTION @@ -729,8 +760,54 @@ enum pbpal_resolv_n_connect_result pbpal_check_connect(pubnub_t* pb) } else if (rslt > 0) { PUBNUB_LOG_TRACE("pbpal_connected(): select() event\n"); +#if defined(_WIN32) + pbpal_set_tcp_keepalive(pb); +#endif return pbpal_connect_success; } PUBNUB_LOG_TRACE("pbpal_connected(): no select() events\n"); return pbpal_connect_wouldblock; } + +void pbpal_set_tcp_keepalive(const pubnub_t *pb) +{ + if (pb->pal.socket == SOCKET_INVALID) return; + const pubnub_tcp_keepalive keepalive = pb->options.tcp_keepalive; + const pb_socket_t skt = pb->pal.socket; + +#if defined(_WIN32) + const BOOL enabled = pbccTrue == keepalive.enabled ? TRUE : FALSE; + (void)setsockopt(skt, SOL_SOCKET, SO_KEEPALIVE, (const char*)&enabled, sizeof(enabled)); +#endif + + if (pbccTrue != keepalive.enabled || + (0 == keepalive.time && 0 == keepalive.interval)) return; + +#if defined(_WIN32) + struct tcp_keepalive alive; + DWORD bytes = 0; + alive.onoff = 1; + alive.keepaliveinterval = (keepalive.interval > 0) ? (ULONG)keepalive.interval * 1000UL : 0; + alive.keepalivetime = (keepalive.time > 0) ? (ULONG)keepalive.time * 1000UL : 0; + (void)WSAIoctl(skt, SIO_KEEPALIVE_VALS, &alive, sizeof(alive), + NULL, 0, &bytes, NULL, NULL); +#else + const int interval = keepalive.interval; + const int probes = keepalive.probes; + const int time = keepalive.time; + + if (time > 0) { +#if defined(__APPLE__) + (void)setsockopt(skt, IPPROTO_TCP, TCP_KEEPALIVE, &time, sizeof(time)); +#else + (void)setsockopt(skt, IPPROTO_TCP, TCP_KEEPIDLE, &time, sizeof(time)); +#endif + } + + if (interval > 0) + (void)setsockopt(skt, IPPROTO_TCP, TCP_KEEPINTVL, &interval, sizeof(interval)); + + if (probes > 0) + (void)setsockopt(skt, IPPROTO_TCP, TCP_KEEPCNT, &probes, sizeof(probes)); +#endif +} \ No newline at end of file diff --git a/microchip_harmony/pbpal_resolv_and_connect_harmony_tcp.c b/microchip_harmony/pbpal_resolv_and_connect_harmony_tcp.c index c754cb92..0ed5148b 100644 --- a/microchip_harmony/pbpal_resolv_and_connect_harmony_tcp.c +++ b/microchip_harmony/pbpal_resolv_and_connect_harmony_tcp.c @@ -74,6 +74,7 @@ enum pbpal_resolv_n_connect_result pbpal_check_resolv_and_connect(pubnub_t *pb) if (SOCKET_INVALID == pb->pal.socket) { return pbpal_connect_resource_failure; } + pbpal_set_tcp_keepalive(pb); /* It's not clear that failure here means "you can retry" if using * `NET_PRES_` functions... */ @@ -107,4 +108,18 @@ int pbpal_dns_rotate_server(pubnub_t *pb) return (pbp->flags.sent_queries < PUBNUB_MAX_DNS_QUERIES ? 0 : 1) } #endif /* PUBNUB_CHANGE_DNS_SERVERS */ -#endif /* defined(PUBNUB_CALLBACK_API) */ \ No newline at end of file +#endif /* defined(PUBNUB_CALLBACK_API) */ + +void pbpal_set_tcp_keepalive(const pubnub_t *pb) +{ + if (pb->pal.socket == SOCKET_INVALID) return; + const pubnub_tcp_keepalive keepalive = pb->options.tcp_keepalive; + TCP_SOCKET tcpSock = (TCP_SOCKET)pb->pal.socket; + + TCP_OPTION_KEEP_ALIVE_DATA ka = { + .keepAliveEnable = pbccTrue == keepalive.enabled, + .keepAliveTmo = keepalive.time * 1000u, + .keepAliveUnackLim = keepalive.probes + }; + (void)TCPIP_TCP_OptionsSet(tcpSock, TCP_OPTION_KEEP_ALIVE, &ka); +} \ No newline at end of file diff --git a/windows/pubnub_dns_system_servers.c b/windows/pubnub_dns_system_servers.c index 08a8c01e..a1ff946b 100644 --- a/windows/pubnub_dns_system_servers.c +++ b/windows/pubnub_dns_system_servers.c @@ -2,68 +2,116 @@ #include "pubnub_internal.h" #include "core/pubnub_dns_servers.h" -#include "lib/pubnub_parse_ipv4_addr.h" #include "core/pubnub_assert.h" #include "core/pubnub_log.h" #include #include -#include #include - +#include #pragma comment(lib, "IPHLPAPI.lib") - #define MALLOC(x) HeapAlloc(GetProcessHeap(), 0, (x)) #define FREE(x) HeapFree(GetProcessHeap(), 0, (x)) + +/* Check if an IPv4 address already exists in the array */ +static int ipv4_already_exists( + const struct pubnub_ipv4_address* array, + const size_t count, + const unsigned char new_ip[4]) +{ + for (size_t i = 0; i < count; i++) { + if (memcmp(array[i].ipv4, new_ip, 4) == 0) { + return 1; /* Found duplicate */ + } + } + return 0; /* Not found */ +} + int pubnub_dns_read_system_servers_ipv4(struct pubnub_ipv4_address* o_ipv4, size_t n) { - FIXED_INFO* pFixedInfo; - ULONG ulOutBufLen; - DWORD dwRetVal; - IP_ADDR_STRING* pIPAddr; - unsigned j; - - pFixedInfo = (FIXED_INFO*)MALLOC(sizeof(FIXED_INFO)); - if (pFixedInfo == NULL) { - PUBNUB_LOG_ERROR( - "Error allocating memory needed to call GetNetworkParams\n"); - return -1; + ULONG buflen; + DWORD ret; + IP_ADAPTER_ADDRESSES* addrs; + IP_ADAPTER_ADDRESSES* aa; + IP_ADAPTER_DNS_SERVER_ADDRESS* ds; + const struct sockaddr_in* sin; + DWORD net_addr; + unsigned j; + unsigned char temp_ip[4]; + + if (!o_ipv4 || n == 0) { + return 0; } - ulOutBufLen = sizeof(FIXED_INFO); - - // Make an initial call to GetAdaptersInfo to get - // the necessary size into the ulOutBufLen variable - if (GetNetworkParams(pFixedInfo, &ulOutBufLen) == ERROR_BUFFER_OVERFLOW) { - FREE(pFixedInfo); - pFixedInfo = (FIXED_INFO*)MALLOC(ulOutBufLen); - if (pFixedInfo == NULL) { - PUBNUB_LOG_ERROR( - "Error allocating memory needed to call GetNetworkParams\n"); - return -1; - } + + buflen = 0; + j = 0; + + /* Get required buffer size */ + ret = GetAdaptersAddresses( + AF_INET, + GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST, /* keep DNS servers */ + NULL, NULL, &buflen + ); + + if (ret != ERROR_BUFFER_OVERFLOW || buflen == 0) { + PUBNUB_LOG_ERROR("GetAdaptersAddresses preflight failed: %lu\n", (unsigned long)ret); + return -1; } - dwRetVal = GetNetworkParams(pFixedInfo, &ulOutBufLen); - if (NO_ERROR == dwRetVal) { - j = 0; - pIPAddr = &pFixedInfo->DnsServerList; - while ((j < n) && pIPAddr) { - struct pubnub_ipv4_address addr; - if (pubnub_parse_ipv4_addr(pIPAddr->IpAddress.String, &addr) == 0) { - memcpy(o_ipv4[j++].ipv4, addr.ipv4, sizeof o_ipv4[0].ipv4); - } - pIPAddr = pIPAddr->Next; - } + + addrs = (IP_ADAPTER_ADDRESSES*)MALLOC(buflen); + if (!addrs) { + PUBNUB_LOG_ERROR("OOM allocating %lu for GetAdaptersAddresses\n", (unsigned long)buflen); + return -1; } - else { - PUBNUB_LOG_ERROR("GetNetworkParams failed with error: %d\n", dwRetVal); - FREE(pFixedInfo); + + /* Get adapter information */ + ret = GetAdaptersAddresses( + AF_INET, + GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST, /* keep DNS servers */ + NULL, addrs, &buflen + ); + if (ret != NO_ERROR) { + PUBNUB_LOG_ERROR("GetAdaptersAddresses failed: %lu\n", (unsigned long)ret); + FREE(addrs); return -1; } - FREE(pFixedInfo); + /* Enumerate adapters and collect unique DNS servers */ + for (aa = addrs; aa && j < n; aa = aa->Next) { + for (ds = aa->FirstDnsServerAddress; ds && j < n; ds = ds->Next) { + if (!ds->Address.lpSockaddr || + ds->Address.lpSockaddr->sa_family != AF_INET) { + continue; + } + + sin = (const struct sockaddr_in*)ds->Address.lpSockaddr; + net_addr = sin->sin_addr.S_un.S_addr; + if (net_addr == 0) { + continue; /* skip 0.0.0.0 */ + } + + /* Convert from network order to host order, then extract bytes */ + { + DWORD host_addr = ntohl(net_addr); + temp_ip[0] = (unsigned char)((host_addr >> 24) & 0xFF); + temp_ip[1] = (unsigned char)((host_addr >> 16) & 0xFF); + temp_ip[2] = (unsigned char)((host_addr >> 8) & 0xFF); + temp_ip[3] = (unsigned char)( host_addr & 0xFF); + } + + if (!ipv4_already_exists(o_ipv4, j, temp_ip)) { + o_ipv4[j].ipv4[0] = temp_ip[0]; + o_ipv4[j].ipv4[1] = temp_ip[1]; + o_ipv4[j].ipv4[2] = temp_ip[2]; + o_ipv4[j].ipv4[3] = temp_ip[3]; + ++j; + } + } + } - return j; + FREE(addrs); + return (int)j; } \ No newline at end of file