From 677c7e87a5da584e68194c9cca19a91191c3140c Mon Sep 17 00:00:00 2001 From: Christian Grothoff Date: Thu, 19 Aug 2021 14:43:39 +0200 Subject: -implement 'poll' transition in state machine --- doc/sphinx/reducer.rst | 228 ++++++++++++++++------------- src/include/anastasis.h | 8 +- src/lib/anastasis_recovery.c | 8 +- src/reducer/anastasis_api_recovery_redux.c | 165 ++++++++++++++++++++- 4 files changed, 307 insertions(+), 102 deletions(-) diff --git a/doc/sphinx/reducer.rst b/doc/sphinx/reducer.rst index e5f1699..68df5b1 100644 --- a/doc/sphinx/reducer.rst +++ b/doc/sphinx/reducer.rst @@ -1459,121 +1459,139 @@ that applications must all handle. States other than ``solved`` are: } } - - **body**: Here, the server provided an HTTP reply for - how to solve the challenge, but the reducer could not parse - them into a known format. A mime-type may be provided and may - help parse the details. + - **body**: Here, the server provided an HTTP reply for + how to solve the challenge, but the reducer could not parse + them into a known format. A mime-type may be provided and may + help parse the details. - .. code-block:: json + .. code-block:: json - { - "recovery_state": "CHALLENGE_SOLVING", - "recovery_information": { - "...": "..." - } - "selected_challenge_uuid": "TXYKGE1SJZHJ4M2FKSV1P2RZVNTHZFB9E3A79QE956D3SCAWXPK0", - "challenge_feedback": { - "TXYKGE1SJZHJ4M2FKSV1P2RZVNTHZFB9E3A79QE956D3SCAWXPK0": { - "state": "body", - "body": "CROCKFORDBASE32ENCODEDBODY", - "http_status": 403, - "mime_type" : "anything/possible" - } - } - } + { + "recovery_state": "CHALLENGE_SOLVING", + "recovery_information": { + "...": "..." + } + "selected_challenge_uuid": "TXYKGE1SJZHJ4M2FKSV1P2RZVNTHZFB9E3A79QE956D3SCAWXPK0", + "challenge_feedback": { + "TXYKGE1SJZHJ4M2FKSV1P2RZVNTHZFB9E3A79QE956D3SCAWXPK0": { + "state": "body", + "body": "CROCKFORDBASE32ENCODEDBODY", + "http_status": 403, + "mime_type" : "anything/possible" + } + } + } - - **hint**: Here, the server provided human-readable hint for - how to solve the challenge. Note that the ``hint`` provided this - time is from the Anastasis provider and may differ from the ``instructions`` - for the challenge under ``recovery_information``: + - **hint**: Here, the server provided human-readable hint for + how to solve the challenge. Note that the ``hint`` provided this + time is from the Anastasis provider and may differ from the ``instructions`` + for the challenge under ``recovery_information``: - .. code-block:: json + .. code-block:: json - { - "recovery_state": "CHALLENGE_SOLVING", - "recovery_information": { - "...": "..." - } - "selected_challenge_uuid": "TXYKGE1SJZHJ4M2FKSV1P2RZVNTHZFB9E3A79QE956D3SCAWXPK0", - "challenge_feedback": { - "TXYKGE1SJZHJ4M2FKSV1P2RZVNTHZFB9E3A79QE956D3SCAWXPK0": { - "state": "hint", - "hint": "Recovery TAN send to email mail@DOMAIN", - "http_status": 403 - } + { + "recovery_state": "CHALLENGE_SOLVING", + "recovery_information": { + "...": "..." + } + "selected_challenge_uuid": "TXYKGE1SJZHJ4M2FKSV1P2RZVNTHZFB9E3A79QE956D3SCAWXPK0", + "challenge_feedback": { + "TXYKGE1SJZHJ4M2FKSV1P2RZVNTHZFB9E3A79QE956D3SCAWXPK0": { + "state": "hint", + "hint": "Recovery TAN send to email mail@DOMAIN", + "http_status": 403 + } + } + } + + - **details**: Here, the server provided a detailed JSON status response + related to solving the challenge: + + .. code-block:: json + + { + "recovery_state": "CHALLENGE_SOLVING", + "recovery_information": { + "...": "..." + } + "selected_challenge_uuid": "TXYKGE1SJZHJ4M2FKSV1P2RZVNTHZFB9E3A79QE956D3SCAWXPK0", + "challenge_feedback": { + "TXYKGE1SJZHJ4M2FKSV1P2RZVNTHZFB9E3A79QE956D3SCAWXPK0": { + "state": "details", + "details": { + "code": 8111, + "hint": "The client's response to the challenge was invalid.", + "detail" : null + }, + "http_status": 403 } } + } - - **details**: Here, the server provided a detailed JSON status response - related to solving the challenge: + - **redirect**: To solve the challenge, the user must visit the indicated + Web site at ``redirect_url``, for example to perform video authentication: - .. code-block:: json + .. code-block:: json - { - "recovery_state": "CHALLENGE_SOLVING", - "recovery_information": { - "...": "..." - } - "selected_challenge_uuid": "TXYKGE1SJZHJ4M2FKSV1P2RZVNTHZFB9E3A79QE956D3SCAWXPK0", - "challenge_feedback": { - "TXYKGE1SJZHJ4M2FKSV1P2RZVNTHZFB9E3A79QE956D3SCAWXPK0": { - "state": "details", - "details": { - "code": 8111, - "hint": "The client's response to the challenge was invalid.", - "detail" : null - }, - "http_status": 403 - } + { + "recovery_state": "CHALLENGE_SOLVING", + "recovery_information": { + "...": "..." + } + "selected_challenge_uuid": "TXYKGE1SJZHJ4M2FKSV1P2RZVNTHZFB9E3A79QE956D3SCAWXPK0", + "challenge_feedback": { + "TXYKGE1SJZHJ4M2FKSV1P2RZVNTHZFB9E3A79QE956D3SCAWXPK0": { + "state": "redirect", + "redirect_url": "https://videoconf.example.com/", + "http_status": 303 } } + } - - **redirect**: To solve the challenge, the user must visit the indicated - Web site at ``redirect_url``, for example to perform video authentication: + - **server-failure**: This indicates that the Anastasis provider encountered + a failure and recovery using this challenge cannot proceed at this time. + Examples for failures might be that the provider is unable to send SMS + messages at this time due to an outage. The body includes details about + the failure. The user may try again later or continue with other challenges. - .. code-block:: json + .. code-block:: json - { - "recovery_state": "CHALLENGE_SOLVING", - "recovery_information": { - "...": "..." - } - "selected_challenge_uuid": "TXYKGE1SJZHJ4M2FKSV1P2RZVNTHZFB9E3A79QE956D3SCAWXPK0", - "challenge_feedback": { - "TXYKGE1SJZHJ4M2FKSV1P2RZVNTHZFB9E3A79QE956D3SCAWXPK0": { - "state": "redirect", - "redirect_url": "https://videoconf.example.com/", - "http_status": 303 - } - } + { + "recovery_state": "CHALLENGE_SELECTING", + "recovery_information": { + "...": "..." } + "selected_challenge_uuid": "TXYKGE1SJZHJ4M2FKSV1P2RZVNTHZFB9E3A79QE956D3SCAWXPK0", + "challenge_feedback": { + "TXYKGE1SJZHJ4M2FKSV1P2RZVNTHZFB9E3A79QE956D3SCAWXPK0": { + "state": "server-failure", + "http_status": "500", + "error_code": 52 + } + } + } - - **server-failure**: This indicates that the Anastasis provider encountered - a failure and recovery using this challenge cannot proceed at this time. - Examples for failures might be that the provider is unable to send SMS - messages at this time due to an outage. The body includes details about - the failure. The user may try again later or continue with other challenges. + - **truth-unknown**: This indicates that the Anastasis provider is unaware of + the specified challenge. This is typically a permanent failure, and user + interfaces should not allow users to re-try this challenge. .. code-block:: json - { - "recovery_state": "CHALLENGE_SELECTING", - "recovery_information": { - "...": "..." - } - "selected_challenge_uuid": "TXYKGE1SJZHJ4M2FKSV1P2RZVNTHZFB9E3A79QE956D3SCAWXPK0", - "challenge_feedback": { - "TXYKGE1SJZHJ4M2FKSV1P2RZVNTHZFB9E3A79QE956D3SCAWXPK0": { - "state": "server-failure", - "http_status": "500", - "error_code": 52 - } - } - } + { + "recovery_state": "CHALLENGE_SELECTING", + "recovery_information": { + "...": "..." + } + "selected_challenge_uuid": "TXYKGE1SJZHJ4M2FKSV1P2RZVNTHZFB9E3A79QE956D3SCAWXPK0", + "challenge_feedback": { + "TXYKGE1SJZHJ4M2FKSV1P2RZVNTHZFB9E3A79QE956D3SCAWXPK0": { + "state": "truth-unknown", + "error_code": 8108 + } + } + } - - **truth-unknown**: This indicates that the Anastasis provider is unaware of - the specified challenge. This is typically a permanent failure, and user - interfaces should not allow users to re-try this challenge. + - **rate-limit-exceeded**: .. code-block:: json @@ -1585,13 +1603,13 @@ that applications must all handle. States other than ``solved`` are: "selected_challenge_uuid": "TXYKGE1SJZHJ4M2FKSV1P2RZVNTHZFB9E3A79QE956D3SCAWXPK0", "challenge_feedback": { "TXYKGE1SJZHJ4M2FKSV1P2RZVNTHZFB9E3A79QE956D3SCAWXPK0": { - "state": "truth-unknown", - "error_code": 8108 + "state": "rate-limit-exceeded", + "error_code": 8121 } } } - - **rate-limit-exceeded**: + - **authentication-timeout**: .. code-block:: json @@ -1603,12 +1621,24 @@ that applications must all handle. States other than ``solved`` are: "selected_challenge_uuid": "TXYKGE1SJZHJ4M2FKSV1P2RZVNTHZFB9E3A79QE956D3SCAWXPK0", "challenge_feedback": { "TXYKGE1SJZHJ4M2FKSV1P2RZVNTHZFB9E3A79QE956D3SCAWXPK0": { - "state": "rate-limit-exceeded", - "error_code": 8121 + "state": "authentication-timeout", + "error_code": 8122 } } } + +**poll:** + +With a ``poll`` transition, the application indicates that it wants to wait longer for one or more of the challenges that are in state ``authentication-timeout`` to possibly complete. While technically optional, the ``timeout`` argument should really be provided to enable long-polling, for example: + +.. code-block:: json + + { + "timeout" : { "d_ms" : 5000 }, + } + + **pay:** With a ``pay`` transition, the application indicates to the reducer that diff --git a/src/include/anastasis.h b/src/include/anastasis.h index 8443eb6..3027e2a 100644 --- a/src/include/anastasis.h +++ b/src/include/anastasis.h @@ -71,10 +71,16 @@ struct ANASTASIS_ChallengeDetails const char *instructions; /** - * true if challenged was already solved, else false. + * true if challenge was already solved, else false. */ bool solved; + /** + * true if challenge is awaiting asynchronous + * resolution by the user. + */ + bool async; + }; diff --git a/src/lib/anastasis_recovery.c b/src/lib/anastasis_recovery.c index 4e23db0..623e882 100644 --- a/src/lib/anastasis_recovery.c +++ b/src/lib/anastasis_recovery.c @@ -348,6 +348,7 @@ keyshare_lookup_cb (void *cls, = dd->details.server_failure.http_status }; + c->ci.async = true; c->af (c->af_cls, &csr); return; @@ -1043,7 +1044,9 @@ ANASTASIS_recovery_serialize (const struct ANASTASIS_Recovery *r) GNUNET_JSON_pack_string ("instructions", c->instructions), GNUNET_JSON_pack_bool ("solved", - c->ci.solved)); + c->ci.solved), + GNUNET_JSON_pack_bool ("async", + c->ci.async)); GNUNET_assert (0 == json_array_append_new (cs_arr, cs)); @@ -1118,6 +1121,9 @@ parse_cs_array (struct ANASTASIS_Recovery *r, &c->provider_salt), GNUNET_JSON_spec_string ("type", &escrow_type), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_bool ("async", + &c->ci.async)), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_fixed_auto ("key_share", &c->key_share)), diff --git a/src/reducer/anastasis_api_recovery_redux.c b/src/reducer/anastasis_api_recovery_redux.c index fab3c24..95632cc 100644 --- a/src/reducer/anastasis_api_recovery_redux.c +++ b/src/reducer/anastasis_api_recovery_redux.c @@ -151,6 +151,13 @@ struct SelectChallengeContext * Payment secret, if we are in the "pay" state. */ struct ANASTASIS_PaymentSecretP ps; + + /** + * Application asked us to only poll for existing + * asynchronous challenges, and not to being a + * new one. + */ + bool poll_only; }; @@ -741,7 +748,6 @@ solve_challenge_cb (void *cls, &ps), GNUNET_JSON_spec_end () }; - json_t *challenge; if (NULL == ri) @@ -769,6 +775,80 @@ solve_challenge_cb (void *cls, return; } + /* resume all async, unsolved challenges */ + { + bool poll_started = false; + + for (unsigned int i = 0; ics_len; i++) + { + struct ANASTASIS_Challenge *ci = ri->cs[i]; + const struct ANASTASIS_ChallengeDetails *cd; + json_t *challenge; + json_t *pin; + + cd = ANASTASIS_challenge_get_details (ci); + if (cd->solved || + (! cd->async) ) + continue; + + challenge = find_challenge_in_ri (sctx->state, + &cd->uuid); + if (NULL == challenge) + { + GNUNET_break_op (0); + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "challenge not found"); + sctx_free (sctx); + return; + } + pin = json_object_get (challenge, + "answer-pin"); + if (! json_is_integer (pin)) + { + GNUNET_break_op (0); + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "async challenge 'answer-pin' not found"); + sctx_free (sctx); + return; + } + if (GNUNET_OK != + ANASTASIS_challenge_answer2 (ci, + psp, + timeout, + json_integer_value (pin), + &answer_feedback_cb, + sctx)) + { + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "Failed to begin answering asynchronous challenge"); + sctx_free (sctx); + return; + } + poll_started = true; + } + + if (sctx->poll_only) + { + if (! poll_started) + { + GNUNET_break_op (0); + ANASTASIS_redux_fail_ (sctx->cb, + sctx->cb_cls, + TALER_EC_ANASTASIS_REDUCER_ACTION_INVALID, + "no challenge available for polling"); + return; + } + /* only polling, do not start new challenges */ + return; + } + } /* end resuming async challenges */ + /* Check if we got a payment_secret */ challenge = find_challenge_in_ri (sctx->state, &sctx->uuid); @@ -823,6 +903,7 @@ solve_challenge_cb (void *cls, psp = &ps; } + /* start or solve selected challenge */ for (unsigned int i = 0; ics_len; i++) { struct ANASTASIS_Challenge *ci = ri->cs[i]; @@ -830,6 +911,8 @@ solve_challenge_cb (void *cls, int ret; cd = ANASTASIS_challenge_get_details (ci); + if (cd->async) + continue; /* handled above */ if (0 != GNUNET_memcmp (&sctx->uuid, &cd->uuid)) @@ -883,6 +966,12 @@ solve_challenge_cb (void *cls, { uint64_t ianswer = json_integer_value (pin); + /* persist answer, in case async processing + happens via poll */ + GNUNET_assert (0 == + json_object_set (challenge, + "answer-pin", + pin)); ret = ANASTASIS_challenge_answer2 (ci, psp, timeout, @@ -1152,6 +1241,75 @@ solve_challenge (json_t *state, } +/** + * The user asked for us to poll on pending + * asynchronous challenges to see if they have + * now completed / been satisfied. + * + * @param[in] state we are in + * @param arguments our arguments with the solution + * @param cb functiont o call with the new state + * @param cb_cls closure for @a cb + * @return handle to cancel challenge selection step + */ +static struct ANASTASIS_ReduxAction * +poll_challenges (json_t *state, + const json_t *arguments, + ANASTASIS_ActionCallback cb, + void *cb_cls) +{ + struct SelectChallengeContext *sctx + = GNUNET_new (struct SelectChallengeContext); + json_t *rd; + + if (NULL == arguments) + { + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID, + "arguments missing"); + return NULL; + } + rd = json_object_get (state, + "recovery_document"); + if (NULL == rd) + { + GNUNET_break_op (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "poll_challenges"); + return NULL; + } + sctx->poll_only = true; + sctx->cb = cb; + sctx->cb_cls = cb_cls; + sctx->state = json_incref (state); + sctx->args = json_incref ((json_t*) arguments); + sctx->r = ANASTASIS_recovery_deserialize (ANASTASIS_REDUX_ctx_, + rd, + &solve_challenge_cb, + sctx, + &core_secret_cb, + sctx); + if (NULL == sctx->r) + { + json_decref (sctx->state); + json_decref (sctx->args); + GNUNET_free (sctx); + GNUNET_break_op (0); + ANASTASIS_redux_fail_ (cb, + cb_cls, + TALER_EC_ANASTASIS_REDUCER_STATE_INVALID, + "'recovery_document' invalid"); + return NULL; + } + sctx->ra.cleanup = &sctx_free; + sctx->ra.cleanup_cls = sctx; + return &sctx->ra; +} + + /** * The user selected a challenge to be solved. Handle the payment * process. @@ -1710,6 +1868,11 @@ ANASTASIS_recovery_action_ (json_t *state, "select_challenge", &select_challenge }, + { + ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING, + "poll", + &poll_challenges + }, { ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING, "back", -- cgit v1.2.3