From 7e669bcf6b6336ec429da949bcb4aa456971dba2 Mon Sep 17 00:00:00 2001 From: Christian Grothoff Date: Fri, 30 Jul 2021 10:38:27 +0200 Subject: folding history in preparation of GNU Anastasis v0.0.0 release --- .../anastasis_authorization_plugin_post.c | 655 +++++++++++++++++++++ 1 file changed, 655 insertions(+) create mode 100644 src/authorization/anastasis_authorization_plugin_post.c (limited to 'src/authorization/anastasis_authorization_plugin_post.c') diff --git a/src/authorization/anastasis_authorization_plugin_post.c b/src/authorization/anastasis_authorization_plugin_post.c new file mode 100644 index 0000000..1f20ff3 --- /dev/null +++ b/src/authorization/anastasis_authorization_plugin_post.c @@ -0,0 +1,655 @@ +/* + This file is part of Anastasis + Copyright (C) 2021 Taler Systems SA + + Anastasis is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + Anastasis 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 General Public License for more details. + + You should have received a copy of the GNU General Public License along with + Anastasis; see the file COPYING.GPL. If not, see +*/ +/** + * @file include/anastasis_authorization_plugin_post.c + * @brief authorization plugin post based + * @author Christian Grothoff + */ +#include "platform.h" +#include "anastasis_authorization_plugin.h" +#include +#include +#include +#include "anastasis_util_lib.h" + + +/** + * Saves the State of a authorization plugin. + */ +struct PostContext +{ + + /** + * Command which is executed to run the plugin (some bash script or a + * command line argument) + */ + char *auth_command; + + /** + * Messages of the plugin, read from a resource file. + */ + json_t *messages; +}; + + +/** + * Saves the state of a authorization process + */ +struct ANASTASIS_AUTHORIZATION_State +{ + /** + * Public key of the challenge which is authorised + */ + struct ANASTASIS_CRYPTO_TruthUUIDP truth_uuid; + + /** + * Code which is sent to the user. + */ + uint64_t code; + + /** + * Our plugin context. + */ + struct PostContext *ctx; + + /** + * Function to call when we made progress. + */ + GNUNET_SCHEDULER_TaskCallback trigger; + + /** + * Closure for @e trigger. + */ + void *trigger_cls; + + /** + * holds the truth information + */ + json_t *post; + + /** + * Handle to the helper process. + */ + struct GNUNET_OS_Process *child; + + /** + * Handle to wait for @e child + */ + struct GNUNET_ChildWaitHandle *cwh; + + /** + * Our client connection, set if suspended. + */ + struct MHD_Connection *connection; + + /** + * Message to send. + */ + char *msg; + + /** + * Offset of transmission in msg. + */ + size_t msg_off; + + /** + * Exit code from helper. + */ + long unsigned int exit_code; + + /** + * How did the helper die? + */ + enum GNUNET_OS_ProcessStatusType pst; + + +}; + + +/** + * Obtain internationalized message @a msg_id from @a ctx using + * language preferences of @a conn. + * + * @param messages JSON object to lookup message from + * @param conn connection to lookup message for + * @param msg_id unique message ID + * @return NULL if message was not found + */ +static const char * +get_message (const json_t *messages, + struct MHD_Connection *conn, + const char *msg_id) +{ + const char *accept_lang; + + accept_lang = MHD_lookup_connection_value (conn, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_ACCEPT_LANGUAGE); + if (NULL == accept_lang) + accept_lang = "en_US"; + { + const char *ret; + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_i18n_string (msg_id, + accept_lang, + &ret), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (messages, + spec, + NULL, NULL)) + { + GNUNET_break (0); + return NULL; + } + return ret; + } +} + + +/** + * Validate @a data is a well-formed input into the challenge method, + * i.e. @a data is a well-formed phone number for sending an SMS, or + * a well-formed e-mail address for sending an e-mail. Not expected to + * check that the phone number or e-mail account actually exists. + * + * To be possibly used before issuing a 402 payment required to the client. + * + * @param cls closure + * @param connection HTTP client request (for queuing response) + * @param truth_mime mime type of @e data + * @param data input to validate (i.e. is it a valid phone number, etc.) + * @param data_length number of bytes in @a data + * @return #GNUNET_OK if @a data is valid, + * #GNUNET_NO if @a data is invalid and a reply was successfully queued on @a connection + * #GNUNET_SYSERR if @a data invalid but we failed to queue a reply on @a connection + */ +static enum GNUNET_GenericReturnValue +post_validate (void *cls, + struct MHD_Connection *connection, + const char *mime_type, + const char *data, + size_t data_length) +{ + struct PostContext *ctx = cls; + json_t *j; + json_error_t error; + const char *name; + const char *street; + const char *city; + const char *zip; + const char *country; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("full_name", + &name), + GNUNET_JSON_spec_string ("street", + &street), + GNUNET_JSON_spec_string ("city", + &city), + GNUNET_JSON_spec_string ("postcode", + &zip), + GNUNET_JSON_spec_string ("country", + &country), + GNUNET_JSON_spec_end () + }; + + (void) ctx; + j = json_loadb (data, + data_length, + JSON_REJECT_DUPLICATES, + &error); + if (NULL == j) + { + if (MHD_NO == + TALER_MHD_reply_with_error (connection, + MHD_HTTP_EXPECTATION_FAILED, + TALER_EC_ANASTASIS_POST_INVALID, + "JSON malformed")) + return GNUNET_SYSERR; + return GNUNET_NO; + } + + if (GNUNET_OK != + GNUNET_JSON_parse (j, + spec, + NULL, NULL)) + { + GNUNET_break (0); + json_decref (j); + if (MHD_NO == + TALER_MHD_reply_with_error (connection, + MHD_HTTP_EXPECTATION_FAILED, + TALER_EC_ANASTASIS_POST_INVALID, + "JSON lacked required address information")) + return GNUNET_SYSERR; + return GNUNET_NO; + } + json_decref (j); + return GNUNET_OK; +} + + +/** + * Begin issuing authentication challenge to user based on @a data. + * I.e. start to send mail. + * + * @param cls closure + * @param trigger function to call when we made progress + * @param trigger_cls closure for @a trigger + * @param truth_uuid Identifier of the challenge, to be (if possible) included in the + * interaction with the user + * @param code secret code that the user has to provide back to satisfy the challenge in + * the main anastasis protocol + * @param data input to validate (i.e. is it a valid phone number, etc.) + * @return state to track progress on the authorization operation, NULL on failure + */ +static struct ANASTASIS_AUTHORIZATION_State * +post_start (void *cls, + GNUNET_SCHEDULER_TaskCallback trigger, + void *trigger_cls, + const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, + uint64_t code, + const void *data, + size_t data_length) +{ + struct PostContext *ctx = cls; + struct ANASTASIS_AUTHORIZATION_State *as; + json_error_t error; + + as = GNUNET_new (struct ANASTASIS_AUTHORIZATION_State); + as->trigger = trigger; + as->trigger_cls = trigger_cls; + as->ctx = ctx; + as->truth_uuid = *truth_uuid; + as->code = code; + as->post = json_loadb (data, + data_length, + JSON_REJECT_DUPLICATES, + &error); + if (NULL == as->post) + { + GNUNET_break (0); + GNUNET_free (as); + return NULL; + } + return as; +} + + +/** + * Function called when our Post helper has terminated. + * + * @param cls our `struct ANASTASIS_AUHTORIZATION_State` + * @param type type of the process + * @param exit_code status code of the process + */ +static void +post_done_cb (void *cls, + enum GNUNET_OS_ProcessStatusType type, + long unsigned int exit_code) +{ + struct ANASTASIS_AUTHORIZATION_State *as = cls; + + as->child = NULL; + as->cwh = NULL; + as->pst = type; + as->exit_code = exit_code; + MHD_resume_connection (as->connection); + as->trigger (as->trigger_cls); +} + + +/** + * Begin issuing authentication challenge to user based on @a data. + * I.e. start to send SMS or e-mail or launch video identification. + * + * @param as authorization state + * @param connection HTTP client request (for queuing response, such as redirection to video portal) + * @return state of the request + */ +static enum ANASTASIS_AUTHORIZATION_Result +post_process (struct ANASTASIS_AUTHORIZATION_State *as, + struct MHD_Connection *connection) +{ + const char *mime; + const char *lang; + MHD_RESULT mres; + const char *name; + const char *street; + const char *city; + const char *zip; + const char *country; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("full_name", + &name), + GNUNET_JSON_spec_string ("street", + &street), + GNUNET_JSON_spec_string ("city", + &city), + GNUNET_JSON_spec_string ("postcode", + &zip), + GNUNET_JSON_spec_string ("country", + &country), + GNUNET_JSON_spec_end () + }; + + mime = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_ACCEPT); + if (NULL == mime) + mime = "text/plain"; + lang = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_ACCEPT_LANGUAGE); + if (NULL == lang) + lang = "en"; + if (GNUNET_OK != + GNUNET_JSON_parse (as->post, + spec, + NULL, NULL)) + { + GNUNET_break (0); + mres = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_POST_INVALID, + "address information incomplete"); + if (MHD_YES != mres) + return ANASTASIS_AUTHORIZATION_RES_FAILED_REPLY_FAILED; + return ANASTASIS_AUTHORIZATION_RES_FAILED; + } + if (NULL == as->msg) + { + /* First time, start child process and feed pipe */ + struct GNUNET_DISK_PipeHandle *p; + struct GNUNET_DISK_FileHandle *pipe_stdin; + + p = GNUNET_DISK_pipe (GNUNET_DISK_PF_BLOCKING_RW); + if (NULL == p) + { + mres = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_POST_HELPER_EXEC_FAILED, + "pipe"); + if (MHD_YES != mres) + return ANASTASIS_AUTHORIZATION_RES_FAILED_REPLY_FAILED; + return ANASTASIS_AUTHORIZATION_RES_FAILED; + } + as->child = GNUNET_OS_start_process (GNUNET_OS_INHERIT_STD_ERR, + p, + NULL, + NULL, + as->ctx->auth_command, + as->ctx->auth_command, + name, + street, + city, + zip, + country, + NULL); + if (NULL == as->child) + { + GNUNET_DISK_pipe_close (p); + mres = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_POST_HELPER_EXEC_FAILED, + "exec"); + if (MHD_YES != mres) + return ANASTASIS_AUTHORIZATION_RES_FAILED_REPLY_FAILED; + return ANASTASIS_AUTHORIZATION_RES_FAILED; + } + pipe_stdin = GNUNET_DISK_pipe_detach_end (p, + GNUNET_DISK_PIPE_END_WRITE); + GNUNET_assert (NULL != pipe_stdin); + GNUNET_DISK_pipe_close (p); + { + char *tpk; + + tpk = GNUNET_STRINGS_data_to_string_alloc ( + &as->truth_uuid, + sizeof (as->truth_uuid)); + GNUNET_asprintf (&as->msg, + get_message (as->ctx->messages, + connection, + "body"), + (unsigned long long) as->code, + tpk); + GNUNET_free (tpk); + } + + { + const char *off = as->msg; + size_t left = strlen (off); + + while (0 != left) + { + ssize_t ret; + + if (0 == left) + break; + ret = GNUNET_DISK_file_write (pipe_stdin, + off, + left); + if (ret <= 0) + { + mres = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_POST_HELPER_EXEC_FAILED, + "write"); + if (MHD_YES != mres) + return ANASTASIS_AUTHORIZATION_RES_FAILED_REPLY_FAILED; + return ANASTASIS_AUTHORIZATION_RES_FAILED; + } + as->msg_off += ret; + off += ret; + left -= ret; + } + GNUNET_DISK_file_close (pipe_stdin); + } + as->cwh = GNUNET_wait_child (as->child, + &post_done_cb, + as); + as->connection = connection; + MHD_suspend_connection (connection); + return ANASTASIS_AUTHORIZATION_RES_SUSPENDED; + } + if (NULL != as->cwh) + { + /* Spurious call, why are we here? */ + GNUNET_break (0); + MHD_suspend_connection (connection); + return ANASTASIS_AUTHORIZATION_RES_SUSPENDED; + } + if ( (GNUNET_OS_PROCESS_EXITED != as->pst) || + (0 != as->exit_code) ) + { + char es[32]; + + GNUNET_snprintf (es, + sizeof (es), + "%u/%d", + (unsigned int) as->exit_code, + as->pst); + mres = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_POST_HELPER_COMMAND_FAILED, + es); + if (MHD_YES != mres) + return ANASTASIS_AUTHORIZATION_RES_FAILED_REPLY_FAILED; + return ANASTASIS_AUTHORIZATION_RES_FAILED; + } + + /* Build HTTP response */ + { + struct MHD_Response *resp; + + if (TALER_MHD_xmime_matches (mime, + "application/json")) + { + json_t *body; + + body = json_pack ("{s:I, s:s, s:s}", + "code", + TALER_EC_ANASTASIS_TRUTH_CHALLENGE_RESPONSE_REQUIRED, + "hint", + TALER_ErrorCode_get_hint ( + TALER_EC_ANASTASIS_TRUTH_CHALLENGE_RESPONSE_REQUIRED), + "detail", + zip); + GNUNET_break (NULL != body); + resp = TALER_MHD_make_json (body); + } + else + { + size_t reply_len; + char *reply; + + reply_len = GNUNET_asprintf (&reply, + get_message (as->ctx->messages, + connection, + "instructions"), + zip); + resp = MHD_create_response_from_buffer (reply_len, + reply, + MHD_RESPMEM_MUST_COPY); + GNUNET_free (reply); + TALER_MHD_add_global_headers (resp); + } + mres = MHD_queue_response (connection, + MHD_HTTP_FORBIDDEN, + resp); + MHD_destroy_response (resp); + if (MHD_YES != mres) + return ANASTASIS_AUTHORIZATION_RES_SUCCESS_REPLY_FAILED; + return ANASTASIS_AUTHORIZATION_RES_SUCCESS; + } +} + + +/** + * Free internal state associated with @a as. + * + * @param as state to clean up + */ +static void +post_cleanup (struct ANASTASIS_AUTHORIZATION_State *as) +{ + if (NULL != as->cwh) + { + GNUNET_wait_child_cancel (as->cwh); + as->cwh = NULL; + } + if (NULL != as->child) + { + (void) GNUNET_OS_process_kill (as->child, + SIGKILL); + GNUNET_break (GNUNET_OK == + GNUNET_OS_process_wait (as->child)); + as->child = NULL; + } + GNUNET_free (as->msg); + json_decref (as->post); + GNUNET_free (as); +} + + +/** + * Initialize post based authorization plugin + * + * @param cls a configuration instance + * @return NULL on error, otherwise a `struct ANASTASIS_AuthorizationPlugin` + */ +void * +libanastasis_plugin_authorization_post_init (void *cls) +{ + struct ANASTASIS_AuthorizationPlugin *plugin; + struct GNUNET_CONFIGURATION_Handle *cfg = cls; + struct PostContext *ctx; + + ctx = GNUNET_new (struct PostContext); + { + char *fn; + json_error_t err; + + GNUNET_asprintf (&fn, + "%sauthorization-post-messages.json", + GNUNET_OS_installation_get_path (GNUNET_OS_IPK_DATADIR)); + ctx->messages = json_load_file (fn, + JSON_REJECT_DUPLICATES, + &err); + if (NULL == ctx->messages) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to load messages from `%s': %s at %d:%d\n", + fn, + err.text, + err.line, + err.column); + GNUNET_free (fn); + GNUNET_free (ctx); + return NULL; + } + GNUNET_free (fn); + } + plugin = GNUNET_new (struct ANASTASIS_AuthorizationPlugin); + plugin->code_validity_period = GNUNET_TIME_UNIT_MONTHS; + plugin->code_rotation_period = GNUNET_TIME_UNIT_WEEKS; + plugin->code_retransmission_frequency + = GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_DAYS, + 2); + plugin->cls = ctx; + plugin->validate = &post_validate; + plugin->start = &post_start; + plugin->process = &post_process; + plugin->cleanup = &post_cleanup; + + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (cfg, + "authorization-post", + "COMMAND", + &ctx->auth_command)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "authorization-post", + "COMMAND"); + json_decref (ctx->messages); + GNUNET_free (ctx); + GNUNET_free (plugin); + return NULL; + } + return plugin; +} + + +/** + * Unload authorization plugin + * + * @param cls a `struct ANASTASIS_AuthorizationPlugin` + * @return NULL (always) + */ +void * +libanastasis_plugin_authorization_post_done (void *cls) +{ + struct ANASTASIS_AuthorizationPlugin *plugin = cls; + struct PostContext *ctx = plugin->cls; + + GNUNET_free (ctx->auth_command); + json_decref (ctx->messages); + GNUNET_free (ctx); + GNUNET_free (plugin); + return NULL; +} -- cgit v1.2.3