anastasis_authorization_plugin_email.c (22462B)
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 * Check that @a value can be passed to the helper as a positional argument. 211 * The address is handed to #GNUNET_process_run_command_va(), i.e. it becomes 212 * argv[1] of the (site-supplied) command; an address starting with '-' would 213 * be read as an option by whatever that command is. The e-mail grammar does 214 * allow a leading '-' in the local part, so this has to be screened out 215 * explicitly. Not every helper honours "--", so we refuse anything 216 * option-shaped rather than rely on it. 217 * 218 * @param value address to check, may be NULL 219 * @return true if @a value is safe to pass as a positional argument 220 */ 221 static bool 222 is_safe_argument (const char *value) 223 { 224 return ( (NULL != value) && 225 ('-' != value[0]) ); 226 } 227 228 229 /** 230 * Validate @a data is a well-formed input into the challenge method, 231 * i.e. @a data is a well-formed phone number for sending an SMS, or 232 * a well-formed e-mail address for sending an e-mail. Not expected to 233 * check that the phone number or e-mail account actually exists. 234 * 235 * To be possibly used before issuing a 402 payment required to the client. 236 * 237 * @param cls closure 238 * @param connection HTTP client request (for queuing response) 239 * @param mime_type mime type of @e data 240 * @param data input to validate (i.e. is it a valid phone number, etc.) 241 * @param data_length number of bytes in @a data 242 * @return #GNUNET_OK if @a data is valid, 243 * #GNUNET_NO if @a data is invalid and a reply was successfully queued on @a connection 244 * #GNUNET_SYSERR if @a data invalid but we failed to queue a reply on @a connection 245 */ 246 static enum GNUNET_GenericReturnValue 247 email_validate (void *cls, 248 struct MHD_Connection *connection, 249 const char *mime_type, 250 const char *data, 251 size_t data_length) 252 { 253 struct Email_Context *ctx = cls; 254 int regex_result; 255 bool safe; 256 char *email; 257 258 email = GNUNET_strndup (data, 259 data_length); 260 regex_result = regexec (&ctx->regex, 261 email, 262 0, 263 NULL, 264 0); 265 safe = is_safe_argument (email); 266 GNUNET_free (email); 267 if (0 != regex_result) 268 { 269 if (MHD_NO == 270 TALER_MHD_reply_with_error (connection, 271 MHD_HTTP_CONFLICT, 272 TALER_EC_ANASTASIS_EMAIL_INVALID, 273 NULL)) 274 return GNUNET_SYSERR; 275 return GNUNET_NO; 276 } 277 if (! safe) 278 { 279 if (MHD_NO == 280 TALER_MHD_reply_with_error (connection, 281 MHD_HTTP_CONFLICT, 282 TALER_EC_ANASTASIS_EMAIL_INVALID, 283 "address must not begin with `-'")) 284 return GNUNET_SYSERR; 285 return GNUNET_NO; 286 } 287 return GNUNET_OK; 288 } 289 290 291 /** 292 * Begin issuing authentication challenge to user based on @a data. 293 * I.e. start to send SMS or e-mail or launch video identification. 294 * 295 * @param cls closure 296 * @param trigger function to call when we made progress 297 * @param trigger_cls closure for @a trigger 298 * @param truth_uuid Identifier of the challenge, to be (if possible) included in the 299 * interaction with the user 300 * @param code secret code that the user has to provide back to satisfy the challenge in 301 * the main anastasis protocol 302 * @param data input to validate (i.e. is it a valid phone number, etc.) 303 * @param data_length number of bytes in @a data 304 * @return state to track progress on the authorization operation, NULL on failure 305 */ 306 static struct ANASTASIS_AUTHORIZATION_State * 307 email_start (void *cls, 308 GNUNET_SCHEDULER_TaskCallback trigger, 309 void *trigger_cls, 310 const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, 311 uint64_t code, 312 const void *data, 313 size_t data_length) 314 { 315 struct Email_Context *ctx = cls; 316 struct ANASTASIS_AUTHORIZATION_State *as; 317 enum GNUNET_DB_QueryStatus qs; 318 319 /* If the user can show this challenge code, this 320 plugin is already happy (no additional 321 requirements), so mark this challenge as 322 already satisfied from the start. */ 323 qs = ANASTASIS_DB_update_to_challenge_code_satisfied ( 324 truth_uuid, 325 code); 326 if (qs <= 0) 327 { 328 GNUNET_break (0); 329 return NULL; 330 } 331 as = GNUNET_new (struct ANASTASIS_AUTHORIZATION_State); 332 as->trigger = trigger; 333 as->trigger_cls = trigger_cls; 334 as->ctx = ctx; 335 as->truth_uuid = *truth_uuid; 336 as->code = code; 337 as->email = GNUNET_strndup (data, 338 data_length); 339 return as; 340 } 341 342 343 /** 344 * Function called when our Email helper has terminated. 345 * 346 * @param cls our `struct ANASTASIS_AUHTORIZATION_State` 347 * @param type type of the process 348 * @param exit_code status code of the process 349 */ 350 static void 351 email_done_cb (void *cls, 352 enum GNUNET_OS_ProcessStatusType type, 353 long unsigned int exit_code) 354 { 355 struct ANASTASIS_AUTHORIZATION_State *as = cls; 356 357 as->cwh = NULL; 358 if (NULL != as->child) 359 { 360 GNUNET_process_destroy (as->child); 361 as->child = NULL; 362 } 363 as->pst = type; 364 as->exit_code = exit_code; 365 MHD_resume_connection (as->connection); 366 as->trigger (as->trigger_cls); 367 } 368 369 370 /** 371 * Begin issuing authentication challenge to user based on @a data. 372 * I.e. start to send SMS or e-mail or launch video identification. 373 * 374 * @param as authorization state 375 * @param connection HTTP client request (for queuing response, such as redirection to video portal) 376 * @return state of the request 377 */ 378 static enum ANASTASIS_AUTHORIZATION_ChallengeResult 379 email_challenge (struct ANASTASIS_AUTHORIZATION_State *as, 380 struct MHD_Connection *connection) 381 { 382 enum MHD_Result mres; 383 const char *mime; 384 const char *lang; 385 386 /* Re-checked here and not only in #email_validate(), because truths stored 387 before that check existed would otherwise still reach the helper. */ 388 if (! is_safe_argument (as->email)) 389 { 390 GNUNET_break_op (0); 391 mres = TALER_MHD_reply_with_error (connection, 392 MHD_HTTP_INTERNAL_SERVER_ERROR, 393 TALER_EC_ANASTASIS_EMAIL_INVALID, 394 "address must not begin with `-'"); 395 if (MHD_YES != mres) 396 return ANASTASIS_AUTHORIZATION_CRES_FAILED_REPLY_FAILED; 397 return ANASTASIS_AUTHORIZATION_CRES_FAILED; 398 } 399 mime = MHD_lookup_connection_value (connection, 400 MHD_HEADER_KIND, 401 MHD_HTTP_HEADER_ACCEPT); 402 if (NULL == mime) 403 mime = "text/plain"; 404 lang = MHD_lookup_connection_value (connection, 405 MHD_HEADER_KIND, 406 MHD_HTTP_HEADER_ACCEPT_LANGUAGE); 407 if (NULL == lang) 408 lang = "en"; 409 if (NULL == as->msg) 410 { 411 /* First time, start child process and feed pipe */ 412 struct GNUNET_DISK_PipeHandle *p; 413 struct GNUNET_DISK_FileHandle *pipe_stdin; 414 415 p = GNUNET_DISK_pipe (GNUNET_DISK_PF_BLOCKING_RW); 416 if (NULL == p) 417 { 418 mres = TALER_MHD_reply_with_error (connection, 419 MHD_HTTP_INTERNAL_SERVER_ERROR, 420 TALER_EC_ANASTASIS_EMAIL_HELPER_EXEC_FAILED, 421 "pipe"); 422 if (MHD_YES != mres) 423 return ANASTASIS_AUTHORIZATION_CRES_FAILED_REPLY_FAILED; 424 return ANASTASIS_AUTHORIZATION_CRES_FAILED; 425 } 426 as->child = GNUNET_process_create (GNUNET_OS_INHERIT_STD_ERR); 427 GNUNET_assert (GNUNET_OK == 428 GNUNET_process_set_options ( 429 as->child, 430 GNUNET_process_option_inherit_rpipe (p, 431 STDIN_FILENO))); 432 if (GNUNET_OK != 433 GNUNET_process_run_command_va (as->child, 434 as->ctx->auth_command, 435 as->ctx->auth_command, 436 as->email, 437 NULL)) 438 { 439 GNUNET_process_destroy (as->child); 440 as->child = NULL; 441 GNUNET_DISK_pipe_close (p); 442 mres = TALER_MHD_reply_with_error (connection, 443 MHD_HTTP_INTERNAL_SERVER_ERROR, 444 TALER_EC_ANASTASIS_EMAIL_HELPER_EXEC_FAILED, 445 "exec"); 446 if (MHD_YES != mres) 447 return ANASTASIS_AUTHORIZATION_CRES_FAILED_REPLY_FAILED; 448 return ANASTASIS_AUTHORIZATION_CRES_FAILED; 449 } 450 pipe_stdin = GNUNET_DISK_pipe_detach_end (p, 451 GNUNET_DISK_PIPE_END_WRITE); 452 GNUNET_assert (NULL != pipe_stdin); 453 GNUNET_DISK_pipe_close (p); 454 GNUNET_asprintf (&as->msg, 455 get_message (as->ctx->messages, 456 connection, 457 "body"), 458 ANASTASIS_pin2s (as->code), 459 ANASTASIS_CRYPTO_uuid2s (&as->truth_uuid)); 460 461 { 462 const char *off = as->msg; 463 size_t left = strlen (off); 464 465 while (0 != left) 466 { 467 ssize_t ret; 468 469 ret = GNUNET_DISK_file_write (pipe_stdin, 470 off, 471 left); 472 if (ret <= 0) 473 { 474 mres = TALER_MHD_reply_with_error (connection, 475 MHD_HTTP_INTERNAL_SERVER_ERROR, 476 TALER_EC_ANASTASIS_EMAIL_HELPER_EXEC_FAILED, 477 "write"); 478 if (MHD_YES != mres) 479 return ANASTASIS_AUTHORIZATION_CRES_FAILED_REPLY_FAILED; 480 return ANASTASIS_AUTHORIZATION_CRES_FAILED; 481 } 482 as->msg_off += ret; 483 off += ret; 484 left -= ret; 485 } 486 GNUNET_DISK_file_close (pipe_stdin); 487 } 488 as->cwh = GNUNET_wait_child (as->child, 489 &email_done_cb, 490 as); 491 as->connection = connection; 492 MHD_suspend_connection (connection); 493 return ANASTASIS_AUTHORIZATION_CRES_SUSPENDED; 494 } 495 if (NULL != as->cwh) 496 { 497 /* Spurious call, why are we here? */ 498 GNUNET_break (0); 499 MHD_suspend_connection (connection); 500 return ANASTASIS_AUTHORIZATION_CRES_SUSPENDED; 501 } 502 if ( (GNUNET_OS_PROCESS_EXITED != as->pst) || 503 (0 != as->exit_code) ) 504 { 505 char es[32]; 506 507 GNUNET_snprintf (es, 508 sizeof (es), 509 "%u/%d", 510 (unsigned int) as->exit_code, 511 as->pst); 512 mres = TALER_MHD_reply_with_error (connection, 513 MHD_HTTP_INTERNAL_SERVER_ERROR, 514 TALER_EC_ANASTASIS_EMAIL_HELPER_COMMAND_FAILED, 515 es); 516 if (MHD_YES != mres) 517 return ANASTASIS_AUTHORIZATION_CRES_FAILED_REPLY_FAILED; 518 return ANASTASIS_AUTHORIZATION_CRES_FAILED; 519 } 520 521 /* Build HTTP response */ 522 { 523 struct MHD_Response *resp; 524 const char *at; 525 size_t len; 526 527 at = strchr (as->email, '@'); 528 if (NULL == at) 529 len = 0; 530 else 531 len = at - as->email; 532 533 if (0.0 < TALER_pattern_matches (mime, 534 "application/json")) 535 { 536 char *user; 537 538 user = GNUNET_strndup (as->email, 539 len); 540 resp = TALER_MHD_MAKE_JSON_PACK ( 541 GNUNET_JSON_pack_string ("challenge_type", 542 "TAN_SENT"), 543 GNUNET_JSON_pack_string ("tan_address_hint", 544 user)); 545 GNUNET_free (user); 546 } 547 else 548 { 549 size_t reply_len; 550 char *reply; 551 552 reply_len = GNUNET_asprintf (&reply, 553 get_message (as->ctx->messages, 554 connection, 555 "instructions"), 556 (unsigned int) len, 557 as->email); 558 resp = MHD_create_response_from_buffer (reply_len, 559 reply, 560 MHD_RESPMEM_MUST_COPY); 561 GNUNET_free (reply); 562 TALER_MHD_add_global_headers (resp, 563 false); 564 GNUNET_break (MHD_YES == 565 MHD_add_response_header (resp, 566 MHD_HTTP_HEADER_CONTENT_TYPE, 567 "text/plain")); 568 } 569 mres = MHD_queue_response (connection, 570 MHD_HTTP_OK, 571 resp); 572 MHD_destroy_response (resp); 573 if (MHD_YES != mres) 574 return ANASTASIS_AUTHORIZATION_CRES_SUCCESS_REPLY_FAILED; 575 return ANASTASIS_AUTHORIZATION_CRES_SUCCESS; 576 } 577 } 578 579 580 /** 581 * Free internal state associated with @a as. 582 * 583 * @param as state to clean up 584 */ 585 static void 586 email_cleanup (struct ANASTASIS_AUTHORIZATION_State *as) 587 { 588 if (NULL != as->cwh) 589 { 590 GNUNET_wait_child_cancel (as->cwh); 591 as->cwh = NULL; 592 } 593 if (NULL != as->child) 594 { 595 GNUNET_break (GNUNET_OK == 596 GNUNET_process_kill (as->child, 597 SIGKILL)); 598 GNUNET_break (GNUNET_OK == 599 GNUNET_process_wait (as->child, 600 true, 601 NULL, 602 NULL)); 603 GNUNET_process_destroy (as->child); 604 as->child = NULL; 605 } 606 GNUNET_free (as->msg); 607 GNUNET_free (as->email); 608 GNUNET_free (as); 609 } 610 611 612 /** 613 * Initialize email based authorization plugin 614 * 615 * @param cls a configuration instance 616 * @return NULL on error, otherwise a `struct ANASTASIS_AuthorizationPlugin` 617 */ 618 void * 619 libanastasis_plugin_authorization_email_init (void *cls); 620 621 /* declaration to fix compiler warning */ 622 void * 623 libanastasis_plugin_authorization_email_init (void *cls) 624 { 625 const struct ANASTASIS_AuthorizationContext *ac = cls; 626 struct ANASTASIS_AuthorizationPlugin *plugin; 627 const struct GNUNET_CONFIGURATION_Handle *cfg = ac->cfg; 628 struct Email_Context *ctx; 629 630 ctx = GNUNET_new (struct Email_Context); 631 ctx->ac = ac; 632 { 633 char *fn; 634 json_error_t err; 635 char *tmp; 636 637 tmp = GNUNET_OS_installation_get_path (ANASTASIS_project_data (), 638 GNUNET_OS_IPK_DATADIR); 639 GNUNET_asprintf (&fn, 640 "%sauthorization-email-messages.json", 641 tmp); 642 GNUNET_free (tmp); 643 ctx->messages = json_load_file (fn, 644 JSON_REJECT_DUPLICATES, 645 &err); 646 if (NULL == ctx->messages) 647 { 648 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 649 "Failed to load messages from `%s': %s at %d:%d\n", 650 fn, 651 err.text, 652 err.line, 653 err.column); 654 GNUNET_free (fn); 655 GNUNET_free (ctx); 656 return NULL; 657 } 658 GNUNET_free (fn); 659 } 660 { 661 int regex_result; 662 /* anchored so the whole address must match, not merely contain a match */ 663 const char *regexp = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"; 664 665 regex_result = regcomp (&ctx->regex, 666 regexp, 667 REG_EXTENDED); 668 if (0 < regex_result) 669 { 670 GNUNET_break (0); 671 json_decref (ctx->messages); 672 GNUNET_free (ctx); 673 return NULL; 674 } 675 } 676 677 plugin = GNUNET_new (struct ANASTASIS_AuthorizationPlugin); 678 plugin->retry_counter = INITIAL_RETRY_COUNTER; 679 plugin->code_validity_period = GNUNET_TIME_UNIT_DAYS; 680 plugin->code_rotation_period = GNUNET_TIME_UNIT_HOURS; 681 plugin->code_retransmission_frequency = GNUNET_TIME_UNIT_MINUTES; 682 plugin->cls = ctx; 683 plugin->validate = &email_validate; 684 plugin->start = &email_start; 685 plugin->challenge = &email_challenge; 686 plugin->cleanup = &email_cleanup; 687 688 if (GNUNET_OK != 689 GNUNET_CONFIGURATION_get_value_string (cfg, 690 "authorization-email", 691 "COMMAND", 692 &ctx->auth_command)) 693 { 694 GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, 695 "authorization-email", 696 "COMMAND"); 697 regfree (&ctx->regex); 698 json_decref (ctx->messages); 699 GNUNET_free (ctx); 700 GNUNET_free (plugin); 701 return NULL; 702 } 703 return plugin; 704 } 705 706 707 /** 708 * Unload authorization plugin 709 * 710 * @param cls a `struct ANASTASIS_AuthorizationPlugin` 711 * @return NULL (always) 712 */ 713 void * 714 libanastasis_plugin_authorization_email_done (void *cls); 715 716 /* declaration to fix compiler warning */ 717 void * 718 libanastasis_plugin_authorization_email_done (void *cls) 719 { 720 struct ANASTASIS_AuthorizationPlugin *plugin = cls; 721 struct Email_Context *ctx = plugin->cls; 722 723 GNUNET_free (ctx->auth_command); 724 regfree (&ctx->regex); 725 json_decref (ctx->messages); 726 GNUNET_free (ctx); 727 GNUNET_free (plugin); 728 return NULL; 729 }