/*
This file is part of Anastasis
Copyright (C) 2021 Anastasis SARL
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 Affero General Public License for more details.
You should have received a copy of the GNU Affero 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"
#include
#include "anastasis_database_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;
/**
* Argument passed to the "init" function of each
* plugin.
*/
const struct ANASTASIS_AuthorizationContext *ac;
};
/**
* 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;
enum GNUNET_DB_QueryStatus qs;
/* If the user can show this challenge code, this
plugin is already happy (no additional
requirements), so mark this challenge as
already satisfied from the start. */
qs = ctx->ac->db->mark_challenge_code_satisfied (ctx->ac->db->cls,
truth_uuid,
code);
if (qs <= 0)
{
GNUNET_break (0);
return NULL;
}
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 timeout how long do we have to produce a reply
* @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 GNUNET_TIME_Absolute timeout,
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 ()
};
(void) timeout;
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)
{
const struct ANASTASIS_AuthorizationContext *ac = cls;
struct ANASTASIS_AuthorizationPlugin *plugin;
const struct GNUNET_CONFIGURATION_Handle *cfg = ac->cfg;
struct PostContext *ctx;
ctx = GNUNET_new (struct PostContext);
ctx->ac = ac;
{
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;
}