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 (®ex, 133 pattern, 134 REG_EXTENDED | REG_NOSUB)); 135 is_valid = (0 == 136 regexec (®ex, 137 email, 138 0, 139 NULL, 140 0)); 141 regfree (®ex); 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 (®ex, 204 pattern, 205 REG_EXTENDED | REG_NOSUB)); 206 is_valid = (0 == 207 regexec (®ex, 208 mime_type, 209 0, 210 NULL, 211 0)); 212 regfree (®ex); 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 (®ex, 303 pattern, 304 REG_EXTENDED | REG_NOSUB | REG_ICASE)); 305 ret = regexec (®ex, 306 phone, 0, 307 NULL, 0); 308 regfree (®ex); 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 }