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