anastasis_authorization_plugin_email.c (20748B)
1 /* 2 This file is part of Anastasis 3 Copyright (C) 2019-2021 Anastasis SARL 4 5 Anastasis is free software; you can redistribute it and/or modify it under the 6 terms of the GNU Affero General Public License as published by the Free Software 7 Foundation; either version 3, or (at your option) any later version. 8 9 Anastasis is distributed in the hope that it will be useful, but WITHOUT ANY 10 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 11 A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 12 13 You should have received a copy of the GNU Affero General Public License along with 14 Anastasis; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/> 15 */ 16 /** 17 * @file anastasis_authorization_plugin_email.c 18 * @brief authorization plugin email based 19 * @author Dominik Meister 20 */ 21 #include "platform.h" 22 #include "anastasis_authorization_plugin.h" 23 #include <taler/taler_mhd_lib.h> 24 #include <taler/taler_json_lib.h> 25 #include <regex.h> 26 #include "anastasis_util_lib.h" 27 #include <gnunet/gnunet_db_lib.h> 28 #include "anastasis_database_lib.h" 29 30 /** 31 * How many retries do we allow per code? 32 */ 33 #define INITIAL_RETRY_COUNTER 3 34 35 /** 36 * Saves the State of a authorization plugin. 37 */ 38 struct Email_Context 39 { 40 41 /** 42 * Command which is executed to run the plugin (some bash script or a 43 * command line argument) 44 */ 45 char *auth_command; 46 47 /** 48 * Regex for email address validation. 49 */ 50 regex_t regex; 51 52 /** 53 * Messages of the plugin, read from a resource file. 54 */ 55 json_t *messages; 56 57 /** 58 * Context we operate in. 59 */ 60 const struct ANASTASIS_AuthorizationContext *ac; 61 62 }; 63 64 65 /** 66 * Saves the state of a authorization process 67 */ 68 struct ANASTASIS_AUTHORIZATION_State 69 { 70 /** 71 * Public key of the challenge which is authorised 72 */ 73 struct ANASTASIS_CRYPTO_TruthUUIDP truth_uuid; 74 75 /** 76 * Code which is sent to the user. 77 */ 78 uint64_t code; 79 80 /** 81 * Our plugin context. 82 */ 83 struct Email_Context *ctx; 84 85 /** 86 * Function to call when we made progress. 87 */ 88 GNUNET_SCHEDULER_TaskCallback trigger; 89 90 /** 91 * Closure for @e trigger. 92 */ 93 void *trigger_cls; 94 95 /** 96 * holds the truth information 97 */ 98 char *email; 99 100 /** 101 * Handle to the helper process. 102 */ 103 struct GNUNET_Process *child; 104 105 /** 106 * Handle to wait for @e child 107 */ 108 struct GNUNET_ChildWaitHandle *cwh; 109 110 /** 111 * Our client connection, set if suspended. 112 */ 113 struct MHD_Connection *connection; 114 115 /** 116 * Message to send. 117 */ 118 char *msg; 119 120 /** 121 * Offset of transmission in msg. 122 */ 123 size_t msg_off; 124 125 /** 126 * Exit code from helper. 127 */ 128 long unsigned int exit_code; 129 130 /** 131 * How did the helper die? 132 */ 133 enum GNUNET_OS_ProcessStatusType pst; 134 135 }; 136 137 138 /** 139 * Returned by #get_message() when the configured messages file has no entry 140 * for the requested ID. This is an installation error, but it must not be 141 * allowed to reach GNUNET_asprintf() as a NULL format string. The text 142 * deliberately contains no printf conversions, so substituting it for any 143 * template is safe whatever argument list the caller passes. 144 */ 145 #define MISSING_MESSAGE \ 146 "The provider is misconfigured: a message template is missing." 147 148 149 /** 150 * Obtain internationalized message @a msg_id from @a ctx using 151 * language preferences of @a conn. 152 * 153 * @param messages JSON object to lookup message from 154 * @param conn connection to lookup message for 155 * @param msg_id unique message ID 156 * @return the requested message, or #MISSING_MESSAGE if it was not 157 * configured; never NULL 158 */ 159 static const char * 160 get_message (const json_t *messages, 161 struct MHD_Connection *conn, 162 const char *msg_id) 163 { 164 const char *accept_lang; 165 166 accept_lang = MHD_lookup_connection_value (conn, 167 MHD_HEADER_KIND, 168 MHD_HTTP_HEADER_ACCEPT_LANGUAGE); 169 if (NULL == accept_lang) 170 accept_lang = "en_US"; 171 { 172 const char *ret; 173 struct GNUNET_JSON_Specification spec[] = { 174 TALER_JSON_spec_i18n_string (msg_id, 175 accept_lang, 176 &ret), 177 GNUNET_JSON_spec_end () 178 }; 179 180 if (GNUNET_OK != 181 GNUNET_JSON_parse (messages, 182 spec, 183 NULL, NULL)) 184 { 185 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 186 "Message `%s' is missing from the configured messages file\n", 187 msg_id); 188 GNUNET_JSON_parse_free (spec); 189 return MISSING_MESSAGE; 190 } 191 GNUNET_JSON_parse_free (spec); 192 if (NULL == ret) 193 { 194 /* The parser of TALER_JSON_spec_i18n_string returns #GNUNET_OK even 195 when the field is absent or is not a string, in which case it stores 196 NULL; the check above therefore never fires for a missing message and 197 this one is what keeps NULL out of the format argument of the 198 GNUNET_asprintf() calls below. */ 199 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 200 "Message `%s' is missing or not a string in the configured messages file\n", 201 msg_id); 202 return MISSING_MESSAGE; 203 } 204 return ret; 205 } 206 } 207 208 209 /** 210 * Validate @a data is a well-formed input into the challenge method, 211 * i.e. @a data is a well-formed phone number for sending an SMS, or 212 * a well-formed e-mail address for sending an e-mail. Not expected to 213 * check that the phone number or e-mail account actually exists. 214 * 215 * To be possibly used before issuing a 402 payment required to the client. 216 * 217 * @param cls closure 218 * @param connection HTTP client request (for queuing response) 219 * @param mime_type mime type of @e data 220 * @param data input to validate (i.e. is it a valid phone number, etc.) 221 * @param data_length number of bytes in @a data 222 * @return #GNUNET_OK if @a data is valid, 223 * #GNUNET_NO if @a data is invalid and a reply was successfully queued on @a connection 224 * #GNUNET_SYSERR if @a data invalid but we failed to queue a reply on @a connection 225 */ 226 static enum GNUNET_GenericReturnValue 227 email_validate (void *cls, 228 struct MHD_Connection *connection, 229 const char *mime_type, 230 const char *data, 231 size_t data_length) 232 { 233 struct Email_Context *ctx = cls; 234 int regex_result; 235 char *phone_number; 236 237 phone_number = GNUNET_strndup (data, 238 data_length); 239 regex_result = regexec (&ctx->regex, 240 phone_number, 241 0, 242 NULL, 243 0); 244 GNUNET_free (phone_number); 245 if (0 != regex_result) 246 { 247 if (MHD_NO == 248 TALER_MHD_reply_with_error (connection, 249 MHD_HTTP_CONFLICT, 250 TALER_EC_ANASTASIS_EMAIL_INVALID, 251 NULL)) 252 return GNUNET_SYSERR; 253 return GNUNET_NO; 254 } 255 return GNUNET_OK; 256 } 257 258 259 /** 260 * Begin issuing authentication challenge to user based on @a data. 261 * I.e. start to send SMS or e-mail or launch video identification. 262 * 263 * @param cls closure 264 * @param trigger function to call when we made progress 265 * @param trigger_cls closure for @a trigger 266 * @param truth_uuid Identifier of the challenge, to be (if possible) included in the 267 * interaction with the user 268 * @param code secret code that the user has to provide back to satisfy the challenge in 269 * the main anastasis protocol 270 * @param data input to validate (i.e. is it a valid phone number, etc.) 271 * @param data_length number of bytes in @a data 272 * @return state to track progress on the authorization operation, NULL on failure 273 */ 274 static struct ANASTASIS_AUTHORIZATION_State * 275 email_start (void *cls, 276 GNUNET_SCHEDULER_TaskCallback trigger, 277 void *trigger_cls, 278 const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, 279 uint64_t code, 280 const void *data, 281 size_t data_length) 282 { 283 struct Email_Context *ctx = cls; 284 struct ANASTASIS_AUTHORIZATION_State *as; 285 enum GNUNET_DB_QueryStatus qs; 286 287 /* If the user can show this challenge code, this 288 plugin is already happy (no additional 289 requirements), so mark this challenge as 290 already satisfied from the start. */ 291 qs = ANASTASIS_DB_update_to_challenge_code_satisfied ( 292 truth_uuid, 293 code); 294 if (qs <= 0) 295 { 296 GNUNET_break (0); 297 return NULL; 298 } 299 as = GNUNET_new (struct ANASTASIS_AUTHORIZATION_State); 300 as->trigger = trigger; 301 as->trigger_cls = trigger_cls; 302 as->ctx = ctx; 303 as->truth_uuid = *truth_uuid; 304 as->code = code; 305 as->email = GNUNET_strndup (data, 306 data_length); 307 return as; 308 } 309 310 311 /** 312 * Function called when our Email helper has terminated. 313 * 314 * @param cls our `struct ANASTASIS_AUHTORIZATION_State` 315 * @param type type of the process 316 * @param exit_code status code of the process 317 */ 318 static void 319 email_done_cb (void *cls, 320 enum GNUNET_OS_ProcessStatusType type, 321 long unsigned int exit_code) 322 { 323 struct ANASTASIS_AUTHORIZATION_State *as = cls; 324 325 as->cwh = NULL; 326 if (NULL != as->child) 327 { 328 GNUNET_process_destroy (as->child); 329 as->child = NULL; 330 } 331 as->pst = type; 332 as->exit_code = exit_code; 333 MHD_resume_connection (as->connection); 334 as->trigger (as->trigger_cls); 335 } 336 337 338 /** 339 * Begin issuing authentication challenge to user based on @a data. 340 * I.e. start to send SMS or e-mail or launch video identification. 341 * 342 * @param as authorization state 343 * @param connection HTTP client request (for queuing response, such as redirection to video portal) 344 * @return state of the request 345 */ 346 static enum ANASTASIS_AUTHORIZATION_ChallengeResult 347 email_challenge (struct ANASTASIS_AUTHORIZATION_State *as, 348 struct MHD_Connection *connection) 349 { 350 enum MHD_Result mres; 351 const char *mime; 352 const char *lang; 353 354 mime = MHD_lookup_connection_value (connection, 355 MHD_HEADER_KIND, 356 MHD_HTTP_HEADER_ACCEPT); 357 if (NULL == mime) 358 mime = "text/plain"; 359 lang = MHD_lookup_connection_value (connection, 360 MHD_HEADER_KIND, 361 MHD_HTTP_HEADER_ACCEPT_LANGUAGE); 362 if (NULL == lang) 363 lang = "en"; 364 if (NULL == as->msg) 365 { 366 /* First time, start child process and feed pipe */ 367 struct GNUNET_DISK_PipeHandle *p; 368 struct GNUNET_DISK_FileHandle *pipe_stdin; 369 370 p = GNUNET_DISK_pipe (GNUNET_DISK_PF_BLOCKING_RW); 371 if (NULL == p) 372 { 373 mres = TALER_MHD_reply_with_error (connection, 374 MHD_HTTP_INTERNAL_SERVER_ERROR, 375 TALER_EC_ANASTASIS_EMAIL_HELPER_EXEC_FAILED, 376 "pipe"); 377 if (MHD_YES != mres) 378 return ANASTASIS_AUTHORIZATION_CRES_FAILED_REPLY_FAILED; 379 return ANASTASIS_AUTHORIZATION_CRES_FAILED; 380 } 381 as->child = GNUNET_process_create (GNUNET_OS_INHERIT_STD_ERR); 382 GNUNET_assert (GNUNET_OK == 383 GNUNET_process_set_options ( 384 as->child, 385 GNUNET_process_option_inherit_rpipe (p, 386 STDIN_FILENO))); 387 if (GNUNET_OK != 388 GNUNET_process_run_command_va (as->child, 389 as->ctx->auth_command, 390 as->ctx->auth_command, 391 as->email, 392 NULL)) 393 { 394 GNUNET_process_destroy (as->child); 395 as->child = NULL; 396 GNUNET_DISK_pipe_close (p); 397 mres = TALER_MHD_reply_with_error (connection, 398 MHD_HTTP_INTERNAL_SERVER_ERROR, 399 TALER_EC_ANASTASIS_EMAIL_HELPER_EXEC_FAILED, 400 "exec"); 401 if (MHD_YES != mres) 402 return ANASTASIS_AUTHORIZATION_CRES_FAILED_REPLY_FAILED; 403 return ANASTASIS_AUTHORIZATION_CRES_FAILED; 404 } 405 pipe_stdin = GNUNET_DISK_pipe_detach_end (p, 406 GNUNET_DISK_PIPE_END_WRITE); 407 GNUNET_assert (NULL != pipe_stdin); 408 GNUNET_DISK_pipe_close (p); 409 GNUNET_asprintf (&as->msg, 410 get_message (as->ctx->messages, 411 connection, 412 "body"), 413 ANASTASIS_pin2s (as->code), 414 ANASTASIS_CRYPTO_uuid2s (&as->truth_uuid)); 415 416 { 417 const char *off = as->msg; 418 size_t left = strlen (off); 419 420 while (0 != left) 421 { 422 ssize_t ret; 423 424 ret = GNUNET_DISK_file_write (pipe_stdin, 425 off, 426 left); 427 if (ret <= 0) 428 { 429 mres = TALER_MHD_reply_with_error (connection, 430 MHD_HTTP_INTERNAL_SERVER_ERROR, 431 TALER_EC_ANASTASIS_EMAIL_HELPER_EXEC_FAILED, 432 "write"); 433 if (MHD_YES != mres) 434 return ANASTASIS_AUTHORIZATION_CRES_FAILED_REPLY_FAILED; 435 return ANASTASIS_AUTHORIZATION_CRES_FAILED; 436 } 437 as->msg_off += ret; 438 off += ret; 439 left -= ret; 440 } 441 GNUNET_DISK_file_close (pipe_stdin); 442 } 443 as->cwh = GNUNET_wait_child (as->child, 444 &email_done_cb, 445 as); 446 as->connection = connection; 447 MHD_suspend_connection (connection); 448 return ANASTASIS_AUTHORIZATION_CRES_SUSPENDED; 449 } 450 if (NULL != as->cwh) 451 { 452 /* Spurious call, why are we here? */ 453 GNUNET_break (0); 454 MHD_suspend_connection (connection); 455 return ANASTASIS_AUTHORIZATION_CRES_SUSPENDED; 456 } 457 if ( (GNUNET_OS_PROCESS_EXITED != as->pst) || 458 (0 != as->exit_code) ) 459 { 460 char es[32]; 461 462 GNUNET_snprintf (es, 463 sizeof (es), 464 "%u/%d", 465 (unsigned int) as->exit_code, 466 as->pst); 467 mres = TALER_MHD_reply_with_error (connection, 468 MHD_HTTP_INTERNAL_SERVER_ERROR, 469 TALER_EC_ANASTASIS_EMAIL_HELPER_COMMAND_FAILED, 470 es); 471 if (MHD_YES != mres) 472 return ANASTASIS_AUTHORIZATION_CRES_FAILED_REPLY_FAILED; 473 return ANASTASIS_AUTHORIZATION_CRES_FAILED; 474 } 475 476 /* Build HTTP response */ 477 { 478 struct MHD_Response *resp; 479 const char *at; 480 size_t len; 481 482 at = strchr (as->email, '@'); 483 if (NULL == at) 484 len = 0; 485 else 486 len = at - as->email; 487 488 if (0.0 < TALER_pattern_matches (mime, 489 "application/json")) 490 { 491 char *user; 492 493 user = GNUNET_strndup (as->email, 494 len); 495 resp = TALER_MHD_MAKE_JSON_PACK ( 496 GNUNET_JSON_pack_string ("challenge_type", 497 "TAN_SENT"), 498 GNUNET_JSON_pack_string ("tan_address_hint", 499 user)); 500 GNUNET_free (user); 501 } 502 else 503 { 504 size_t reply_len; 505 char *reply; 506 507 reply_len = GNUNET_asprintf (&reply, 508 get_message (as->ctx->messages, 509 connection, 510 "instructions"), 511 (unsigned int) len, 512 as->email); 513 resp = MHD_create_response_from_buffer (reply_len, 514 reply, 515 MHD_RESPMEM_MUST_COPY); 516 GNUNET_free (reply); 517 TALER_MHD_add_global_headers (resp, 518 false); 519 GNUNET_break (MHD_YES == 520 MHD_add_response_header (resp, 521 MHD_HTTP_HEADER_CONTENT_TYPE, 522 "text/plain")); 523 } 524 mres = MHD_queue_response (connection, 525 MHD_HTTP_OK, 526 resp); 527 MHD_destroy_response (resp); 528 if (MHD_YES != mres) 529 return ANASTASIS_AUTHORIZATION_CRES_SUCCESS_REPLY_FAILED; 530 return ANASTASIS_AUTHORIZATION_CRES_SUCCESS; 531 } 532 } 533 534 535 /** 536 * Free internal state associated with @a as. 537 * 538 * @param as state to clean up 539 */ 540 static void 541 email_cleanup (struct ANASTASIS_AUTHORIZATION_State *as) 542 { 543 if (NULL != as->cwh) 544 { 545 GNUNET_wait_child_cancel (as->cwh); 546 as->cwh = NULL; 547 } 548 if (NULL != as->child) 549 { 550 GNUNET_break (GNUNET_OK == 551 GNUNET_process_kill (as->child, 552 SIGKILL)); 553 GNUNET_break (GNUNET_OK == 554 GNUNET_process_wait (as->child, 555 true, 556 NULL, 557 NULL)); 558 GNUNET_process_destroy (as->child); 559 as->child = NULL; 560 } 561 GNUNET_free (as->msg); 562 GNUNET_free (as->email); 563 GNUNET_free (as); 564 } 565 566 567 /** 568 * Initialize email based authorization plugin 569 * 570 * @param cls a configuration instance 571 * @return NULL on error, otherwise a `struct ANASTASIS_AuthorizationPlugin` 572 */ 573 void * 574 libanastasis_plugin_authorization_email_init (void *cls); 575 576 /* declaration to fix compiler warning */ 577 void * 578 libanastasis_plugin_authorization_email_init (void *cls) 579 { 580 const struct ANASTASIS_AuthorizationContext *ac = cls; 581 struct ANASTASIS_AuthorizationPlugin *plugin; 582 const struct GNUNET_CONFIGURATION_Handle *cfg = ac->cfg; 583 struct Email_Context *ctx; 584 585 ctx = GNUNET_new (struct Email_Context); 586 ctx->ac = ac; 587 { 588 char *fn; 589 json_error_t err; 590 char *tmp; 591 592 tmp = GNUNET_OS_installation_get_path (ANASTASIS_project_data (), 593 GNUNET_OS_IPK_DATADIR); 594 GNUNET_asprintf (&fn, 595 "%sauthorization-email-messages.json", 596 tmp); 597 GNUNET_free (tmp); 598 ctx->messages = json_load_file (fn, 599 JSON_REJECT_DUPLICATES, 600 &err); 601 if (NULL == ctx->messages) 602 { 603 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 604 "Failed to load messages from `%s': %s at %d:%d\n", 605 fn, 606 err.text, 607 err.line, 608 err.column); 609 GNUNET_free (fn); 610 GNUNET_free (ctx); 611 return NULL; 612 } 613 GNUNET_free (fn); 614 } 615 { 616 int regex_result; 617 /* anchored so the whole address must match, not merely contain a match */ 618 const char *regexp = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"; 619 620 regex_result = regcomp (&ctx->regex, 621 regexp, 622 REG_EXTENDED); 623 if (0 < regex_result) 624 { 625 GNUNET_break (0); 626 json_decref (ctx->messages); 627 GNUNET_free (ctx); 628 return NULL; 629 } 630 } 631 632 plugin = GNUNET_new (struct ANASTASIS_AuthorizationPlugin); 633 plugin->retry_counter = INITIAL_RETRY_COUNTER; 634 plugin->code_validity_period = GNUNET_TIME_UNIT_DAYS; 635 plugin->code_rotation_period = GNUNET_TIME_UNIT_HOURS; 636 plugin->code_retransmission_frequency = GNUNET_TIME_UNIT_MINUTES; 637 plugin->cls = ctx; 638 plugin->validate = &email_validate; 639 plugin->start = &email_start; 640 plugin->challenge = &email_challenge; 641 plugin->cleanup = &email_cleanup; 642 643 if (GNUNET_OK != 644 GNUNET_CONFIGURATION_get_value_string (cfg, 645 "authorization-email", 646 "COMMAND", 647 &ctx->auth_command)) 648 { 649 GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, 650 "authorization-email", 651 "COMMAND"); 652 regfree (&ctx->regex); 653 json_decref (ctx->messages); 654 GNUNET_free (ctx); 655 GNUNET_free (plugin); 656 return NULL; 657 } 658 return plugin; 659 } 660 661 662 /** 663 * Unload authorization plugin 664 * 665 * @param cls a `struct ANASTASIS_AuthorizationPlugin` 666 * @return NULL (always) 667 */ 668 void * 669 libanastasis_plugin_authorization_email_done (void *cls); 670 671 /* declaration to fix compiler warning */ 672 void * 673 libanastasis_plugin_authorization_email_done (void *cls) 674 { 675 struct ANASTASIS_AuthorizationPlugin *plugin = cls; 676 struct Email_Context *ctx = plugin->cls; 677 678 GNUNET_free (ctx->auth_command); 679 regfree (&ctx->regex); 680 json_decref (ctx->messages); 681 GNUNET_free (ctx); 682 GNUNET_free (plugin); 683 return NULL; 684 }