anastasis_authorization_plugin_email.c (19525B)
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 * Obtain internationalized message @a msg_id from @a ctx using 140 * language preferences of @a conn. 141 * 142 * @param messages JSON object to lookup message from 143 * @param conn connection to lookup message for 144 * @param msg_id unique message ID 145 * @return NULL if message was not found 146 */ 147 static const char * 148 get_message (const json_t *messages, 149 struct MHD_Connection *conn, 150 const char *msg_id) 151 { 152 const char *accept_lang; 153 154 accept_lang = MHD_lookup_connection_value (conn, 155 MHD_HEADER_KIND, 156 MHD_HTTP_HEADER_ACCEPT_LANGUAGE); 157 if (NULL == accept_lang) 158 accept_lang = "en_US"; 159 { 160 const char *ret; 161 struct GNUNET_JSON_Specification spec[] = { 162 TALER_JSON_spec_i18n_string (msg_id, 163 accept_lang, 164 &ret), 165 GNUNET_JSON_spec_end () 166 }; 167 168 if (GNUNET_OK != 169 GNUNET_JSON_parse (messages, 170 spec, 171 NULL, NULL)) 172 { 173 GNUNET_break (0); 174 GNUNET_JSON_parse_free (spec); 175 return NULL; 176 } 177 GNUNET_JSON_parse_free (spec); 178 return ret; 179 } 180 } 181 182 183 /** 184 * Validate @a data is a well-formed input into the challenge method, 185 * i.e. @a data is a well-formed phone number for sending an SMS, or 186 * a well-formed e-mail address for sending an e-mail. Not expected to 187 * check that the phone number or e-mail account actually exists. 188 * 189 * To be possibly used before issuing a 402 payment required to the client. 190 * 191 * @param cls closure 192 * @param connection HTTP client request (for queuing response) 193 * @param mime_type mime type of @e data 194 * @param data input to validate (i.e. is it a valid phone number, etc.) 195 * @param data_length number of bytes in @a data 196 * @return #GNUNET_OK if @a data is valid, 197 * #GNUNET_NO if @a data is invalid and a reply was successfully queued on @a connection 198 * #GNUNET_SYSERR if @a data invalid but we failed to queue a reply on @a connection 199 */ 200 static enum GNUNET_GenericReturnValue 201 email_validate (void *cls, 202 struct MHD_Connection *connection, 203 const char *mime_type, 204 const char *data, 205 size_t data_length) 206 { 207 struct Email_Context *ctx = cls; 208 int regex_result; 209 char *phone_number; 210 211 phone_number = GNUNET_strndup (data, 212 data_length); 213 regex_result = regexec (&ctx->regex, 214 phone_number, 215 0, 216 NULL, 217 0); 218 GNUNET_free (phone_number); 219 if (0 != regex_result) 220 { 221 if (MHD_NO == 222 TALER_MHD_reply_with_error (connection, 223 MHD_HTTP_CONFLICT, 224 TALER_EC_ANASTASIS_EMAIL_INVALID, 225 NULL)) 226 return GNUNET_SYSERR; 227 return GNUNET_NO; 228 } 229 return GNUNET_OK; 230 } 231 232 233 /** 234 * Begin issuing authentication challenge to user based on @a data. 235 * I.e. start to send SMS or e-mail or launch video identification. 236 * 237 * @param cls closure 238 * @param trigger function to call when we made progress 239 * @param trigger_cls closure for @a trigger 240 * @param truth_uuid Identifier of the challenge, to be (if possible) included in the 241 * interaction with the user 242 * @param code secret code that the user has to provide back to satisfy the challenge in 243 * the main anastasis protocol 244 * @param data input to validate (i.e. is it a valid phone number, etc.) 245 * @param data_length number of bytes in @a data 246 * @return state to track progress on the authorization operation, NULL on failure 247 */ 248 static struct ANASTASIS_AUTHORIZATION_State * 249 email_start (void *cls, 250 GNUNET_SCHEDULER_TaskCallback trigger, 251 void *trigger_cls, 252 const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, 253 uint64_t code, 254 const void *data, 255 size_t data_length) 256 { 257 struct Email_Context *ctx = cls; 258 struct ANASTASIS_AUTHORIZATION_State *as; 259 enum GNUNET_DB_QueryStatus qs; 260 261 /* If the user can show this challenge code, this 262 plugin is already happy (no additional 263 requirements), so mark this challenge as 264 already satisfied from the start. */ 265 qs = ctx->ac->db->mark_challenge_code_satisfied (ctx->ac->db->cls, 266 truth_uuid, 267 code); 268 if (qs <= 0) 269 { 270 GNUNET_break (0); 271 return NULL; 272 } 273 as = GNUNET_new (struct ANASTASIS_AUTHORIZATION_State); 274 as->trigger = trigger; 275 as->trigger_cls = trigger_cls; 276 as->ctx = ctx; 277 as->truth_uuid = *truth_uuid; 278 as->code = code; 279 as->email = GNUNET_strndup (data, 280 data_length); 281 return as; 282 } 283 284 285 /** 286 * Function called when our Email helper has terminated. 287 * 288 * @param cls our `struct ANASTASIS_AUHTORIZATION_State` 289 * @param type type of the process 290 * @param exit_code status code of the process 291 */ 292 static void 293 email_done_cb (void *cls, 294 enum GNUNET_OS_ProcessStatusType type, 295 long unsigned int exit_code) 296 { 297 struct ANASTASIS_AUTHORIZATION_State *as = cls; 298 299 as->cwh = NULL; 300 if (NULL != as->child) 301 { 302 GNUNET_process_destroy (as->child); 303 as->child = NULL; 304 } 305 as->pst = type; 306 as->exit_code = exit_code; 307 MHD_resume_connection (as->connection); 308 as->trigger (as->trigger_cls); 309 } 310 311 312 /** 313 * Begin issuing authentication challenge to user based on @a data. 314 * I.e. start to send SMS or e-mail or launch video identification. 315 * 316 * @param as authorization state 317 * @param connection HTTP client request (for queuing response, such as redirection to video portal) 318 * @return state of the request 319 */ 320 static enum ANASTASIS_AUTHORIZATION_ChallengeResult 321 email_challenge (struct ANASTASIS_AUTHORIZATION_State *as, 322 struct MHD_Connection *connection) 323 { 324 MHD_RESULT mres; 325 const char *mime; 326 const char *lang; 327 328 mime = MHD_lookup_connection_value (connection, 329 MHD_HEADER_KIND, 330 MHD_HTTP_HEADER_ACCEPT); 331 if (NULL == mime) 332 mime = "text/plain"; 333 lang = MHD_lookup_connection_value (connection, 334 MHD_HEADER_KIND, 335 MHD_HTTP_HEADER_ACCEPT_LANGUAGE); 336 if (NULL == lang) 337 lang = "en"; 338 if (NULL == as->msg) 339 { 340 /* First time, start child process and feed pipe */ 341 struct GNUNET_DISK_PipeHandle *p; 342 struct GNUNET_DISK_FileHandle *pipe_stdin; 343 344 p = GNUNET_DISK_pipe (GNUNET_DISK_PF_BLOCKING_RW); 345 if (NULL == p) 346 { 347 mres = TALER_MHD_reply_with_error (connection, 348 MHD_HTTP_INTERNAL_SERVER_ERROR, 349 TALER_EC_ANASTASIS_EMAIL_HELPER_EXEC_FAILED, 350 "pipe"); 351 if (MHD_YES != mres) 352 return ANASTASIS_AUTHORIZATION_CRES_FAILED_REPLY_FAILED; 353 return ANASTASIS_AUTHORIZATION_CRES_FAILED; 354 } 355 as->child = GNUNET_process_create (GNUNET_OS_INHERIT_STD_ERR); 356 GNUNET_assert (GNUNET_OK == 357 GNUNET_process_set_options ( 358 as->child, 359 GNUNET_process_option_inherit_rpipe (p, 360 STDIN_FILENO))); 361 if (GNUNET_OK != 362 GNUNET_process_run_command_va (as->child, 363 as->ctx->auth_command, 364 as->ctx->auth_command, 365 as->email, 366 NULL)) 367 { 368 GNUNET_process_destroy (as->child); 369 as->child = NULL; 370 GNUNET_DISK_pipe_close (p); 371 mres = TALER_MHD_reply_with_error (connection, 372 MHD_HTTP_INTERNAL_SERVER_ERROR, 373 TALER_EC_ANASTASIS_EMAIL_HELPER_EXEC_FAILED, 374 "exec"); 375 if (MHD_YES != mres) 376 return ANASTASIS_AUTHORIZATION_CRES_FAILED_REPLY_FAILED; 377 return ANASTASIS_AUTHORIZATION_CRES_FAILED; 378 } 379 pipe_stdin = GNUNET_DISK_pipe_detach_end (p, 380 GNUNET_DISK_PIPE_END_WRITE); 381 GNUNET_assert (NULL != pipe_stdin); 382 GNUNET_DISK_pipe_close (p); 383 GNUNET_asprintf (&as->msg, 384 get_message (as->ctx->messages, 385 connection, 386 "body"), 387 ANASTASIS_pin2s (as->code), 388 ANASTASIS_CRYPTO_uuid2s (&as->truth_uuid)); 389 390 { 391 const char *off = as->msg; 392 size_t left = strlen (off); 393 394 while (0 != left) 395 { 396 ssize_t ret; 397 398 ret = GNUNET_DISK_file_write (pipe_stdin, 399 off, 400 left); 401 if (ret <= 0) 402 { 403 mres = TALER_MHD_reply_with_error (connection, 404 MHD_HTTP_INTERNAL_SERVER_ERROR, 405 TALER_EC_ANASTASIS_EMAIL_HELPER_EXEC_FAILED, 406 "write"); 407 if (MHD_YES != mres) 408 return ANASTASIS_AUTHORIZATION_CRES_FAILED_REPLY_FAILED; 409 return ANASTASIS_AUTHORIZATION_CRES_FAILED; 410 } 411 as->msg_off += ret; 412 off += ret; 413 left -= ret; 414 } 415 GNUNET_DISK_file_close (pipe_stdin); 416 } 417 as->cwh = GNUNET_wait_child (as->child, 418 &email_done_cb, 419 as); 420 as->connection = connection; 421 MHD_suspend_connection (connection); 422 return ANASTASIS_AUTHORIZATION_CRES_SUSPENDED; 423 } 424 if (NULL != as->cwh) 425 { 426 /* Spurious call, why are we here? */ 427 GNUNET_break (0); 428 MHD_suspend_connection (connection); 429 return ANASTASIS_AUTHORIZATION_CRES_SUSPENDED; 430 } 431 if ( (GNUNET_OS_PROCESS_EXITED != as->pst) || 432 (0 != as->exit_code) ) 433 { 434 char es[32]; 435 436 GNUNET_snprintf (es, 437 sizeof (es), 438 "%u/%d", 439 (unsigned int) as->exit_code, 440 as->pst); 441 mres = TALER_MHD_reply_with_error (connection, 442 MHD_HTTP_INTERNAL_SERVER_ERROR, 443 TALER_EC_ANASTASIS_EMAIL_HELPER_COMMAND_FAILED, 444 es); 445 if (MHD_YES != mres) 446 return ANASTASIS_AUTHORIZATION_CRES_FAILED_REPLY_FAILED; 447 return ANASTASIS_AUTHORIZATION_CRES_FAILED; 448 } 449 450 /* Build HTTP response */ 451 { 452 struct MHD_Response *resp; 453 const char *at; 454 size_t len; 455 456 at = strchr (as->email, '@'); 457 if (NULL == at) 458 len = 0; 459 else 460 len = at - as->email; 461 462 if (0.0 < TALER_pattern_matches (mime, 463 "application/json")) 464 { 465 char *user; 466 467 user = GNUNET_strndup (as->email, 468 len); 469 resp = TALER_MHD_MAKE_JSON_PACK ( 470 GNUNET_JSON_pack_string ("challenge_type", 471 "TAN_SENT"), 472 GNUNET_JSON_pack_string ("tan_address_hint", 473 user)); 474 GNUNET_free (user); 475 } 476 else 477 { 478 size_t reply_len; 479 char *reply; 480 481 reply_len = GNUNET_asprintf (&reply, 482 get_message (as->ctx->messages, 483 connection, 484 "instructions"), 485 (unsigned int) len, 486 as->email); 487 resp = MHD_create_response_from_buffer (reply_len, 488 reply, 489 MHD_RESPMEM_MUST_COPY); 490 GNUNET_free (reply); 491 TALER_MHD_add_global_headers (resp, 492 false); 493 GNUNET_break (MHD_YES == 494 MHD_add_response_header (resp, 495 MHD_HTTP_HEADER_CONTENT_TYPE, 496 "text/plain")); 497 } 498 mres = MHD_queue_response (connection, 499 MHD_HTTP_OK, 500 resp); 501 MHD_destroy_response (resp); 502 if (MHD_YES != mres) 503 return ANASTASIS_AUTHORIZATION_CRES_SUCCESS_REPLY_FAILED; 504 return ANASTASIS_AUTHORIZATION_CRES_SUCCESS; 505 } 506 } 507 508 509 /** 510 * Free internal state associated with @a as. 511 * 512 * @param as state to clean up 513 */ 514 static void 515 email_cleanup (struct ANASTASIS_AUTHORIZATION_State *as) 516 { 517 if (NULL != as->cwh) 518 { 519 GNUNET_wait_child_cancel (as->cwh); 520 as->cwh = NULL; 521 } 522 if (NULL != as->child) 523 { 524 GNUNET_break (GNUNET_OK == 525 GNUNET_process_kill (as->child, 526 SIGKILL)); 527 GNUNET_break (GNUNET_OK == 528 GNUNET_process_wait (as->child, 529 true, 530 NULL, 531 NULL)); 532 GNUNET_process_destroy (as->child); 533 as->child = NULL; 534 } 535 GNUNET_free (as->msg); 536 GNUNET_free (as->email); 537 GNUNET_free (as); 538 } 539 540 541 /** 542 * Initialize email based authorization plugin 543 * 544 * @param cls a configuration instance 545 * @return NULL on error, otherwise a `struct ANASTASIS_AuthorizationPlugin` 546 */ 547 void * 548 libanastasis_plugin_authorization_email_init (void *cls); 549 550 /* declaration to fix compiler warning */ 551 void * 552 libanastasis_plugin_authorization_email_init (void *cls) 553 { 554 const struct ANASTASIS_AuthorizationContext *ac = cls; 555 struct ANASTASIS_AuthorizationPlugin *plugin; 556 const struct GNUNET_CONFIGURATION_Handle *cfg = ac->cfg; 557 struct Email_Context *ctx; 558 559 ctx = GNUNET_new (struct Email_Context); 560 ctx->ac = ac; 561 { 562 char *fn; 563 json_error_t err; 564 char *tmp; 565 566 tmp = GNUNET_OS_installation_get_path (ANASTASIS_project_data (), 567 GNUNET_OS_IPK_DATADIR); 568 GNUNET_asprintf (&fn, 569 "%sauthorization-email-messages.json", 570 tmp); 571 GNUNET_free (tmp); 572 ctx->messages = json_load_file (fn, 573 JSON_REJECT_DUPLICATES, 574 &err); 575 if (NULL == ctx->messages) 576 { 577 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 578 "Failed to load messages from `%s': %s at %d:%d\n", 579 fn, 580 err.text, 581 err.line, 582 err.column); 583 GNUNET_free (fn); 584 GNUNET_free (ctx); 585 return NULL; 586 } 587 GNUNET_free (fn); 588 } 589 { 590 int regex_result; 591 const char *regexp = "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,4}"; 592 593 regex_result = regcomp (&ctx->regex, 594 regexp, 595 REG_EXTENDED); 596 if (0 < regex_result) 597 { 598 GNUNET_break (0); 599 json_decref (ctx->messages); 600 GNUNET_free (ctx); 601 return NULL; 602 } 603 } 604 605 plugin = GNUNET_new (struct ANASTASIS_AuthorizationPlugin); 606 plugin->retry_counter = INITIAL_RETRY_COUNTER; 607 plugin->code_validity_period = GNUNET_TIME_UNIT_DAYS; 608 plugin->code_rotation_period = GNUNET_TIME_UNIT_HOURS; 609 plugin->code_retransmission_frequency = GNUNET_TIME_UNIT_MINUTES; 610 plugin->cls = ctx; 611 plugin->validate = &email_validate; 612 plugin->start = &email_start; 613 plugin->challenge = &email_challenge; 614 plugin->cleanup = &email_cleanup; 615 616 if (GNUNET_OK != 617 GNUNET_CONFIGURATION_get_value_string (cfg, 618 "authorization-email", 619 "COMMAND", 620 &ctx->auth_command)) 621 { 622 GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, 623 "authorization-email", 624 "COMMAND"); 625 regfree (&ctx->regex); 626 json_decref (ctx->messages); 627 GNUNET_free (ctx); 628 GNUNET_free (plugin); 629 return NULL; 630 } 631 return plugin; 632 } 633 634 635 /** 636 * Unload authorization plugin 637 * 638 * @param cls a `struct ANASTASIS_AuthorizationPlugin` 639 * @return NULL (always) 640 */ 641 void * 642 libanastasis_plugin_authorization_email_done (void *cls); 643 644 /* declaration to fix compiler warning */ 645 void * 646 libanastasis_plugin_authorization_email_done (void *cls) 647 { 648 struct ANASTASIS_AuthorizationPlugin *plugin = cls; 649 struct Email_Context *ctx = plugin->cls; 650 651 GNUNET_free (ctx->auth_command); 652 regfree (&ctx->regex); 653 json_decref (ctx->messages); 654 GNUNET_free (ctx); 655 GNUNET_free (plugin); 656 return NULL; 657 }