merchant

Merchant backend to process payments, run by merchants
Log | Files | Refs | Submodules | README | LICENSE

commit 00cdf3f8ed2acd83208227aab90bf8652a837d44
parent 8ca128478cf6dd8524572bf4fb344abde24ea34e
Author: Christian Grothoff <christian@grothoff.org>
Date:   Sat, 20 Jun 2026 01:18:53 +0200

transform GET /private/kyc to use state machine, in preparation of fixing #11520

Diffstat:
Msrc/backend/taler-merchant-httpd_get-private-kyc.c | 1135++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
1 file changed, 633 insertions(+), 502 deletions(-)

diff --git a/src/backend/taler-merchant-httpd_get-private-kyc.c b/src/backend/taler-merchant-httpd_get-private-kyc.c @@ -184,11 +184,6 @@ struct KycContext struct TMH_HandlerContext *hc; /** - * Response to return, NULL if we don't have one yet. - */ - struct MHD_Response *response; - - /** * JSON array where we are building up the array with * pending KYC operations. */ @@ -271,11 +266,22 @@ struct KycContext enum TALER_EXCHANGE_KycLongPollTarget lpt; /** - * HTTP status code to use for the reply, i.e 200 for "OK". - * Special value UINT_MAX is used to indicate hard errors - * (no reply, return #MHD_NO). + * Processing phase. */ - unsigned int response_code; + enum + { + PHASE_INIT = 0, + PHASE_DETERMINE_LONG_POLL, + PHASE_DATABASE_KYC_CHECK, + PHASE_NO_ACCOUNTS, + PHASE_GENERATE_RESPONSE, + PHASE_IN_SHUTDOWN = 999, + PHASE_RETURN_YES, + PHASE_RETURN_NO, + PHASE_SUSPENDED_ON_ACCOUNT, + PHASE_SUSPENDED_ON_EXCHANGE, + } phase; + /** * Output format requested by the client. */ @@ -287,6 +293,13 @@ struct KycContext } format; /** + * Set to true if the database notified us about a change + * in the account but we did not yet check the database + * status as we were waiting on something else. + */ + bool account_signal; + + /** * True if @e h_wire was given. */ bool have_h_wire; @@ -321,6 +334,8 @@ static struct KycContext *kc_head; static struct KycContext *kc_tail; +/* ******************* cleanup ***************** */ + void TMH_force_kyc_resume () { @@ -331,6 +346,7 @@ TMH_force_kyc_resume () if (GNUNET_YES == kc->suspended) { kc->suspended = GNUNET_SYSERR; + kc->phase = PHASE_IN_SHUTDOWN; MHD_resume_connection (kc->connection); } } @@ -385,11 +401,6 @@ kyc_context_cleanup (void *cls) TALER_MERCHANTDB_event_listen_cancel (kc->eh); kc->eh = NULL; } - if (NULL != kc->response) - { - MHD_destroy_response (kc->response); - kc->response = NULL; - } GNUNET_CONTAINER_DLL_remove (kc_head, kc_tail, kc); @@ -399,172 +410,104 @@ kyc_context_cleanup (void *cls) /** - * We have found an exchange in status @a status. Clear any - * long-pollers that wait for us having (or not having) this - * status. + * Finish handling the connection returning @a ret to MHD * - * @param[in,out] kc context - * @param status the status we encountered + * @param[in,out] kc connection we are handling + * @param mhd_ret result to return for the @a kc request */ static void -clear_status (struct KycContext *kc, - const char *status) +finish_request (struct KycContext *kc, + enum MHD_Result mhd_ret) { - if ( (NULL != kc->lp_status) && - (0 == strcmp (kc->lp_status, - status)) ) - kc->lp_status = NULL; /* satisfied! */ - if ( (NULL != kc->lp_not_status) && - (0 != strcmp (kc->lp_not_status, - status) ) ) - kc->lp_not_status = NULL; /* satisfied! */ + kc->phase = (MHD_YES == mhd_ret) + ? PHASE_RETURN_YES + : PHASE_RETURN_NO; } +/* ******************* phase_init ***************** */ + + /** - * Resume the given KYC context and send the final response. Stores the - * response in the @a kc and signals MHD to resume the connection. Also - * ensures MHD runs immediately. + * Initialize basic data structures of the connection, + * finishes parsing the request. * - * @param kc KYC context + * @param[in,out] kc connection we are handling */ static void -resume_kyc_with_response (struct KycContext *kc) +phase_init (struct KycContext *kc) { - struct GNUNET_ShortHashCode sh; - bool not_modified; - char *can; + kc->kycs_data = json_array (); + GNUNET_assert (NULL != kc->kycs_data); + /* process 'exchange_url' argument */ + kc->exchange_url = MHD_lookup_connection_value ( + kc->connection, + MHD_GET_ARGUMENT_KIND, + "exchange_url"); + if ( (NULL != kc->exchange_url) && + ( (! TALER_url_valid_charset (kc->exchange_url)) || + (! TALER_is_web_url (kc->exchange_url)) ) ) + { + GNUNET_break_op (0); + finish_request (kc, + TALER_MHD_reply_with_error ( + kc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "exchange_url must be a valid HTTP(s) URL")); + } - if ( (! GNUNET_TIME_absolute_is_past (kc->timeout)) && - ( (NULL != kc->lp_not_status) || - (NULL != kc->lp_status) ) ) + /* Determine desired output format from Accept header */ { + const char *mime; + + mime = MHD_lookup_connection_value (kc->connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_ACCEPT); + if (NULL == mime) + mime = "application/json"; + if (0 == strcmp (mime, + "*/*")) + mime = "application/json"; GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Long-poll target status not reached, not returning response yet\n"); - if (GNUNET_NO == kc->suspended) + "KYC status requested for format %s\n", + mime); + if (0 == strcmp (mime, + "application/json")) { - MHD_suspend_connection (kc->connection); - kc->suspended = GNUNET_YES; + kc->format = POF_JSON; } - return; - } - can = TALER_JSON_canonicalize (kc->kycs_data); - GNUNET_assert (GNUNET_YES == - GNUNET_CRYPTO_hkdf_gnunet (&sh, - sizeof (sh), - "KYC-SALT", - strlen ("KYC-SALT"), - can, - strlen (can))); - not_modified = kc->have_lp_not_etag && - (0 == GNUNET_memcmp (&sh, - &kc->lp_not_etag)); - if (not_modified && - (! GNUNET_TIME_absolute_is_past (kc->timeout)) ) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Status unchanged, not returning response yet\n"); - if (GNUNET_NO == kc->suspended) + else if (0 == strcmp (mime, + "text/plain")) { - MHD_suspend_connection (kc->connection); - kc->suspended = GNUNET_YES; + kc->format = POF_TEXT; } - GNUNET_free (can); - return; - } - { - const char *inm; - - inm = MHD_lookup_connection_value (kc->connection, - MHD_GET_ARGUMENT_KIND, - MHD_HTTP_HEADER_IF_NONE_MATCH); - if ( (NULL == inm) || - ('"' != inm[0]) || - ('"' != inm[strlen (inm) - 1]) || - (0 != strncmp (inm + 1, - can, - strlen (can))) ) - not_modified = false; /* must return full response */ - } - GNUNET_free (can); - kc->response_code = not_modified - ? MHD_HTTP_NOT_MODIFIED - : MHD_HTTP_OK; - switch (kc->format) - { - case POF_JSON: - kc->response = TALER_MHD_MAKE_JSON_PACK ( - GNUNET_JSON_pack_array_incref ("kyc_data", - kc->kycs_data)); - break; - case POF_TEXT: +#if FUTURE + else if (0 == strcmp (mime, + "application/pdf")) { - enum GNUNET_GenericReturnValue ret; - json_t *obj; - - obj = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_array_incref ("kyc_data", - kc->kycs_data)); - ret = TALER_TEMPLATING_build (kc->connection, - &kc->response_code, - "kyc_text", - kc->mi->settings.id, - NULL, - obj, - &kc->response); - json_decref (obj); - if (GNUNET_SYSERR == ret) - { - /* fail hard */ - kc->suspended = GNUNET_SYSERR; - MHD_resume_connection (kc->connection); - TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */ - return; - } - if (GNUNET_OK == ret) - { - TALER_MHD_add_global_headers (kc->response, - false); - GNUNET_break (MHD_YES == - MHD_add_response_header (kc->response, - MHD_HTTP_HEADER_CONTENT_TYPE, - "text/plain")); - } + kc->format = POF_PDF; + } +#endif + else + { + GNUNET_break_op (0); + finish_request (kc, + TALER_MHD_REPLY_JSON_PACK ( + kc->connection, + MHD_HTTP_NOT_ACCEPTABLE, + GNUNET_JSON_pack_string ("hint", + mime))); + return; } - break; - case POF_PDF: - // not yet implemented - GNUNET_assert (0); - break; - } - { - char *etag; - char *qetag; - - etag = GNUNET_STRINGS_data_to_string_alloc (&sh, - sizeof (sh)); - GNUNET_asprintf (&qetag, - "\"%s\"", - etag); - GNUNET_break (MHD_YES == - MHD_add_response_header (kc->response, - MHD_HTTP_HEADER_ETAG, - qetag)); - GNUNET_free (qetag); - GNUNET_free (etag); - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Resuming /kyc handling as exchange interaction is done (%u)\n", - MHD_HTTP_OK); - if (GNUNET_YES == kc->suspended) - { - kc->suspended = GNUNET_NO; - MHD_resume_connection (kc->connection); - TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */ } + kc->phase++; } +/* ******************* phase_determine_long_poll ***************** */ + + /** * Handle a DB event about an update relevant * for the processing of the kyc request. @@ -580,132 +523,99 @@ kyc_change_cb (void *cls, { struct KycContext *kc = cls; - if (GNUNET_YES == kc->suspended) + if ( (GNUNET_YES == kc->suspended) && + (PHASE_SUSPENDED_ON_ACCOUNT == kc->phase) ) { GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Resuming KYC with gateway timeout\n"); kc->suspended = GNUNET_NO; + kc->phase = PHASE_DATABASE_KYC_CHECK; MHD_resume_connection (kc->connection); TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */ } + else + { + /* remember for later */ + kc->account_signal = true; + } } /** - * Pack the given @a limit into the JSON @a limits array. + * Suspend @a kc until we have a change in the account status. * - * @param kc overall request context - * @param limit account limit to pack - * @param[in,out] limits JSON array to extend + * @param[in,out] kc request to suspend */ static void -pack_limit (const struct KycContext *kc, - const struct TALER_EXCHANGE_AccountLimit *limit, - json_t *limits) +wait_for_account (struct KycContext *kc) { - json_t *jl; - - jl = GNUNET_JSON_PACK ( - TALER_JSON_pack_kycte ("operation_type", - limit->operation_type), - GNUNET_JSON_pack_bool ( - "disallowed", - GNUNET_TIME_relative_is_zero (limit->timeframe) || - TALER_amount_is_zero (&limit->threshold)), - (POF_TEXT == kc->format) - ? GNUNET_JSON_pack_string ("interval", - GNUNET_TIME_relative2s (limit->timeframe, - true)) - : GNUNET_JSON_pack_time_rel ("timeframe", - limit->timeframe), - TALER_JSON_pack_amount ("threshold", - &limit->threshold), - GNUNET_JSON_pack_bool ("soft_limit", - limit->soft_limit) - ); - GNUNET_assert (0 == - json_array_append_new (limits, - jl)); + GNUNET_assert (GNUNET_NO == kc->suspended); + if (kc->account_signal) + { + /* we got a NOTIFY earlier, handle it immediately */ + kc->account_signal = false; + kc->phase = PHASE_DATABASE_KYC_CHECK; + return; + } + /* Wait on account notification */ + MHD_suspend_connection (kc->connection); + kc->suspended = GNUNET_YES; + kc->phase = PHASE_SUSPENDED_ON_ACCOUNT; } /** - * Return JSON array with AccountLimit objects giving - * the current limits for this exchange. + * Setup long-polling for the connection, if applicable. * - * @param[in,out] ekr overall request context + * @param[in,out] kc connection we are handling */ -static json_t * -get_exchange_limits ( - struct ExchangeKycRequest *ekr) +static void +phase_determine_long_poll (struct KycContext *kc) { - const struct TALER_EXCHANGE_Keys *keys = ekr->keys; - json_t *limits; - - if (NULL != ekr->jlimits) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Returning custom KYC limits\n"); - return json_incref (ekr->jlimits); - } - if (NULL == keys) + if (GNUNET_TIME_absolute_is_past (kc->timeout)) { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "No keys, thus no default KYC limits known\n"); - return NULL; + kc->phase++; + return; } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Returning default KYC limits (%u/%u)\n", - keys->hard_limits_length, - keys->zero_limits_length); - limits = json_array (); - GNUNET_assert (NULL != limits); - for (unsigned int i = 0; i<keys->hard_limits_length; i++) + if (kc->have_h_wire) { - const struct TALER_EXCHANGE_AccountLimit *limit - = &keys->hard_limits[i]; + struct TALER_MERCHANTDB_MerchantKycStatusChangeEventP ev = { + .header.size = htons (sizeof (ev)), + .header.type = htons ( + TALER_DBEVENT_MERCHANT_EXCHANGE_KYC_STATUS_CHANGED + ), + .h_wire = kc->h_wire + }; - pack_limit (ekr->kc, - limit, - limits); + kc->eh = TALER_MERCHANTDB_event_listen ( + TMH_db, + &ev.header, + GNUNET_TIME_absolute_get_remaining (kc->timeout), + &kyc_change_cb, + kc); } - for (unsigned int i = 0; i<keys->zero_limits_length; i++) + else { - const struct TALER_EXCHANGE_ZeroLimitedOperation *zlimit - = &keys->zero_limits[i]; - json_t *jl; - struct TALER_Amount zero; + struct GNUNET_DB_EventHeaderP hdr = { + .size = htons (sizeof (hdr)), + .type = htons (TALER_DBEVENT_MERCHANT_KYC_STATUS_CHANGED) + }; - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (keys->currency, - &zero)); - jl = GNUNET_JSON_PACK ( - TALER_JSON_pack_kycte ("operation_type", - zlimit->operation_type), - GNUNET_JSON_pack_bool ( - "disallowed", - true), - (POF_TEXT == ekr->kc->format) - ? GNUNET_JSON_pack_string ( - "interval", - GNUNET_TIME_relative2s (GNUNET_TIME_UNIT_ZERO, - true)) - : GNUNET_JSON_pack_time_rel ("timeframe", - GNUNET_TIME_UNIT_ZERO), - TALER_JSON_pack_amount ("threshold", - &zero), - GNUNET_JSON_pack_bool ("soft_limit", - true) - ); - GNUNET_assert (0 == - json_array_append_new (limits, - jl)); + kc->eh = TALER_MERCHANTDB_event_listen ( + TMH_db, + &hdr, + GNUNET_TIME_absolute_get_remaining (kc->timeout), + &kyc_change_cb, + kc); } - return limits; + kc->phase++; } -/** +/* ***************** phase_database_kyc_check ************** */ + + +/** * Maps @a ekr to a status code for clients to interpret the * overall result. * @@ -847,6 +757,143 @@ map_to_status (const struct ExchangeKycRequest *ekr) /** + * We have found an exchange in status @a status. Clear any + * long-pollers that wait for us having (or not having) this + * status. + * + * @param[in,out] kc context + * @param status the status we encountered + */ +static void +clear_status (struct KycContext *kc, + const char *status) +{ + if ( (NULL != kc->lp_status) && + (0 == strcmp (kc->lp_status, + status)) ) + kc->lp_status = NULL; /* satisfied! */ + if ( (NULL != kc->lp_not_status) && + (0 != strcmp (kc->lp_not_status, + status) ) ) + kc->lp_not_status = NULL; /* satisfied! */ +} + + +/** + * Pack the given @a limit into the JSON @a limits array. + * + * @param kc overall request context + * @param limit account limit to pack + * @param[in,out] limits JSON array to extend + */ +static void +pack_limit (const struct KycContext *kc, + const struct TALER_EXCHANGE_AccountLimit *limit, + json_t *limits) +{ + json_t *jl; + + jl = GNUNET_JSON_PACK ( + TALER_JSON_pack_kycte ("operation_type", + limit->operation_type), + GNUNET_JSON_pack_bool ( + "disallowed", + GNUNET_TIME_relative_is_zero (limit->timeframe) || + TALER_amount_is_zero (&limit->threshold)), + (POF_TEXT == kc->format) + ? GNUNET_JSON_pack_string ("interval", + GNUNET_TIME_relative2s (limit->timeframe, + true)) + : GNUNET_JSON_pack_time_rel ("timeframe", + limit->timeframe), + TALER_JSON_pack_amount ("threshold", + &limit->threshold), + GNUNET_JSON_pack_bool ("soft_limit", + limit->soft_limit) + ); + GNUNET_assert (0 == + json_array_append_new (limits, + jl)); +} + + +/** + * Return JSON array with AccountLimit objects giving + * the current limits for this exchange. + * + * @param[in,out] ekr overall request context + */ +static json_t * +get_exchange_limits ( + struct ExchangeKycRequest *ekr) +{ + const struct TALER_EXCHANGE_Keys *keys = ekr->keys; + json_t *limits; + + if (NULL != ekr->jlimits) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Returning custom KYC limits\n"); + return json_incref (ekr->jlimits); + } + if (NULL == keys) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "No keys, thus no default KYC limits known\n"); + return NULL; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Returning default KYC limits (%u/%u)\n", + keys->hard_limits_length, + keys->zero_limits_length); + limits = json_array (); + GNUNET_assert (NULL != limits); + for (unsigned int i = 0; i<keys->hard_limits_length; i++) + { + const struct TALER_EXCHANGE_AccountLimit *limit + = &keys->hard_limits[i]; + + pack_limit (ekr->kc, + limit, + limits); + } + for (unsigned int i = 0; i<keys->zero_limits_length; i++) + { + const struct TALER_EXCHANGE_ZeroLimitedOperation *zlimit + = &keys->zero_limits[i]; + json_t *jl; + struct TALER_Amount zero; + + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (keys->currency, + &zero)); + jl = GNUNET_JSON_PACK ( + TALER_JSON_pack_kycte ("operation_type", + zlimit->operation_type), + GNUNET_JSON_pack_bool ( + "disallowed", + true), + (POF_TEXT == ekr->kc->format) + ? GNUNET_JSON_pack_string ( + "interval", + GNUNET_TIME_relative2s (GNUNET_TIME_UNIT_ZERO, + true)) + : GNUNET_JSON_pack_time_rel ("timeframe", + GNUNET_TIME_UNIT_ZERO), + TALER_JSON_pack_amount ("threshold", + &zero), + GNUNET_JSON_pack_bool ("soft_limit", + true) + ); + GNUNET_assert (0 == + json_array_append_new (limits, + jl)); + } + return limits; +} + + +/** * Take data from @a ekr to expand our response. * * @param ekr exchange we are done inspecting @@ -974,40 +1021,6 @@ ekr_expand_response (struct ExchangeKycRequest *ekr) /** - * We are done with asynchronous processing, generate the - * response for the @e kc. - * - * @param[in,out] kc KYC context to respond for - */ -static void -kc_respond (struct KycContext *kc) -{ - if ( (! kc->return_immediately) && - (! GNUNET_TIME_absolute_is_past (kc->timeout)) ) - { - if (GNUNET_NO == kc->suspended) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Suspending: long poll target %d not reached\n", - kc->lpt); - MHD_suspend_connection (kc->connection); - kc->suspended = GNUNET_YES; - } - else - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Remaining suspended: long poll target %d not reached\n", - kc->lpt); - } - return; - } - /* All exchange requests done, create final - big response from cumulated replies */ - resume_kyc_with_response (kc); -} - - -/** * We are done with the KYC request @a ekr. Remove it from the work list and * check if we are done overall. * @@ -1024,7 +1037,11 @@ ekr_finished (struct ExchangeKycRequest *ekr) return; /* wait for more */ if (kc->in_db) return; - kc_respond (kc); + GNUNET_assert (GNUNET_YES == kc->suspended); + kc->phase = PHASE_GENERATE_RESPONSE; + kc->suspended = GNUNET_NO; + MHD_resume_connection (kc->connection); + TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */ } @@ -1046,8 +1063,6 @@ determine_eligible_accounts ( char *merchant_pub_str; struct TALER_NormalizedPayto np; - ekr->pkaa = json_array (); - GNUNET_assert (NULL != ekr->pkaa); { const struct TALER_EXCHANGE_GlobalFee *gf; @@ -1159,6 +1174,8 @@ kyc_with_exchange (void *cls, ekr->keys = TALER_EXCHANGE_keys_incref (keys); if (! ekr->auth_ok) { + ekr->pkaa = json_array (); + GNUNET_assert (NULL != ekr->pkaa); determine_eligible_accounts (ekr); if (0 == json_array_size (ekr->pkaa)) { @@ -1198,6 +1215,7 @@ struct UnreachableContext }; + /** * Add all trusted exchanges with "unknown" status for the * bank account given in the context. @@ -1349,13 +1367,7 @@ kyc_status_cb ( GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Awaiting /keys from `%s'\n", exchange_url); - /* Figure out wire transfer instructions */ - if (GNUNET_NO == kc->suspended) - { - MHD_suspend_connection (kc->connection); - kc->suspended = GNUNET_YES; - } ekr->fo = TMH_EXCHANGES_keys4exchange ( exchange_url, false, @@ -1375,6 +1387,302 @@ kyc_status_cb ( /** + * Check our database for the KYC status. Determines if we then + * need to wait on exchange data or have no exchange and can + * immediately proceed to return 204. + * + * @param[in,out] kc connection we are handling + */ +static void +phase_database_kyc_check (struct KycContext *kc) +{ + enum GNUNET_DB_QueryStatus qs; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Checking KYC status for %s (%d/%s)\n", + kc->mi->settings.id, + kc->have_h_wire, + kc->exchange_url); + /* We may run repeatedly due to long-polling; clear data + from previous runs first */ + GNUNET_break (0 == + json_array_clear (kc->kycs_data)); + kc->in_db = true; + qs = TALER_MERCHANTDB_account_kyc_get_status ( + TMH_db, + kc->mi->settings.id, + kc->have_h_wire + ? &kc->h_wire + : NULL, + kc->exchange_url, + &kyc_status_cb, + kc); + kc->in_db = false; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "account_kyc_get_status returned %d records\n", + (int) qs); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + /* Database error */ + GNUNET_break (0); + finish_request (kc, + TALER_MHD_reply_with_ec ( + kc->connection, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "account_kyc_get_status")); + return; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + kc->phase = PHASE_NO_ACCOUNTS; + return; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + /* Handled below */ + break; + } + if (NULL == kc->exchange_pending_head) + { + kc->phase = PHASE_GENERATE_RESPONSE; + return; + } + MHD_suspend_connection (kc->connection); + kc->suspended = GNUNET_YES; + kc->phase = PHASE_SUSPENDED_ON_EXCHANGE; +} + + +/* ********************* phase_no_accounts *********** */ + +/** + * We have no accounts, return a 204 No content, + * or suspend if long-polling. + * + * @param[in,out] kc connection we are handling + */ +static void +phase_no_accounts (struct KycContext *kc) +{ + /* We use an Etag of all zeros for the 204 status code */ + static struct GNUNET_ShortHashCode zero_etag; + struct MHD_Response *response; + + /* no matching accounts, could not have suspended */ + GNUNET_assert (GNUNET_NO == kc->suspended); + if (kc->have_lp_not_etag && + (0 == GNUNET_memcmp (&zero_etag, + &kc->lp_not_etag)) && + (! GNUNET_TIME_absolute_is_past (kc->timeout)) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "No matching accounts, suspending to wait for this to change\n"); + MHD_suspend_connection (kc->connection); + kc->suspended = GNUNET_YES; + kc->phase = PHASE_SUSPENDED_ON_ACCOUNT; + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "No matching accounts, returning empty response\n"); + response = MHD_create_response_from_buffer_static (0, + NULL); + TALER_MHD_add_global_headers (response, + false); + { + char *etag; + + etag = GNUNET_STRINGS_data_to_string_alloc (&zero_etag, + sizeof (zero_etag)); + GNUNET_break (MHD_YES == + MHD_add_response_header (response, + MHD_HTTP_HEADER_ETAG, + etag)); + GNUNET_free (etag); + } + finish_request (kc, + MHD_queue_response (kc->connection, + MHD_HTTP_NO_CONTENT, + response)); + MHD_destroy_response (response); +} + + +/* ********************* phase_generate_response *********** */ + +/** + * Resume the given KYC context and send the final response. Stores the + * response in the @a kc and signals MHD to resume the connection. Also + * ensures MHD runs immediately. + * + * @param kc KYC context + */ +static void +resume_kyc_with_response (struct KycContext *kc) +{ + struct GNUNET_ShortHashCode sh; + bool not_modified; + char *can; + unsigned int response_code; + struct MHD_Response *response; + + can = TALER_JSON_canonicalize (kc->kycs_data); + GNUNET_assert (GNUNET_YES == + GNUNET_CRYPTO_hkdf_gnunet (&sh, + sizeof (sh), + "KYC-SALT", + strlen ("KYC-SALT"), + can, + strlen (can))); + not_modified = kc->have_lp_not_etag && + (0 == GNUNET_memcmp (&sh, + &kc->lp_not_etag)); + if (not_modified && + (! GNUNET_TIME_absolute_is_past (kc->timeout)) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Status unchanged, not returning response yet\n"); + wait_for_account (kc); + GNUNET_free (can); + return; + } + { + const char *inm; + + inm = MHD_lookup_connection_value (kc->connection, + MHD_GET_ARGUMENT_KIND, + MHD_HTTP_HEADER_IF_NONE_MATCH); + if ( (NULL == inm) || + ('"' != inm[0]) || + ('"' != inm[strlen (inm) - 1]) || + (0 != strncmp (inm + 1, + can, + strlen (can))) ) + not_modified = false; /* must return full response */ + } + GNUNET_free (can); + response_code = not_modified + ? MHD_HTTP_NOT_MODIFIED + : MHD_HTTP_OK; + switch (kc->format) + { + case POF_JSON: + response = TALER_MHD_MAKE_JSON_PACK ( + GNUNET_JSON_pack_array_incref ("kyc_data", + kc->kycs_data)); + break; + case POF_TEXT: + { + enum GNUNET_GenericReturnValue ret; + json_t *obj; + + obj = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_array_incref ("kyc_data", + kc->kycs_data)); + ret = TALER_TEMPLATING_build (kc->connection, + &response_code, + "kyc_text", + kc->mi->settings.id, + NULL, + obj, + &response); + json_decref (obj); + switch (ret) + { + case GNUNET_SYSERR: + /* failed to even produce a response */ + GNUNET_break (0); + kc->phase = PHASE_RETURN_NO; + return; + case GNUNET_NO: + finish_request (kc, + MHD_queue_response ( + kc->connection, + response_code, + response)); + MHD_destroy_response (response); + return; + case GNUNET_OK: + TALER_MHD_add_global_headers (response, + false); + GNUNET_break (MHD_YES == + MHD_add_response_header (response, + MHD_HTTP_HEADER_CONTENT_TYPE, + "text/plain")); + break; + } /* switch (ret) */ + } + break; + case POF_PDF: + // not yet implemented + GNUNET_assert (0); + break; + } + { + char *etag; + char *qetag; + + etag = GNUNET_STRINGS_data_to_string_alloc (&sh, + sizeof (sh)); + GNUNET_asprintf (&qetag, + "\"%s\"", + etag); + GNUNET_break (MHD_YES == + MHD_add_response_header (response, + MHD_HTTP_HEADER_ETAG, + qetag)); + GNUNET_free (qetag); + GNUNET_free (etag); + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Resuming /kyc handling as exchange interaction is done (%u)\n", + MHD_HTTP_OK); + finish_request (kc, + MHD_queue_response ( + kc->connection, + response_code, + response)); + MHD_destroy_response (response); +} + + +/** + * We are done with asynchronous processing, generate the + * response for the @e kc. + * + * @param[in,out] kc KYC context to respond for + */ +static void +phase_generate_response (struct KycContext *kc) +{ + GNUNET_assert (NULL == kc->exchange_pending_head); + GNUNET_assert (GNUNET_NO == kc->suspended); + /* FIXME: mixing these two suspend conditions like this + does not seem sane */ + if ( (! kc->return_immediately) && + (! GNUNET_TIME_absolute_is_past (kc->timeout)) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Suspending: long poll target %d not reached\n", + kc->lpt); + wait_for_account (kc); + return; + } + if ( (! GNUNET_TIME_absolute_is_past (kc->timeout)) && + ( (NULL != kc->lp_not_status) || + (NULL != kc->lp_status) ) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Long-poll target status not reached, not returning response yet\n"); + wait_for_account (kc); + return; + } + /* All exchange requests done, create final + big response from cumulated replies */ + resume_kyc_with_response (kc); +} + + +/* ******************* main logic ***************** */ + +/** * Check the KYC status of an instance. * * @param mi instance to check KYC status of @@ -1401,8 +1709,6 @@ get_instances_ID_kyc ( kc); kc->connection = connection; kc->hc = hc; - kc->kycs_data = json_array (); - GNUNET_assert (NULL != kc->kycs_data); TALER_MHD_parse_request_timeout (connection, &kc->timeout); { @@ -1425,22 +1731,6 @@ get_instances_ID_kyc ( } kc->return_immediately = (TALER_EXCHANGE_KLPT_NONE == kc->lpt); - /* process 'exchange_url' argument */ - kc->exchange_url = MHD_lookup_connection_value ( - connection, - MHD_GET_ARGUMENT_KIND, - "exchange_url"); - if ( (NULL != kc->exchange_url) && - ( (! TALER_url_valid_charset (kc->exchange_url)) || - (! TALER_is_web_url (kc->exchange_url)) ) ) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "exchange_url must be a valid HTTP(s) URL"); - } kc->lp_status = MHD_lookup_connection_value ( connection, MHD_GET_ARGUMENT_KIND, @@ -1457,203 +1747,44 @@ get_instances_ID_kyc ( "lp_not_etag", &kc->lp_not_etag, kc->have_lp_not_etag); - - /* Determine desired output format from Accept header */ - { - const char *mime; - - mime = MHD_lookup_connection_value (connection, - MHD_HEADER_KIND, - MHD_HTTP_HEADER_ACCEPT); - if (NULL == mime) - mime = "application/json"; - if (0 == strcmp (mime, - "*/*")) - mime = "application/json"; - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "KYC status requested for format %s\n", - mime); - if (0 == strcmp (mime, - "application/json")) - { - kc->format = POF_JSON; - } - else if (0 == strcmp (mime, - "text/plain")) - { - kc->format = POF_TEXT; - } -#if FUTURE - else if (0 == strcmp (mime, - "application/pdf")) - { - kc->format = POF_PDF; - } -#endif - else - { - GNUNET_break_op (0); - return TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_NOT_ACCEPTABLE, - GNUNET_JSON_pack_string ("hint", - mime)); - } - } - - if (! GNUNET_TIME_absolute_is_past (kc->timeout)) - { - if (kc->have_h_wire) - { - struct TALER_MERCHANTDB_MerchantKycStatusChangeEventP ev = { - .header.size = htons (sizeof (ev)), - .header.type = htons ( - TALER_DBEVENT_MERCHANT_EXCHANGE_KYC_STATUS_CHANGED - ), - .h_wire = kc->h_wire - }; - - kc->eh = TALER_MERCHANTDB_event_listen ( - TMH_db, - &ev.header, - GNUNET_TIME_absolute_get_remaining (kc->timeout), - &kyc_change_cb, - kc); - } - else - { - struct GNUNET_DB_EventHeaderP hdr = { - .size = htons (sizeof (hdr)), - .type = htons (TALER_DBEVENT_MERCHANT_KYC_STATUS_CHANGED) - }; - - kc->eh = TALER_MERCHANTDB_event_listen ( - TMH_db, - &hdr, - GNUNET_TIME_absolute_get_remaining (kc->timeout), - &kyc_change_cb, - kc); - } - } /* end register LISTEN hooks */ - } /* end 1st time initialization */ - - if (GNUNET_SYSERR == kc->suspended) - return MHD_NO; /* during shutdown, we don't generate any more replies */ - GNUNET_assert (GNUNET_NO == kc->suspended); - - if (NULL != kc->response) - return MHD_queue_response (connection, - kc->response_code, - kc->response); - - /* Check our database */ + } + while (1) { - enum GNUNET_DB_QueryStatus qs; - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Checking KYC status for %s (%d/%s)\n", - mi->settings.id, - kc->have_h_wire, - kc->exchange_url); - /* We may run repeatedly due to long-polling; clear data - from previous runs first */ - GNUNET_break (0 == - json_array_clear (kc->kycs_data)); - kc->in_db = true; - qs = TALER_MERCHANTDB_account_kyc_get_status ( - TMH_db, - mi->settings.id, - kc->have_h_wire - ? &kc->h_wire - : NULL, - kc->exchange_url, - &kyc_status_cb, - kc); - kc->in_db = false; - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "account_kyc_get_status returned %d records\n", - (int) qs); - switch (qs) + switch (kc->phase) { - case GNUNET_DB_STATUS_HARD_ERROR: - case GNUNET_DB_STATUS_SOFT_ERROR: - /* Database error */ - GNUNET_break (0); - if (GNUNET_YES == kc->suspended) - { - /* must have suspended before DB error, resume! */ - MHD_resume_connection (connection); - kc->suspended = GNUNET_NO; - } - return TALER_MHD_reply_with_ec ( - connection, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "account_kyc_get_status"); - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - { - /* We use an Etag of all zeros for the 204 status code */ - static struct GNUNET_ShortHashCode zero_etag; - - /* no matching accounts, could not have suspended */ - GNUNET_assert (GNUNET_NO == kc->suspended); - if (kc->have_lp_not_etag && - (0 == GNUNET_memcmp (&zero_etag, - &kc->lp_not_etag)) && - (! GNUNET_TIME_absolute_is_past (kc->timeout)) ) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "No matching accounts, suspending to wait for this to change\n"); - MHD_suspend_connection (kc->connection); - kc->suspended = GNUNET_YES; - return MHD_YES; - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "No matching accounts, returning empty response\n"); - kc->response_code = MHD_HTTP_NO_CONTENT; - kc->response = MHD_create_response_from_buffer_static (0, - NULL); - TALER_MHD_add_global_headers (kc->response, - false); - { - char *etag; - - etag = GNUNET_STRINGS_data_to_string_alloc (&zero_etag, - sizeof (zero_etag)); - GNUNET_break (MHD_YES == - MHD_add_response_header (kc->response, - MHD_HTTP_HEADER_ETAG, - etag)); - GNUNET_free (etag); - } - return MHD_queue_response (connection, - kc->response_code, - kc->response); - } - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + case PHASE_INIT: + phase_init (kc); break; - } /* end switch (qs) */ - } - - /* normal case, but maybe no async activity? In this case, - respond immediately */ - if (NULL == kc->exchange_pending_head) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "No asynchronous activity, responding now\n"); - kc_respond (kc); - } - if (GNUNET_YES == kc->suspended) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Request handling suspended, waiting for KYC status change\n"); - return MHD_YES; + case PHASE_DETERMINE_LONG_POLL: + phase_determine_long_poll (kc); + break; + case PHASE_DATABASE_KYC_CHECK: + phase_database_kyc_check (kc); + break; + case PHASE_NO_ACCOUNTS: + phase_no_accounts (kc); + break; + case PHASE_GENERATE_RESPONSE: + phase_generate_response (kc); + break; + case PHASE_IN_SHUTDOWN: + /* during shutdown, we don't generate any more replies */ + GNUNET_assert (GNUNET_SYSERR == kc->suspended); + return MHD_NO; + case PHASE_RETURN_YES: + return MHD_YES; + case PHASE_RETURN_NO: + return MHD_NO; + case PHASE_SUSPENDED_ON_ACCOUNT: + /* suspended */ + GNUNET_assert (GNUNET_YES == kc->suspended); + return MHD_YES; + case PHASE_SUSPENDED_ON_EXCHANGE: + /* suspended */ + GNUNET_assert (GNUNET_YES == kc->suspended); + return MHD_YES; + } } - - /* Should have generated a response */ - GNUNET_break (NULL != kc->response); - return MHD_queue_response (connection, - kc->response_code, - kc->response); }