Home | History | Annotate | Line # | Download | only in util
      1 /*	$NetBSD: midna_domain.c,v 1.6 2026/05/09 18:49:22 christos Exp $	*/
      2 
      3 /*++
      4 /* NAME
      5 /*	midna_domain 3
      6 /* SUMMARY
      7 /*	ASCII/UTF-8 domain name conversion
      8 /* SYNOPSIS
      9 /*	#include <midna_domain.h>
     10 /*
     11 /*	int midna_domain_cache_size;
     12 /*	int midna_domain_transitional;
     13 /*
     14 /*	const char *midna_domain_to_ascii(
     15 /*	const char *name)
     16 /*
     17 /*	const char *midna_domain_to_utf8(
     18 /*	const char *name)
     19 /*
     20 /*	const char *midna_domain_suffix_to_ascii(
     21 /*	const char *name)
     22 /*
     23 /*	const char *midna_domain_suffix_to_utf8(
     24 /*	const char *name)
     25 /* AUXILIARY FUNCTIONS
     26 /*	void midna_domain_pre_chroot(void)
     27 /* DESCRIPTION
     28 /*	The functions in this module transform domain names from/to
     29 /*	ASCII and UTF-8 form. The result is cached to avoid repeated
     30 /*	conversion.
     31 /*
     32 /*	This module builds on the ICU library implementation of the
     33 /*	UTS #46 specification, using default ICU library options
     34 /*	because those are likely best tested: with transitional
     35 /*	processing, with case mapping, with normalization, with
     36 /*	limited IDNA2003 compatibility, without STD3 ASCII rules.
     37 /*
     38 /*	midna_domain_to_ascii() converts an UTF-8 or ASCII domain
     39 /*	name to ASCII.  The result is a null pointer in case of
     40 /*	error.  This function verifies that the result passes
     41 /*	valid_hostname().
     42 /*
     43 /*	midna_domain_to_utf8() converts an UTF-8 or ASCII domain
     44 /*	name to UTF-8.  The result is a null pointer in case of
     45 /*	error.  This function verifies that the result, after
     46 /*	conversion to ASCII, passes valid_hostname().
     47 /*
     48 /*	midna_domain_suffix_to_ascii() and midna_domain_suffix_to_utf8()
     49 /*	take a name that starts with '.' and otherwise perform the
     50 /*	same operations as midna_domain_to_ascii() and
     51 /*	midna_domain_to_utf8().
     52 /*
     53 /*	midna_domain_cache_size specifies the size of the conversion
     54 /*	result cache.  This value is used only once, upon the first
     55 /*	lookup request.
     56 /*
     57 /*	midna_domain_transitional enables transitional conversion
     58 /*	between UTF8 and ASCII labels.
     59 /*
     60 /*	midna_domain_pre_chroot() does some pre-chroot initialization.
     61 /* SEE ALSO
     62 /*	https://unicode.org/reports/tr46/ Unicode IDNA Compatibility processing
     63 /*	msg(3) diagnostics interface
     64 /* DIAGNOSTICS
     65 /*	Fatal errors: memory allocation problem.
     66 /*	Warnings: conversion error or result validation error.
     67 /* LICENSE
     68 /* .ad
     69 /* .fi
     70 /*	The Secure Mailer license must be distributed with this software.
     71 /* AUTHOR(S)
     72 /*	Arnt Gulbrandsen
     73 /*
     74 /*	Wietse Venema
     75 /*	IBM T.J. Watson Research
     76 /*	P.O. Box 704
     77 /*	Yorktown Heights, NY 10598, USA
     78 /*
     79 /*	Wietse Venema
     80 /*	Google, Inc.
     81 /*	111 8th Avenue
     82 /*	New York, NY 10011, USA
     83 /*--*/
     84 
     85  /*
     86   * System library.
     87   */
     88 #include <sys_defs.h>
     89 #include <string.h>
     90 #include <ctype.h>
     91 
     92 #ifndef NO_EAI
     93 #include <unicode/uidna.h>
     94 
     95  /*
     96   * Utility library.
     97   */
     98 #include <mymalloc.h>
     99 #include <msg.h>
    100 #include <ctable.h>
    101 #include <stringops.h>
    102 #include <valid_hostname.h>
    103 #include <name_mask.h>
    104 #include <midna_domain.h>
    105 
    106  /*
    107   * Application-specific.
    108   */
    109 #define DEF_MIDNA_CACHE_SIZE	256
    110 
    111 int     midna_domain_cache_size = DEF_MIDNA_CACHE_SIZE;
    112 int     midna_domain_transitional = 0;
    113 static VSTRING *midna_domain_buf;	/* x.suffix */
    114 
    115 #define STR(x)	vstring_str(x)
    116 
    117 /* midna_domain_strerror - pick one for error reporting */
    118 
    119 static const char *midna_domain_strerror(UErrorCode error, int info_errors)
    120 {
    121 
    122     /*
    123      * XXX The UIDNA_ERROR_EMPTY_LABEL etc. names are defined in an ENUM, so
    124      * we can't use #ifdef to dynamically determine which names exist.
    125      */
    126     static LONG_NAME_MASK uidna_errors[] = {
    127 	"UIDNA_ERROR_EMPTY_LABEL", UIDNA_ERROR_EMPTY_LABEL,
    128 	"UIDNA_ERROR_LABEL_TOO_LONG", UIDNA_ERROR_LABEL_TOO_LONG,
    129 	"UIDNA_ERROR_DOMAIN_NAME_TOO_LONG", UIDNA_ERROR_DOMAIN_NAME_TOO_LONG,
    130 	"UIDNA_ERROR_LEADING_HYPHEN", UIDNA_ERROR_LEADING_HYPHEN,
    131 	"UIDNA_ERROR_TRAILING_HYPHEN", UIDNA_ERROR_TRAILING_HYPHEN,
    132 	"UIDNA_ERROR_HYPHEN_3_4", UIDNA_ERROR_HYPHEN_3_4,
    133 	"UIDNA_ERROR_LEADING_COMBINING_MARK", UIDNA_ERROR_LEADING_COMBINING_MARK,
    134 	"UIDNA_ERROR_DISALLOWED", UIDNA_ERROR_DISALLOWED,
    135 	"UIDNA_ERROR_PUNYCODE", UIDNA_ERROR_PUNYCODE,
    136 	"UIDNA_ERROR_LABEL_HAS_DOT", UIDNA_ERROR_LABEL_HAS_DOT,
    137 	"UIDNA_ERROR_INVALID_ACE_LABEL", UIDNA_ERROR_INVALID_ACE_LABEL,
    138 	"UIDNA_ERROR_BIDI", UIDNA_ERROR_BIDI,
    139 	"UIDNA_ERROR_CONTEXTJ", UIDNA_ERROR_CONTEXTJ,
    140 	/* The above errors are defined with ICU 46 and later. */
    141 	0,
    142     };
    143 
    144     if (info_errors) {
    145 	return (str_long_name_mask_opt((VSTRING *) 0, "idna error",
    146 				       uidna_errors, info_errors,
    147 				       NAME_MASK_NUMBER | NAME_MASK_COMMA));
    148     } else {
    149 	return u_errorName(error);
    150     }
    151 }
    152 
    153 /* midna_domain_pre_chroot - pre-chroot initialization */
    154 
    155 void    midna_domain_pre_chroot(void)
    156 {
    157     UErrorCode error = U_ZERO_ERROR;
    158     UIDNAInfo info = UIDNA_INFO_INITIALIZER;
    159     UIDNA  *idna;
    160 
    161     idna = uidna_openUTS46(midna_domain_transitional ? UIDNA_DEFAULT
    162 			   : UIDNA_NONTRANSITIONAL_TO_ASCII, &error);
    163     if (U_FAILURE(error))
    164 	msg_warn("ICU library initialization failed: %s",
    165 		 midna_domain_strerror(error, info.errors));
    166     uidna_close(idna);
    167 }
    168 
    169 /* midna_domain_to_ascii_create - convert domain to ASCII */
    170 
    171 static void *midna_domain_to_ascii_create(const char *name, void *unused_context)
    172 {
    173     static const char myname[] = "midna_domain_to_ascii_create";
    174     char    buf[1024];			/* XXX */
    175     UErrorCode error = U_ZERO_ERROR;
    176     UIDNAInfo info = UIDNA_INFO_INITIALIZER;
    177     UIDNA  *idna;
    178     int     anl;
    179 
    180     /*
    181      * Paranoia: do not expose uidna_*() to unfiltered network data.
    182      */
    183     if (allascii(name) == 0 && valid_utf8_stringz(name) == 0) {
    184 	msg_warn("%s: Problem translating domain \"%.100s\" to ASCII form: %s",
    185 		 myname, name, "malformed UTF-8");
    186 	return (0);
    187     }
    188 
    189     /*
    190      * Perform the requested conversion.
    191      */
    192     idna = uidna_openUTS46(midna_domain_transitional ? UIDNA_DEFAULT
    193 			   : UIDNA_NONTRANSITIONAL_TO_ASCII, &error);
    194     /* 202604 Claude: avoid null deref after uidna_openUTS46() failure. */
    195     if (idna && U_SUCCESS(error))
    196 	anl = uidna_nameToASCII_UTF8(idna,
    197 				     name, strlen(name),
    198 				     buf, sizeof(buf) - 1,
    199 				     &info,
    200 				     &error);
    201     uidna_close(idna);
    202 
    203     /*
    204      * Paranoia: verify that the result passes valid_hostname(). A quick
    205      * check shows that UTS46 ToASCII by default rejects inputs with labels
    206      * that start or end in '-', with names or labels that are over-long, or
    207      * "fake" A-labels, as required by UTS 46 section 4.1, but we rely on
    208      * valid_hostname() on the output side just to be sure.
    209      */
    210     if (idna && U_SUCCESS(error) && info.errors == 0 && anl > 0) {
    211 	buf[anl] = 0;				/* XXX */
    212 	if (!valid_hostname(buf, DONT_GRIPE)) {
    213 	    msg_warn("%s: Problem translating domain \"%.100s\" to ASCII form: %s",
    214 		     myname, name, "malformed ASCII label(s)");
    215 	    return (0);
    216 	}
    217 	return (mystrndup(buf, anl));
    218     } else {
    219 	msg_warn("%s: Problem translating domain \"%.100s\" to ASCII form: %s",
    220 		 myname, name, midna_domain_strerror(error, info.errors));
    221 	return (0);
    222     }
    223 }
    224 
    225 /* midna_domain_to_utf8_create - convert domain to UTF8 */
    226 
    227 static void *midna_domain_to_utf8_create(const char *name, void *unused_context)
    228 {
    229     static const char myname[] = "midna_domain_to_utf8_create";
    230     char    buf[1024];			/* XXX */
    231     UErrorCode error = U_ZERO_ERROR;
    232     UIDNAInfo info = UIDNA_INFO_INITIALIZER;
    233     UIDNA  *idna;
    234     int     anl;
    235 
    236     /*
    237      * Paranoia: do not expose uidna_*() to unfiltered network data.
    238      */
    239     if (allascii(name) == 0 && valid_utf8_stringz(name) == 0) {
    240 	msg_warn("%s: Problem translating domain \"%.100s\" to UTF-8 form: %s",
    241 		 myname, name, "malformed UTF-8");
    242 	return (0);
    243     }
    244 
    245     /*
    246      * Perform the requested conversion.
    247      */
    248     idna = uidna_openUTS46(midna_domain_transitional ? UIDNA_DEFAULT
    249 			   : UIDNA_NONTRANSITIONAL_TO_UNICODE, &error);
    250     /* 202604 Claude: avoid null deref after uidna_openUTS46() failure. */
    251     if (idna && U_SUCCESS(error))
    252 	anl = uidna_nameToUnicodeUTF8(idna,
    253 				      name, strlen(name),
    254 				      buf, sizeof(buf) - 1,
    255 				      &info,
    256 				      &error);
    257     uidna_close(idna);
    258 
    259     /*
    260      * Paranoia: UTS46 toUTF8 by default accepts and produces an over-long
    261      * name or a name that contains an over-long NR-LDH label (and perhaps
    262      * other invalid forms that are not covered in UTS 46, section 4.1). We
    263      * rely on midna_domain_to_ascii() to validate the output.
    264      */
    265     if (idna && U_SUCCESS(error) && info.errors == 0 && anl > 0) {
    266 	buf[anl] = 0;				/* XXX */
    267 	if (midna_domain_to_ascii(buf) == 0)
    268 	    return (0);
    269 	return (mystrndup(buf, anl));
    270     } else {
    271 	msg_warn("%s: Problem translating domain \"%.100s\" to UTF8 form: %s",
    272 		 myname, name, midna_domain_strerror(error, info.errors));
    273 	return (0);
    274     }
    275 }
    276 
    277 /* midna_domain_cache_free - cache element destructor */
    278 
    279 static void midna_domain_cache_free(void *value, void *unused_context)
    280 {
    281     if (value)
    282 	myfree(value);
    283 }
    284 
    285 /* midna_domain_to_ascii - convert name to ASCII */
    286 
    287 const char *midna_domain_to_ascii(const char *name)
    288 {
    289     static CTABLE *midna_domain_to_ascii_cache = 0;
    290 
    291     if (midna_domain_to_ascii_cache == 0)
    292 	midna_domain_to_ascii_cache = ctable_create(midna_domain_cache_size,
    293 					       midna_domain_to_ascii_create,
    294 						    midna_domain_cache_free,
    295 						    (void *) 0);
    296     return (ctable_locate(midna_domain_to_ascii_cache, name));
    297 }
    298 
    299 /* midna_domain_to_utf8 - convert name to UTF8 */
    300 
    301 const char *midna_domain_to_utf8(const char *name)
    302 {
    303     static CTABLE *midna_domain_to_utf8_cache = 0;
    304 
    305     if (midna_domain_to_utf8_cache == 0)
    306 	midna_domain_to_utf8_cache = ctable_create(midna_domain_cache_size,
    307 						midna_domain_to_utf8_create,
    308 						   midna_domain_cache_free,
    309 						   (void *) 0);
    310     return (ctable_locate(midna_domain_to_utf8_cache, name));
    311 }
    312 
    313 /* midna_domain_suffix_to_ascii - convert .name to ASCII */
    314 
    315 const char *midna_domain_suffix_to_ascii(const char *suffix)
    316 {
    317     const char *cache_res;
    318 
    319     /*
    320      * If prepending x to .name causes the result to become too long, then
    321      * the suffix is bad.
    322      */
    323     if (midna_domain_buf == 0)
    324 	midna_domain_buf = vstring_alloc(100);
    325     vstring_sprintf(midna_domain_buf, "x%s", suffix);
    326     if ((cache_res = midna_domain_to_ascii(STR(midna_domain_buf))) == 0)
    327 	return (0);
    328     else
    329 	return (cache_res + 1);
    330 }
    331 
    332 /* midna_domain_suffix_to_utf8 - convert .name to UTF8 */
    333 
    334 const char *midna_domain_suffix_to_utf8(const char *name)
    335 {
    336     const char *cache_res;
    337 
    338     /*
    339      * If prepending x to .name causes the result to become too long, then
    340      * the suffix is bad.
    341      */
    342     if (midna_domain_buf == 0)
    343 	midna_domain_buf = vstring_alloc(100);
    344     vstring_sprintf(midna_domain_buf, "x%s", name);
    345     if ((cache_res = midna_domain_to_utf8(STR(midna_domain_buf))) == 0)
    346 	return (0);
    347     else
    348 	return (cache_res + 1);
    349 }
    350 
    351 #ifdef TEST
    352 
    353  /*
    354   * Test program - reads names from stdin, reports invalid names to stderr.
    355   */
    356 #include <unistd.h>
    357 #include <stdlib.h>
    358 #include <locale.h>
    359 
    360 #include <stringops.h>			/* XXX util_utf8_enable */
    361 #include <vstring.h>
    362 #include <vstream.h>
    363 #include <vstring_vstream.h>
    364 #include <msg_vstream.h>
    365 
    366 int     main(int argc, char **argv)
    367 {
    368     VSTRING *buffer = vstring_alloc(1);
    369     const char *bp;
    370     const char *ascii;
    371     const char *utf8;
    372 
    373     if (setlocale(LC_ALL, "C") == 0)
    374 	msg_fatal("setlocale(LC_ALL, C) failed: %m");
    375 
    376     msg_vstream_init(argv[0], VSTREAM_ERR);
    377     /* msg_verbose = 1; */
    378     util_utf8_enable = 1;
    379 
    380     if (geteuid() == 0) {
    381 	midna_domain_pre_chroot();
    382 	if (chroot(".") != 0)
    383 	    msg_fatal("chroot(\".\"): %m");
    384     }
    385     while (vstring_fgets_nonl(buffer, VSTREAM_IN)) {
    386 	bp = STR(buffer);
    387 	msg_info("> %s", bp);
    388 	while (ISSPACE(*bp))
    389 	    bp++;
    390 	if (*bp == '#' || *bp == 0)
    391 	    continue;
    392 	msg_info("unconditional conversions:");
    393 	utf8 = midna_domain_to_utf8(bp);
    394 	msg_info("\"%s\" ->utf8 \"%s\"", bp, utf8 ? utf8 : "(error)");
    395 	ascii = midna_domain_to_ascii(bp);
    396 	msg_info("\"%s\" ->ascii \"%s\"", bp, ascii ? ascii : "(error)");
    397 	msg_info("conditional conversions:");
    398 	if (!allascii(bp)) {
    399 	    if (ascii != 0) {
    400 		utf8 = midna_domain_to_utf8(ascii);
    401 		msg_info("\"%s\" ->ascii \"%s\" ->utf8 \"%s\"",
    402 			 bp, ascii, utf8 ? utf8 : "(error)");
    403 		if (utf8 != 0) {
    404 		    if (strcmp(utf8, bp) != 0)
    405 			msg_warn("\"%s\" != \"%s\"", bp, utf8);
    406 		}
    407 	    }
    408 	} else {
    409 	    if (utf8 != 0) {
    410 		ascii = midna_domain_to_ascii(utf8);
    411 		msg_info("\"%s\" ->utf8 \"%s\" ->ascii \"%s\"",
    412 			 bp, utf8, ascii ? ascii : "(error)");
    413 		if (ascii != 0) {
    414 		    if (strcmp(ascii, bp) != 0)
    415 			msg_warn("\"%s\" != \"%s\"", bp, ascii);
    416 		}
    417 	    }
    418 	}
    419     }
    420     exit(0);
    421 }
    422 
    423 #endif					/* TEST */
    424 
    425 #endif					/* NO_EAI */
    426