/* 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 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 mime_type 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.) * @param data_length number of bytes in @a data * @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")) { resp = TALER_MHD_MAKE_JSON_PACK ( GNUNET_JSON_pack_uint64 ("code", TALER_EC_ANASTASIS_TRUTH_CHALLENGE_RESPONSE_REQUIRED), GNUNET_JSON_pack_string ("hint", TALER_ErrorCode_get_hint ( TALER_EC_ANASTASIS_TRUTH_CHALLENGE_RESPONSE_REQUIRED)), GNUNET_JSON_pack_string ("detail", zip)); } 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; }