/* This file is part of TALER Copyright (C) 2021 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. TALER is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with TALER; see the file COPYING. If not, see */ /** * @file taler-exchange-httpd_kyc-proof.c * @brief Handle request for proof for KYC check. * @author Christian Grothoff */ #include "platform.h" #include #include #include #include #include #include "taler_json_lib.h" #include "taler_mhd_lib.h" #include "taler-exchange-httpd_kyc-proof.h" #include "taler-exchange-httpd_responses.h" /** * Context for the proof. */ struct KycProofContext { /** * Kept in a DLL while suspended. */ struct KycProofContext *next; /** * Kept in a DLL while suspended. */ struct KycProofContext *prev; /** * Details about the connection we are processing. */ struct TEH_RequestContext *rc; /** * Handle for the OAuth 2.0 CURL request. */ struct GNUNET_CURL_Job *job; /** * OAuth 2.0 authorization code. */ const char *authorization_code; /** * OAuth 2.0 token URL we are using for the * request. */ char *token_url; /** * Body of the POST request. */ char *post_body; /** * Payment target this is about. */ unsigned long long payment_target_uuid; /** * HTTP response to return. */ struct MHD_Response *response; /** * HTTP response code to return. */ unsigned int response_code; /** * #GNUNET_YES if we are suspended, * #GNUNET_NO if not. * #GNUNET_SYSERR if we had some error. */ enum GNUNET_GenericReturnValue suspended; }; /** * Contexts are kept in a DLL while suspended. */ static struct KycProofContext *kpc_head; /** * Contexts are kept in a DLL while suspended. */ static struct KycProofContext *kpc_tail; void TEH_kyc_proof_cleanup (void) { struct KycProofContext *kpc; while (NULL != (kpc = kpc_head)) { if (NULL != kpc->job) { GNUNET_CURL_job_cancel (kpc->job); kpc->job = NULL; } GNUNET_CONTAINER_DLL_remove (kpc_head, kpc_tail, kpc); kpc->suspended = GNUNET_NO; MHD_resume_connection (kpc->rc->connection); } } /** * Function implementing database transaction to check proof's KYC status. * Runs the transaction logic; IF it returns a non-error code, the transaction * logic MUST NOT queue a MHD response. IF it returns an hard error, the * transaction logic MUST queue a MHD response and set @a mhd_ret. IF it * returns the soft error code, the function MAY be called again to retry and * MUST not queue a MHD response. * * @param cls closure with a `struct KycProofContext *` * @param connection MHD proof which triggered the transaction * @param[out] mhd_ret set to MHD response status for @a connection, * if transaction failed (!) * @return transaction status */ static enum GNUNET_DB_QueryStatus persist_kyc_ok (void *cls, struct MHD_Connection *connection, MHD_RESULT *mhd_ret) { struct KycProofContext *kpc = cls; return TEH_plugin->set_kyc_ok (TEH_plugin->cls, kpc->payment_target_uuid); } /** * After we are done with the CURL interaction we * need to update our database state. * * @param cls our `struct KycProofContext` * @param response_code HTTP response code from server, 0 on hard error * @param response in JSON, NULL if response was not in JSON format */ static void handle_curl_login_finished (void *cls, long response_code, const void *response) { struct KycProofContext *kpc = cls; const json_t *j = response; kpc->job = NULL; switch (response_code) { case MHD_HTTP_OK: { const char *access_token; const char *token_type; uint64_t expires_in_s; const char *refresh_token; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_string ("access_token", &access_token), GNUNET_JSON_spec_string ("token_type", &token_type), GNUNET_JSON_spec_uint64 ("expires_in", &expires_in_s), GNUNET_JSON_spec_string ("refresh_token", &refresh_token), GNUNET_JSON_spec_end () }; { enum GNUNET_GenericReturnValue res; const char *emsg; unsigned int line; res = GNUNET_JSON_parse (j, spec, &emsg, &line); if (GNUNET_OK != res) { GNUNET_break_op (0); kpc->response = TALER_MHD_make_error ( TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE, "Unexpected response from KYC gateway"); kpc->response_code = MHD_HTTP_BAD_GATEWAY; MHD_resume_connection (kpc->rc->connection); TALER_MHD_daemon_trigger (); return; } } if (0 != strcasecmp (token_type, "bearer")) { GNUNET_break_op (0); kpc->response = TALER_MHD_make_error ( TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE, "Unexpected token type in response from KYC gateway"); kpc->response_code = MHD_HTTP_BAD_GATEWAY; MHD_resume_connection (kpc->rc->connection); TALER_MHD_daemon_trigger (); return; } /* TODO: Here we might want to keep something to persist in the DB, and possibly use the access_token to download information we should persist; then continue! */ MHD_resume_connection (kpc->rc->connection); TALER_MHD_daemon_trigger (); return; } default: { const char *msg; const char *desc; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_string ("error", &msg), GNUNET_JSON_spec_string ("error_description", &desc), GNUNET_JSON_spec_end () }; { enum GNUNET_GenericReturnValue res; const char *emsg; unsigned int line; res = GNUNET_JSON_parse (j, spec, &emsg, &line); if (GNUNET_OK != res) { GNUNET_break_op (0); kpc->response = TALER_MHD_make_error ( TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE, "Unexpected response from KYC gateway"); kpc->response_code = MHD_HTTP_BAD_GATEWAY; MHD_resume_connection (kpc->rc->connection); TALER_MHD_daemon_trigger (); return; } } /* case TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_AUTHORZATION_FAILED, we MAY want to in the future look at the requested content type and possibly respond in JSON if indicated. */ { char *reply; GNUNET_asprintf (&reply, "%s

%s

%s

", msg, msg, desc); kpc->response = MHD_create_response_from_buffer (strlen (reply), reply, MHD_RESPMEM_MUST_COPY); GNUNET_assert (NULL != kpc->response); GNUNET_free (reply); } kpc->response_code = MHD_HTTP_FORBIDDEN; MHD_resume_connection (kpc->rc->connection); TALER_MHD_daemon_trigger (); } break; } } /** * Function called to clean up a context. * * @param rc request context */ static void clean_kpc (struct TEH_RequestContext *rc) { struct KycProofContext *kpc = rc->rh_ctx; if (NULL != kpc->job) { GNUNET_CURL_job_cancel (kpc->job); kpc->job = NULL; } if (NULL != kpc->response) { MHD_destroy_response (kpc->response); kpc->response = NULL; } GNUNET_free (kpc->post_body); GNUNET_free (kpc->token_url); GNUNET_free (kpc); } MHD_RESULT TEH_handler_kyc_proof ( struct TEH_RequestContext *rc, const char *const args[]) { struct KycProofContext *kpc = rc->rh_ctx; if (NULL == kpc) { /* first time */ char dummy; kpc = GNUNET_new (struct KycProofContext); kpc->rc = rc; rc->rh_ctx = kpc; rc->rh_cleaner = &clean_kpc; if (1 != sscanf (args[0], "%llu%c", &kpc->payment_target_uuid, &dummy)) { GNUNET_break_op (0); return TALER_MHD_reply_with_error (rc->connection, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_PARAMETER_MALFORMED, "payment_target_uuid"); } kpc->authorization_code = MHD_lookup_connection_value (rc->connection, MHD_GET_ARGUMENT_KIND, "code"); if (NULL == kpc->authorization_code) { GNUNET_break_op (0); return TALER_MHD_reply_with_error (rc->connection, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_PARAMETER_MALFORMED, "code"); } if (TEH_KYC_NONE == TEH_kyc_config.mode) return TALER_MHD_reply_static ( rc->connection, MHD_HTTP_NO_CONTENT, NULL, NULL, 0); { CURL *eh; eh = curl_easy_init (); if (NULL == eh) { GNUNET_break (0); return TALER_MHD_reply_with_error (rc->connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_ALLOCATION_FAILURE, "curl_easy_init"); } GNUNET_asprintf (&kpc->token_url, "%s/token", TEH_kyc_config.details.oauth2.url); GNUNET_assert (CURLE_OK == curl_easy_setopt (eh, CURLOPT_URL, kpc->token_url)); GNUNET_assert (CURLE_OK == curl_easy_setopt (eh, CURLOPT_POST, 1)); { char *client_id; char *redirect_uri; char *client_secret; char *authorization_code; client_id = curl_easy_escape (eh, TEH_kyc_config.details.oauth2.client_id, 0); GNUNET_assert (NULL != client_id); { char *request_uri; GNUNET_asprintf (&request_uri, "%s/login?client_id=%s", TEH_kyc_config.details.oauth2.url, TEH_kyc_config.details.oauth2.client_id); redirect_uri = curl_easy_escape (eh, request_uri, 0); GNUNET_free (request_uri); } GNUNET_assert (NULL != redirect_uri); client_secret = curl_easy_escape (eh, TEH_kyc_config.details.oauth2. client_secret, 0); GNUNET_assert (NULL != client_secret); authorization_code = curl_easy_escape (eh, kpc->authorization_code, 0); GNUNET_assert (NULL != authorization_code); GNUNET_asprintf (&kpc->post_body, "client_id=%s&redirect_uri=%s&client_secret=%s&code=%s&grant_type=authorization_code", client_id, redirect_uri, client_secret, authorization_code); curl_free (authorization_code); curl_free (client_secret); curl_free (redirect_uri); curl_free (client_id); } GNUNET_assert (CURLE_OK == curl_easy_setopt (eh, CURLOPT_POSTFIELDS, kpc->post_body)); GNUNET_assert (CURLE_OK == curl_easy_setopt (eh, CURLOPT_FOLLOWLOCATION, 1L)); /* limit MAXREDIRS to 5 as a simple security measure against a potential infinite loop caused by a malicious target */ GNUNET_assert (CURLE_OK == curl_easy_setopt (eh, CURLOPT_MAXREDIRS, 5L)); kpc->job = GNUNET_CURL_job_add (TEH_curl_ctx, eh, &handle_curl_login_finished, kpc); kpc->suspended = GNUNET_YES; GNUNET_CONTAINER_DLL_insert (kpc_head, kpc_tail, kpc); MHD_suspend_connection (rc->connection); return MHD_YES; } } if (NULL != kpc->response) { /* handle _failed_ resumed cases */ return MHD_queue_response (rc->connection, kpc->response_code, kpc->response); } /* _successfully_ resumed case */ { MHD_RESULT res; enum GNUNET_GenericReturnValue ret; ret = TEH_DB_run_transaction (kpc->rc->connection, "check proof kyc", &res, &persist_kyc_ok, kpc); if (GNUNET_SYSERR == ret) return res; } { struct MHD_Response *response; MHD_RESULT res; response = MHD_create_response_from_buffer (0, "", MHD_RESPMEM_PERSISTENT); if (NULL == response) { GNUNET_break (0); return MHD_NO; } GNUNET_break (MHD_YES == MHD_add_response_header ( response, MHD_HTTP_HEADER_LOCATION, TEH_kyc_config.details.oauth2.post_kyc_redirect_url)); res = MHD_queue_response (rc->connection, MHD_HTTP_SEE_OTHER, response); MHD_destroy_response (response); return res; } } /* end of taler-exchange-httpd_kyc-proof.c */