Re: [PATCH v3 11/18] gweb: Close web request session finalization 'hole'.
Denis Kenzior <[email protected]> Mon, 24 Mar 2025 17:45:25 -0500
| Newsgroups | dev.linux.lists.connman |
|---|---|
| Message-ID | <[email protected]> |
Hi Grant,
On 3/20/25 6:39 PM, Grant Erickson wrote:
> There exist two "holes" in the finalization of web request sessions
> that can, for example in a client such as WISPr, leave one or both of
> IPv4 and IPv6 "online" HTTP-based Internet reachability checks
> "dangling" and non-renewing such that an expected active, default
> network service failover does not occur when it should.
>
> The first of those two failure "holes" involves an end-of-file (EOF)
> condition. In the normal case, there are a series of one or more data
> receipts for the web request as headers and, if present, the body of
> the request response are fulfilled. When there is no further data to
> send, the final receive completes with 'g_io_channel_read_chars'
> returning 'G_IO_STATUS_EOF' status and zero (0) bytes read. This should
> and does lead to a successful EOF closure of the web request.
>
> However, there is a second EOF case that appears to happen with a
> random distribution in the nginx server backing the default "online"
> HTTP-based Internet reachability check URLs,
> 'ipv[46].connman.net/online/status.html'. In that case, the nginx
> server appears to periodically do an unexpected and spontaneous remote
> connection close after the initial connection but before any data has
> been received. Presently, all such failures hit the same
> error-handling block which effectively maps such a failure to the
> effective "null" 'GWEB_HTTP_STATUS_CODE_UNKNOWN' status
> value. Unfortunately, clients such as WISPr have no way to distinguish
> this as an actual failure or success and so it lands on the case:
>
> case GWEB_HTTP_STATUS_CODE_UNKNOWN:
> wispr_portal_context_ref(wp_context);
> __connman_agent_request_browser(wp_context->service,
> wispr_portal_browser_reply_cb,
> wp_context->status_url, wp_context);
>
> which does not "bookend" the original, initiating WISPr web request
> and leaves the original request "dangling" and non-renewed, eventually
> leading to the aforementioned "hole".
>
> To handle this failure EOF case, if GWeb asked for a non-zero amount
> of data from 'g_io_channel_read_chars' but received none and has
> accumulated no headers thus far upon receiving the status
> 'G_IO_STATUS_EOF', then GWeb assumes that the remote peer server
> unexpectedly closed the connection, and synthesizes a new GError with
> the error 'ECONNRESET'. With the addition of the optional, immutable
> GError parameter to the GWebResult callback function, clients such as
> WISPr can now distinguish between a HTTP non-response in which no HTTP
> data was received and one in which HTTP data was received and the
> status can be disguished by the HTTP status code.
>
> The second of the two failure "holes" involves the case where
> 'g_io_channel_read_chars' returns 'G_IO_STATUS_ERROR' status. Prior to
> this change, this funneled to the same "hole" as the 'G_IO_STATUS_ERROR'
> failure with the same consequence to clients such as WISPr.
>
> To handle this error case, GWeb now passes a glib GError pointer to
> 'g_io_channel_read_chars'. If it is set, GWeb passes the GError
> through to the GWebResult callback function. Otherwise, it synthesizes
> a GError anew from 'EIO'. As with the failure EOF case, this allows
> clients to distinguish and handle such failures and to successfully
> "bookend" their initial web request.
> ---
> gweb/gweb.c | 70 ++++++++++++++++++++++++++++++++++++++++++++++++-----
> 1 file changed, 64 insertions(+), 6 deletions(-)
>
<snip>
> diff --git a/gweb/gweb.c b/gweb/gweb.c
> index 7ad12c977ff8..6d517f9467bb 100644
> --- a/gweb/gweb.c
> +++ b/gweb/gweb.c
> @@ -1202,14 +1202,65 @@ static void add_header_field(struct web_session *session)
> }
> }
>
> -static void received_data_finalize(struct web_session *session)
> +static void received_data_finalize(struct web_session *session,
> + GIOStatus status, gsize bytes_available,
> + gsize bytes_read, const GError *error)
> {
> + GError *local_error = NULL;
g_autoptr(GError)?
> + const GError *passed_error = NULL;
> +
> session->transport_watch = 0;
>
> session->result.buffer = NULL;
> session->result.length = 0;
>
> - call_result_func(session, NULL);
> + /* Handle post-channel read errors, which could be either
> + * G_IO_STATUS_ERROR or G_IO_STATUS_EOF.
> + *
> + * For G_IO_STATUS_ERROR, simply pass through the GError, if
> + * non-null. If there is no GError, create a GError anew based on
> + * EIO.
> + *
> + * For G_IO_STATUS_EOF, this could occur at the end of a nominal,
> + * successful get. That is, some number of headers, with or
> + * without a body, termiated by an expected end-of-file (EOF)
> + * condition. However, G_IO_STATUS_EOF can also happen as a result
> + * of the remote peer server unexpectedly terminating the
> + * connection without transferring any data at all. The only
> + * reasonable recovery for this case it to fail the request,
> + * synthesizing ECONNRESET as the error, and to let the client
> + * request again.
> + *
> + * If we asked for a non-zero amount of data but received none and
> + * have accumulated no headers thus far, then we assume that the
> + * remote peer server unexpectedly closed the connection;
> + * otherwise, we assume it is a normal EOF closure.
> + */
> +
> + if (status == G_IO_STATUS_ERROR) {
> + if (error != NULL)
> + passed_error = error;
> + else {
> + local_error = g_error_new_literal(G_IO_ERROR,
> + g_io_error_from_errno(EIO),
> + g_strerror(EIO));
> + passed_error = local_error;
> + }
Why is this needed? Isn't g_io_read_chars always creating a GError object for
you in case of G_IO_STATUS_ERROR return?
> + } else if (status == G_IO_STATUS_EOF) {
> + if (bytes_available > 0 &&
> + bytes_read == 0 &&
> + !g_web_result_has_headers(&session->result, NULL)) {
> + local_error = g_error_new_literal(G_IO_ERROR,
> + g_io_error_from_errno(ECONNRESET),
> + g_strerror(ECONNRESET));
> + passed_error = local_error;
> + }
> + }
> +
> + call_result_func_failure(session, passed_error);
> +
> + if (local_error)
> + g_error_free(local_error);
This could be omitted if g_autoptr is used
> }
>
> static bool received_data_continue(struct web_session *session,
> @@ -1330,9 +1381,11 @@ static gboolean received_data(GIOChannel *channel, GIOCondition cond,
> gpointer user_data)
> {
> struct web_session *session = user_data;
> + const gsize bytes_available = session->receive_space - 1;
> gsize bytes_read;
> GIOStatus status;
> GError *local_error = NULL;
> + GError *error = NULL;
g_autoptr?
>
> /* We received some data or condition, cancel the connect timeout. */
>
Regards,
-Denis