aboutsummaryrefslogtreecommitdiff
path: root/src/authorization/anastasis_authorization_plugin_sms.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/authorization/anastasis_authorization_plugin_sms.c')
-rw-r--r--src/authorization/anastasis_authorization_plugin_sms.c607
1 files changed, 607 insertions, 0 deletions
diff --git a/src/authorization/anastasis_authorization_plugin_sms.c b/src/authorization/anastasis_authorization_plugin_sms.c
new file mode 100644
index 0000000..01b5f73
--- /dev/null
+++ b/src/authorization/anastasis_authorization_plugin_sms.c
@@ -0,0 +1,607 @@
1/*
2 This file is part of Anastasis
3 Copyright (C) 2019, 2021 Taler Systems SA
4
5 Anastasis is free software; you can redistribute it and/or modify it under the
6 terms of the GNU Lesser 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 General Public License for more details.
12
13 You should have received a copy of the GNU General Public License along with
14 Anastasis; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/>
15*/
16/**
17 * @file include/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
28
29/**
30 * Saves the State of a authorization plugin.
31 */
32struct SMS_Context
33{
34
35 /**
36 * Command which is executed to run the plugin (some bash script or a
37 * command line argument)
38 */
39 char *auth_command;
40
41 /**
42 * Regex for phone number validation.
43 */
44 regex_t regex;
45
46 /**
47 * Messages of the plugin, read from a resource file.
48 */
49 json_t *messages;
50};
51
52
53/**
54 * Saves the State of a authorization process
55 */
56struct ANASTASIS_AUTHORIZATION_State
57{
58 /**
59 * Public key of the challenge which is authorised
60 */
61 struct ANASTASIS_CRYPTO_TruthUUIDP truth_uuid;
62
63 /**
64 * Code which is sent to the user (here sent via SMS)
65 */
66 uint64_t code;
67
68 /**
69 * Our plugin context.
70 */
71 struct SMS_Context *ctx;
72
73 /**
74 * Function to call when we made progress.
75 */
76 GNUNET_SCHEDULER_TaskCallback trigger;
77
78 /**
79 * Closure for @e trigger.
80 */
81 void *trigger_cls;
82
83 /**
84 * holds the truth information
85 */
86 char *phone_number;
87
88 /**
89 * Handle to the helper process.
90 */
91 struct GNUNET_OS_Process *child;
92
93 /**
94 * Handle to wait for @e child
95 */
96 struct GNUNET_ChildWaitHandle *cwh;
97
98 /**
99 * Our client connection, set if suspended.
100 */
101 struct MHD_Connection *connection;
102
103 /**
104 * Message to send.
105 */
106 char *msg;
107
108 /**
109 * Offset of transmission in msg.
110 */
111 size_t msg_off;
112
113 /**
114 * Exit code from helper.
115 */
116 long unsigned int exit_code;
117
118 /**
119 * How did the helper die?
120 */
121 enum GNUNET_OS_ProcessStatusType pst;
122
123};
124
125
126/**
127 * Obtain internationalized message @a msg_id from @a ctx using
128 * language preferences of @a conn.
129 *
130 * @param messages JSON object to lookup message from
131 * @param conn connection to lookup message for
132 * @param msg_id unique message ID
133 * @return NULL if message was not found
134 */
135static const char *
136get_message (const json_t *messages,
137 struct MHD_Connection *conn,
138 const char *msg_id)
139{
140 const char *accept_lang;
141
142 accept_lang = MHD_lookup_connection_value (conn,
143 MHD_HEADER_KIND,
144 MHD_HTTP_HEADER_ACCEPT_LANGUAGE);
145 if (NULL == accept_lang)
146 accept_lang = "en_US";
147 {
148 const char *ret;
149 struct GNUNET_JSON_Specification spec[] = {
150 TALER_JSON_spec_i18n_string (msg_id,
151 accept_lang,
152 &ret),
153 GNUNET_JSON_spec_end ()
154 };
155
156 if (GNUNET_OK !=
157 GNUNET_JSON_parse (messages,
158 spec,
159 NULL, NULL))
160 {
161 GNUNET_break (0);
162 return NULL;
163 }
164 return ret;
165 }
166}
167
168
169/**
170 * Validate @a data is a well-formed input into the challenge method,
171 * i.e. @a data is a well-formed phone number for sending an SMS, or
172 * a well-formed e-mail address for sending an e-mail. Not expected to
173 * check that the phone number or e-mail account actually exists.
174 *
175 * To be possibly used before issuing a 402 payment required to the client.
176 *
177 * @param cls closure with a `struct SMS_Context`
178 * @param connection HTTP client request (for queuing response)
179 * @param truth_mime mime type of @e data
180 * @param data input to validate (i.e. is it a valid phone number, etc.)
181 * @param data_length number of bytes in @a data
182 * @return #GNUNET_OK if @a data is valid,
183 * #GNUNET_NO if @a data is invalid and a reply was successfully queued on @a connection
184 * #GNUNET_SYSERR if @a data invalid but we failed to queue a reply on @a connection
185 */
186static enum GNUNET_GenericReturnValue
187sms_validate (void *cls,
188 struct MHD_Connection *connection,
189 const char *truth_mime,
190 const char *data,
191 size_t data_length)
192{
193 struct SMS_Context *ctx = cls;
194 int regex_result;
195 char *phone_number;
196
197 phone_number = GNUNET_strndup (data,
198 data_length);
199 regex_result = regexec (&ctx->regex,
200 phone_number,
201 0,
202 NULL,
203 0);
204 GNUNET_free (phone_number);
205 if (0 != regex_result)
206 {
207 if (MHD_NO ==
208 TALER_MHD_reply_with_error (connection,
209 MHD_HTTP_EXPECTATION_FAILED,
210 TALER_EC_ANASTASIS_SMS_PHONE_INVALID,
211 NULL))
212 return GNUNET_SYSERR;
213 return GNUNET_NO;
214 }
215 return GNUNET_OK;
216}
217
218
219/**
220 * Begin issuing authentication challenge to user based on @a data.
221 * Sends SMS.
222 *
223 * @param cls closure with a `struct SMS_Context`
224 * @param trigger function to call when we made progress
225 * @param trigger_cls closure for @a trigger
226 * @param truth_uuid Identifier of the challenge, to be (if possible) included in the
227 * interaction with the user
228 * @param code secret code that the user has to provide back to satisfy the challenge in
229 * the main anastasis protocol
230 * @param data input to validate (i.e. is it a valid phone number, etc.)
231 * @return state to track progress on the authorization operation, NULL on failure
232 */
233static struct ANASTASIS_AUTHORIZATION_State *
234sms_start (void *cls,
235 GNUNET_SCHEDULER_TaskCallback trigger,
236 void *trigger_cls,
237 const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid,
238 uint64_t code,
239 const void *data,
240 size_t data_length)
241{
242 struct SMS_Context *ctx = cls;
243 struct ANASTASIS_AUTHORIZATION_State *as;
244
245 as = GNUNET_new (struct ANASTASIS_AUTHORIZATION_State);
246 as->trigger = trigger;
247 as->trigger_cls = trigger_cls;
248 as->ctx = ctx;
249 as->truth_uuid = *truth_uuid;
250 as->code = code;
251 as->phone_number = GNUNET_strndup (data,
252 data_length);
253 return as;
254}
255
256
257/**
258 * Function called when our SMS helper has terminated.
259 *
260 * @param cls our `struct ANASTASIS_AUHTORIZATION_State`
261 * @param type type of the process
262 * @param exit_code status code of the process
263 */
264static void
265sms_done_cb (void *cls,
266 enum GNUNET_OS_ProcessStatusType type,
267 long unsigned int exit_code)
268{
269 struct ANASTASIS_AUTHORIZATION_State *as = cls;
270
271 as->child = NULL;
272 as->cwh = NULL;
273 as->pst = type;
274 as->exit_code = exit_code;
275 MHD_resume_connection (as->connection);
276 as->trigger (as->trigger_cls);
277}
278
279
280/**
281 * Begin issuing authentication challenge to user based on @a data.
282 * I.e. start to send SMS or e-mail or launch video identification.
283 *
284 * @param as authorization state
285 * @param connection HTTP client request (for queuing response, such as redirection to video portal)
286 * @return state of the request
287 */
288static enum ANASTASIS_AUTHORIZATION_Result
289sms_process (struct ANASTASIS_AUTHORIZATION_State *as,
290 struct MHD_Connection *connection)
291{
292 MHD_RESULT mres;
293 const char *mime;
294 const char *lang;
295
296 mime = MHD_lookup_connection_value (connection,
297 MHD_HEADER_KIND,
298 MHD_HTTP_HEADER_ACCEPT);
299 if (NULL == mime)
300 mime = "text/plain";
301 lang = MHD_lookup_connection_value (connection,
302 MHD_HEADER_KIND,
303 MHD_HTTP_HEADER_ACCEPT_LANGUAGE);
304 if (NULL == lang)
305 lang = "en";
306 if (NULL == as->msg)
307 {
308 /* First time, start child process and feed pipe */
309 struct GNUNET_DISK_PipeHandle *p;
310 struct GNUNET_DISK_FileHandle *pipe_stdin;
311
312 p = GNUNET_DISK_pipe (GNUNET_DISK_PF_BLOCKING_RW);
313 if (NULL == p)
314 {
315 mres = TALER_MHD_reply_with_error (connection,
316 MHD_HTTP_INTERNAL_SERVER_ERROR,
317 TALER_EC_ANASTASIS_SMS_HELPER_EXEC_FAILED,
318 "pipe");
319 if (MHD_YES != mres)
320 return ANASTASIS_AUTHORIZATION_RES_FAILED_REPLY_FAILED;
321 return ANASTASIS_AUTHORIZATION_RES_FAILED;
322 }
323 as->child = GNUNET_OS_start_process (GNUNET_OS_INHERIT_STD_ERR,
324 p,
325 NULL,
326 NULL,
327 as->ctx->auth_command,
328 as->ctx->auth_command,
329 as->phone_number,
330 NULL);
331 if (NULL == as->child)
332 {
333 GNUNET_DISK_pipe_close (p);
334 mres = TALER_MHD_reply_with_error (connection,
335 MHD_HTTP_INTERNAL_SERVER_ERROR,
336 TALER_EC_ANASTASIS_SMS_HELPER_EXEC_FAILED,
337 "exec");
338 if (MHD_YES != mres)
339 return ANASTASIS_AUTHORIZATION_RES_FAILED_REPLY_FAILED;
340 return ANASTASIS_AUTHORIZATION_RES_FAILED;
341 }
342 pipe_stdin = GNUNET_DISK_pipe_detach_end (p,
343 GNUNET_DISK_PIPE_END_WRITE);
344 GNUNET_assert (NULL != pipe_stdin);
345 GNUNET_DISK_pipe_close (p);
346 {
347 char *tpk;
348
349 tpk = GNUNET_STRINGS_data_to_string_alloc (
350 &as->truth_uuid,
351 sizeof (as->truth_uuid));
352 GNUNET_asprintf (&as->msg,
353 "A-%llu\nAnastasis\n%s",
354 (unsigned long long) as->code,
355 tpk);
356 GNUNET_free (tpk);
357 }
358
359 {
360 const char *off = as->msg;
361 size_t left = strlen (off);
362
363 while (0 != left)
364 {
365 ssize_t ret;
366
367 if (0 == left)
368 break;
369 ret = GNUNET_DISK_file_write (pipe_stdin,
370 off,
371 left);
372 if (ret <= 0)
373 {
374 mres = TALER_MHD_reply_with_error (connection,
375 MHD_HTTP_INTERNAL_SERVER_ERROR,
376 TALER_EC_ANASTASIS_SMS_HELPER_EXEC_FAILED,
377 "write");
378 if (MHD_YES != mres)
379 return ANASTASIS_AUTHORIZATION_RES_FAILED_REPLY_FAILED;
380 return ANASTASIS_AUTHORIZATION_RES_FAILED;
381 }
382 as->msg_off += ret;
383 off += ret;
384 left -= ret;
385 }
386 GNUNET_DISK_file_close (pipe_stdin);
387 }
388 as->cwh = GNUNET_wait_child (as->child,
389 &sms_done_cb,
390 as);
391 as->connection = connection;
392 MHD_suspend_connection (connection);
393 return ANASTASIS_AUTHORIZATION_RES_SUSPENDED;
394 }
395 if (NULL != as->cwh)
396 {
397 /* Spurious call, why are we here? */
398 GNUNET_break (0);
399 MHD_suspend_connection (connection);
400 return ANASTASIS_AUTHORIZATION_RES_SUSPENDED;
401 }
402 if ( (GNUNET_OS_PROCESS_EXITED != as->pst) ||
403 (0 != as->exit_code) )
404 {
405 char es[32];
406
407 GNUNET_snprintf (es,
408 sizeof (es),
409 "%u/%d",
410 (unsigned int) as->exit_code,
411 as->pst);
412 mres = TALER_MHD_reply_with_error (connection,
413 MHD_HTTP_INTERNAL_SERVER_ERROR,
414 TALER_EC_ANASTASIS_SMS_HELPER_COMMAND_FAILED,
415 es);
416 if (MHD_YES != mres)
417 return ANASTASIS_AUTHORIZATION_RES_FAILED_REPLY_FAILED;
418 return ANASTASIS_AUTHORIZATION_RES_FAILED;
419 }
420
421 /* Build HTTP response */
422 {
423 struct MHD_Response *resp;
424 const char *end;
425 size_t slen;
426
427 slen = strlen (as->phone_number);
428 if (slen > 4)
429 end = &as->phone_number[slen - 4];
430 else
431 end = &as->phone_number[slen / 2];
432
433 if (TALER_MHD_xmime_matches (mime,
434 "application/json"))
435 {
436 json_t *body;
437
438 body = json_pack ("{s:I, s:s, s:s}",
439 "code",
440 TALER_EC_ANASTASIS_TRUTH_CHALLENGE_RESPONSE_REQUIRED,
441 "hint",
442 TALER_ErrorCode_get_hint (
443 TALER_EC_ANASTASIS_TRUTH_CHALLENGE_RESPONSE_REQUIRED),
444 "detail",
445 end);
446 GNUNET_break (NULL != body);
447 resp = TALER_MHD_make_json (body);
448 }
449 else
450 {
451 size_t reply_len;
452 char *reply;
453
454 reply_len = GNUNET_asprintf (&reply,
455 get_message (as->ctx->messages,
456 connection,
457 "instructions"),
458 end);
459 resp = MHD_create_response_from_buffer (reply_len,
460 reply,
461 MHD_RESPMEM_MUST_COPY);
462 GNUNET_free (reply);
463 TALER_MHD_add_global_headers (resp);
464 GNUNET_break (MHD_YES ==
465 MHD_add_response_header (resp,
466 MHD_HTTP_HEADER_CONTENT_TYPE,
467 "text/plain"));
468 }
469 mres = MHD_queue_response (connection,
470 MHD_HTTP_FORBIDDEN,
471 resp);
472 MHD_destroy_response (resp);
473 if (MHD_YES != mres)
474 return ANASTASIS_AUTHORIZATION_RES_SUCCESS_REPLY_FAILED;
475 return ANASTASIS_AUTHORIZATION_RES_SUCCESS;
476 }
477}
478
479
480/**
481 * Free internal state associated with @a as.
482 *
483 * @param as state to clean up
484 */
485static void
486sms_cleanup (struct ANASTASIS_AUTHORIZATION_State *as)
487{
488 if (NULL != as->cwh)
489 {
490 GNUNET_wait_child_cancel (as->cwh);
491 as->cwh = NULL;
492 }
493 if (NULL != as->child)
494 {
495 (void) GNUNET_OS_process_kill (as->child,
496 SIGKILL);
497 GNUNET_break (GNUNET_OK ==
498 GNUNET_OS_process_wait (as->child));
499 as->child = NULL;
500 }
501 GNUNET_free (as->msg);
502 GNUNET_free (as->phone_number);
503 GNUNET_free (as);
504}
505
506
507/**
508 * Initialize email based authorization plugin
509 *
510 * @param cls a configuration instance
511 * @return NULL on error, otherwise a `struct ANASTASIS_AuthorizationPlugin`
512 */
513void *
514libanastasis_plugin_authorization_sms_init (void *cls)
515{
516 struct ANASTASIS_AuthorizationPlugin *plugin;
517 struct GNUNET_CONFIGURATION_Handle *cfg = cls;
518 struct SMS_Context *ctx;
519
520 ctx = GNUNET_new (struct SMS_Context);
521 {
522 char *fn;
523 json_error_t err;
524
525 GNUNET_asprintf (&fn,
526 "%sauthorization-sms-messages.json",
527 GNUNET_OS_installation_get_path (GNUNET_OS_IPK_DATADIR));
528 ctx->messages = json_load_file (fn,
529 JSON_REJECT_DUPLICATES,
530 &err);
531 if (NULL == ctx->messages)
532 {
533 GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
534 "Failed to load messages from `%s': %s at %d:%d\n",
535 fn,
536 err.text,
537 err.line,
538 err.column);
539 GNUNET_free (fn);
540 GNUNET_free (ctx);
541 return NULL;
542 }
543 GNUNET_free (fn);
544 }
545 {
546 int regex_result;
547 const char *regexp = "^\\+?[0-9]+$";
548
549 regex_result = regcomp (&ctx->regex,
550 regexp,
551 REG_EXTENDED);
552 if (0 != regex_result)
553 {
554 GNUNET_break (0);
555 json_decref (ctx->messages);
556 GNUNET_free (ctx);
557 return NULL;
558 }
559 }
560 plugin = GNUNET_new (struct ANASTASIS_AuthorizationPlugin);
561 plugin->code_validity_period = GNUNET_TIME_UNIT_DAYS;
562 plugin->code_rotation_period = GNUNET_TIME_UNIT_HOURS;
563 plugin->code_retransmission_frequency = GNUNET_TIME_UNIT_MINUTES;
564 plugin->cls = ctx;
565 plugin->validate = &sms_validate;
566 plugin->start = &sms_start;
567 plugin->process = &sms_process;
568 plugin->cleanup = &sms_cleanup;
569
570 if (GNUNET_OK !=
571 GNUNET_CONFIGURATION_get_value_string (cfg,
572 "authorization-sms",
573 "COMMAND",
574 &ctx->auth_command))
575 {
576 GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
577 "authorization-sms",
578 "COMMAND");
579 regfree (&ctx->regex);
580 json_decref (ctx->messages);
581 GNUNET_free (ctx);
582 GNUNET_free (plugin);
583 return NULL;
584 }
585 return plugin;
586}
587
588
589/**
590 * Unload authorization plugin
591 *
592 * @param cls a `struct ANASTASIS_AuthorizationPlugin`
593 * @return NULL (always)
594 */
595void *
596libanastasis_plugin_authorization_sms_done (void *cls)
597{
598 struct ANASTASIS_AuthorizationPlugin *plugin = cls;
599 struct SMS_Context *ctx = plugin->cls;
600
601 GNUNET_free (ctx->auth_command);
602 regfree (&ctx->regex);
603 json_decref (ctx->messages);
604 GNUNET_free (ctx);
605 GNUNET_free (plugin);
606 return NULL;
607}