merchant

Merchant backend to process payments, run by merchants
Log | Files | Refs | Submodules | README | LICENSE

validators.c (10933B)


      1 /*
      2   This file is part of TALER
      3   (C) 2025 Taler Systems SA
      4 
      5   TALER 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   TALER 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   TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15 */
     16 /**
     17  * @file src/util/validators.c
     18  * @brief Input validators
     19  * @author Christian Grothoff
     20  */
     21 #include "platform.h"
     22 #include <gnunet/gnunet_util_lib.h>
     23 #include <gnunet/gnunet_db_lib.h>
     24 #include <taler/taler_json_lib.h>
     25 #include "taler/taler_merchant_util.h"
     26 #include <regex.h>
     27 
     28 bool
     29 TALER_MERCHANT_image_data_url_valid (const char *image_data_url)
     30 {
     31   if (0 == strcmp (image_data_url,
     32                    ""))
     33     return true;
     34   if (0 != strncasecmp ("data:image/",
     35                         image_data_url,
     36                         strlen ("data:image/")))
     37   {
     38     GNUNET_break_op (0);
     39     return false;
     40   }
     41   if (NULL == strstr (image_data_url,
     42                       ";base64,"))
     43   {
     44     GNUNET_break_op (0);
     45     return false;
     46   }
     47   if (! TALER_url_valid_charset (image_data_url))
     48   {
     49     GNUNET_break_op (0);
     50     return false;
     51   }
     52   return true;
     53 }
     54 
     55 
     56 bool
     57 TALER_MERCHANT_email_valid (const char *email)
     58 {
     59   regex_t regex;
     60   bool is_valid;
     61 
     62   /*
     63    * Email regex pattern supporting:
     64    *
     65    * Local part (before @):
     66    * - Dot-atom: alphanumeric, dots, hyphens, underscores
     67    *   (no leading/trailing dots, no consecutive dots)
     68    * - Quoted-string: quoted text with escaped chars inside
     69    *
     70    * Domain part (after @):
     71    * - Domain labels: alphanumeric and hyphens
     72    *   (no leading/trailing hyphens per label)
     73    * - IP literals: [IPv4] or [IPv6:...]
     74    *
     75    * Pattern breakdown:
     76    * Local part:
     77    *   ([a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+
     78    *    (\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*)
     79    *   = dot-atom (atext chars, dots allowed between parts)
     80    *
     81    *   |"([^"\\]|\\.)*"
     82    *   = quoted-string (anything in quotes with escaping)
     83    *
     84    * Domain part:
     85    *   ([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?
     86    *    (\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)
     87    *   = domain labels (63 chars max, hyphens in middle)
     88    *
     89    *   |\[([0-9]{1,3}\.){3}[0-9]{1,3}\]
     90    *   = IPv4 literal
     91    *
     92    *   |\[IPv6:[0-9a-fA-F:]+\]
     93    *   = IPv6 literal
     94    */
     95   const char *pattern =
     96     "^("
     97     /* Local part: dot-atom-text or quoted-string */
     98     "([a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(\\.)?)*[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+"
     99     "|"
    100     "\"([^\"\\\\]|\\\\.)*\""
    101     ")"
    102     "@"
    103     "("
    104     /* Domain: domain labels (with at least one dot) or IP literal */
    105     "([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+)"
    106     "|"
    107     "\\[((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}"
    108     "([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\\]"
    109     "|"
    110     "\\[IPv6:[0-9a-fA-F:]*[0-9a-fA-F]\\]"
    111     ")$";
    112 
    113   if ('\0' == email[0])
    114     return false;
    115 
    116   /* A local part may legally begin with '-', but such an address is
    117      parsed as an option by the command-line tools that deliver the
    118      mail (mail(1), sendmail(1)); the report generator hands the
    119      address to one of them.  Refuse it here as well, so that the
    120      address stored for an instance is safe for any consumer. */
    121   if ('-' == email[0])
    122   {
    123     GNUNET_break_op (0);
    124     return false;
    125   }
    126 
    127   /* Maximum email length per RFC 5321 */
    128   if (strlen (email) > 254)
    129     return false;
    130 
    131   GNUNET_assert (0 ==
    132                  regcomp (&regex,
    133                           pattern,
    134                           REG_EXTENDED | REG_NOSUB));
    135   is_valid = (0 ==
    136               regexec (&regex,
    137                        email,
    138                        0,
    139                        NULL,
    140                        0));
    141   regfree (&regex);
    142   return is_valid;
    143 }
    144 
    145 
    146 bool
    147 TALER_MERCHANT_report_target_valid (const char *target_address)
    148 {
    149   if ('\0' == target_address[0])
    150   {
    151     GNUNET_break_op (0);
    152     return false;
    153   }
    154   /* The report generator passes the target address to the generator
    155      binary as the argument of its "-t" option; the binary then hands
    156      it to a delivery tool (mail(1) for the e-mail generator), where a
    157      leading '-' would be taken for an option. */
    158   if ('-' == target_address[0])
    159   {
    160     GNUNET_break_op (0);
    161     return false;
    162   }
    163   /* The generators are shell scripts and the address may also end up
    164      in a mail header; allow printable US-ASCII only. */
    165   for (const char *p = target_address; '\0' != *p; p++)
    166     if ( (((unsigned char) *p) < 32) ||
    167          (((unsigned char) *p) > 126) )
    168     {
    169       GNUNET_break_op (0);
    170       return false;
    171     }
    172   return true;
    173 }
    174 
    175 
    176 bool
    177 TALER_MERCHANT_mime_type_valid (const char *mime_type)
    178 {
    179   regex_t regex;
    180   bool is_valid;
    181   /*
    182    * "type/subtype" of RFC 2045, Section 5.1, optionally followed by
    183    * parameters.  Both tokens use the restricted character set of the
    184    * RFC; the parameters are only required to be printable US-ASCII,
    185    * which is what matters for the consumers: the mime type is written
    186    * into an "Accept:" request header of the report generator and
    187    * passed to the generator binary on the command line.
    188    */
    189   const char *pattern =
    190     "^"
    191     "[a-zA-Z0-9][a-zA-Z0-9!#$&^_.+-]*"
    192     "/"
    193     "[a-zA-Z0-9][a-zA-Z0-9!#$&^_.+-]*"
    194     "([ \t]*;[ -~\t]*)?"
    195     "$";
    196 
    197   if ('\0' == mime_type[0])
    198   {
    199     GNUNET_break_op (0);
    200     return false;
    201   }
    202   GNUNET_assert (0 ==
    203                  regcomp (&regex,
    204                           pattern,
    205                           REG_EXTENDED | REG_NOSUB));
    206   is_valid = (0 ==
    207               regexec (&regex,
    208                        mime_type,
    209                        0,
    210                        NULL,
    211                        0));
    212   regfree (&regex);
    213   if (! is_valid)
    214     GNUNET_break_op (0);
    215   return is_valid;
    216 }
    217 
    218 
    219 char *
    220 TALER_MERCHANT_phone_validate_normalize (const char *phone,
    221                                          bool allow_letters)
    222 {
    223   if ('\0' == phone[0])
    224     return NULL;
    225 
    226   /* Maximum phone length (reasonable practical limit) */
    227   if (strlen (phone) > 30)
    228     return NULL;
    229 
    230   {
    231     regex_t regex;
    232     int ret;
    233 
    234     /*
    235      * Phone number regex pattern with +CC prefix requirement:
    236      *
    237      * Supports:
    238      * - Country codes (1-3 digits after +)
    239      * - Variable length national numbers
    240      * - Spaces, hyphens, and dots as separators
    241      * - Parentheses for area codes
    242      * - Optional letters representing digits (2-9) if allow_letters is true
    243      *
    244      * Examples:
    245      *   +1-202-555-0173
    246      *   +33 1 42 68 53 00
    247      *   +44.20.7946.0958
    248      *   +1 (202) 555-0173
    249      *   +886 2 2345 6789
    250      *   +1-800-CALL-NOW (if allow_letters is true)
    251      *
    252      * Pattern breakdown:
    253      * ^\+[0-9]{1,3}
    254      *   = Plus sign followed by 1-3 digit country code
    255      *
    256      * [-. ]?
    257      *   = Optional separator after country code
    258      *
    259      * (\([0-9]{1,4}\)[-. ]?)?
    260      *   = Optional parenthesized area code with separator
    261      *
    262      * [0-9A-Z]
    263      *   = Start with digit or letter
    264      *
    265      * ([-. ]?[0-9A-Z])*
    266      *   = Digit/letter groups with optional separators
    267      *
    268      * $
    269      *   = End of string
    270      */
    271     const char *pattern_digits =
    272       "^\\+[0-9]{1,3}"                     /* Plus and country code (1-3 digits) */
    273       "[-. ]?"                             /* Optional single separator */
    274       "("                                  /* Optional area code group */
    275       "\\([0-9]{1,4}\\)"                   /* Area code in parens */
    276       "[-. ]?"                             /* Optional separator after parens */
    277       ")?"
    278       "[0-9]"                              /* Start national number with digit */
    279       "("                                  /* National number: alternating digits and separators */
    280       "[-. ]?[0-9]"                        /* Separator optionally followed by digit */
    281       ")*"
    282       "$";
    283 
    284     const char *pattern_with_letters =
    285       "^\\+[0-9]{1,3}"                     /* Plus and country code (1-3 digits) */
    286       "[-. ]?"                             /* Optional single separator */
    287       "("                                  /* Optional area code group */
    288       "\\([0-9]{1,4}\\)"                   /* Area code in parens */
    289       "[-. ]?"                             /* Optional separator after parens */
    290       ")?"
    291       "[0-9A-Z]"                           /* Start national number with digit or letter */
    292       "("                                  /* National number: alternating digits/letters and separators */
    293       "[-. ]?[0-9A-Z]"                     /* Separator optionally followed by digit or letter */
    294       ")*"
    295       "$";
    296 
    297     const char *pattern = allow_letters
    298       ? pattern_with_letters
    299       : pattern_digits;
    300 
    301     GNUNET_assert (0 ==
    302                    regcomp (&regex,
    303                             pattern,
    304                             REG_EXTENDED | REG_NOSUB | REG_ICASE));
    305     ret = regexec (&regex,
    306                    phone, 0,
    307                    NULL, 0);
    308     regfree (&regex);
    309     if (0 != ret)
    310       return NULL; /* invalid number */
    311   }
    312 
    313   /* Phone is valid - normalize it */
    314   {
    315     char *normalized;
    316     char *out;
    317 
    318     normalized = GNUNET_malloc (strlen (phone) + 1);
    319     out = normalized;
    320     *out++ = '+';  /* Start with plus sign */
    321 
    322     for (const char *in = phone;
    323          '\0' != *in;
    324          in++)
    325     {
    326       if (isdigit ((unsigned char) *in))
    327       {
    328         /* Copy digit as-is */
    329         *out++ = *in;
    330       }
    331       else if (allow_letters && isalpha ((unsigned char) *in))
    332       {
    333         /* Convert letter to corresponding digit (A-Z maps to 2-9) */
    334         char upper = toupper ((unsigned char) *in);
    335         /* T9 keypad mapping:
    336          * 2: ABC
    337          * 3: DEF
    338          * 4: GHI
    339          * 5: JKL
    340          * 6: MNO
    341          * 7: PQRS
    342          * 8: TUV
    343          * 9: WXYZ
    344          */
    345         char digit;
    346 
    347         if (upper >= 'A' && upper <= 'C')
    348           digit = '2';
    349         else if (upper >= 'D' && upper <= 'F')
    350           digit = '3';
    351         else if (upper >= 'G' && upper <= 'I')
    352           digit = '4';
    353         else if (upper >= 'J' && upper <= 'L')
    354           digit = '5';
    355         else if (upper >= 'M' && upper <= 'O')
    356           digit = '6';
    357         else if (upper >= 'P' && upper <= 'S')
    358           digit = '7';
    359         else if (upper >= 'T' && upper <= 'V')
    360           digit = '8';
    361         else if (upper >= 'W' && upper <= 'Z')
    362           digit = '9';
    363         else
    364           digit = '0';  /* Fallback (shouldn't happen) */
    365         *out++ = digit;
    366       }
    367       /* Skip separators, parentheses, and spaces */
    368     }
    369     *out = '\0'; /* redundant, but helps analyzers... */
    370     return normalized;
    371   }
    372 }