/* $OpenBSD: util.c,v 1.157 2024/05/02 18:14:33 op Exp $ */ /* * Copyright (c) 2000,2001 Markus Friedl. All rights reserved. * Copyright (c) 2008 Gilles Chehade * Copyright (c) 2009 Jacek Masiulaniec * Copyright (c) 2012 Eric Faurot * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include "smtpd.h" #include "log.h" const char *log_in6addr(const struct in6_addr *); const char *log_sockaddr(struct sockaddr *); static int parse_mailname_file(char *, size_t); int tracing = 0; int foreground_log = 0; void * xmalloc(size_t size) { void *r; if ((r = malloc(size)) == NULL) fatal("malloc"); return (r); } void * xcalloc(size_t nmemb, size_t size) { void *r; if ((r = calloc(nmemb, size)) == NULL) fatal("calloc"); return (r); } char * xstrdup(const char *str) { char *r; if ((r = strdup(str)) == NULL) fatal("strdup"); return (r); } void * xmemdup(const void *ptr, size_t size) { void *r; if ((r = malloc(size)) == NULL) fatal("malloc"); memmove(r, ptr, size); return (r); } int xasprintf(char **ret, const char *format, ...) { int r; va_list ap; va_start(ap, format); r = vasprintf(ret, format, ap); va_end(ap); if (r == -1) fatal("vasprintf"); return (r); } #if !defined(NO_IO) int io_xprintf(struct io *io, const char *fmt, ...) { va_list ap; int len; va_start(ap, fmt); len = io_vprintf(io, fmt, ap); va_end(ap); if (len == -1) fatal("io_xprintf(%p, %s, ...)", io, fmt); return len; } int io_xprint(struct io *io, const char *str) { int len; len = io_print(io, str); if (len == -1) fatal("io_xprint(%p, %s, ...)", io, str); return len; } #endif char * strip(char *s) { size_t l; while (isspace((unsigned char)*s)) s++; for (l = strlen(s); l; l--) { if (!isspace((unsigned char)s[l-1])) break; s[l-1] = '\0'; } return (s); } int bsnprintf(char *str, size_t size, const char *format, ...) { int ret; va_list ap; va_start(ap, format); ret = vsnprintf(str, size, format, ap); va_end(ap); if (ret < 0 || (size_t)ret >= size) return 0; return 1; } int ckdir(const char *path, mode_t mode, uid_t owner, gid_t group, int create) { char mode_str[12]; int ret; struct stat sb; if (stat(path, &sb) == -1) { if (errno != ENOENT || create == 0) { log_warn("stat: %s", path); return (0); } /* chmod is deferred to avoid umask effect */ if (mkdir(path, 0) == -1) { log_warn("mkdir: %s", path); return (0); } if (chown(path, owner, group) == -1) { log_warn("chown: %s", path); return (0); } if (chmod(path, mode) == -1) { log_warn("chmod: %s", path); return (0); } if (stat(path, &sb) == -1) { log_warn("stat: %s", path); return (0); } } ret = 1; /* check if it's a directory */ if (!S_ISDIR(sb.st_mode)) { ret = 0; log_warnx("%s is not a directory", path); } /* check that it is owned by owner/group */ if (sb.st_uid != owner) { ret = 0; log_warnx("%s is not owned by uid %d", path, owner); } if (sb.st_gid != group) { ret = 0; log_warnx("%s is not owned by gid %d", path, group); } /* check permission */ if ((sb.st_mode & 07777) != mode) { ret = 0; strmode(mode, mode_str); mode_str[10] = '\0'; log_warnx("%s must be %s (%o)", path, mode_str + 1, mode); } return ret; } int rmtree(char *path, int keepdir) { char *path_argv[2]; FTS *fts; FTSENT *e; int ret, depth; path_argv[0] = path; path_argv[1] = NULL; ret = 0; depth = 0; fts = fts_open(path_argv, FTS_PHYSICAL | FTS_NOCHDIR, NULL); if (fts == NULL) { log_warn("fts_open: %s", path); return (-1); } while ((e = fts_read(fts)) != NULL) { switch (e->fts_info) { case FTS_D: depth++; break; case FTS_DP: case FTS_DNR: depth--; if (keepdir && depth == 0) continue; if (rmdir(e->fts_path) == -1) { log_warn("rmdir: %s", e->fts_path); ret = -1; } break; case FTS_F: if (unlink(e->fts_path) == -1) { log_warn("unlink: %s", e->fts_path); ret = -1; } } } fts_close(fts); return (ret); } int mvpurge(char *from, char *to) { size_t n; int retry; const char *sep; char buf[PATH_MAX]; if ((n = strlen(to)) == 0) fatalx("to is empty"); sep = (to[n - 1] == '/') ? "" : "/"; retry = 0; again: (void)snprintf(buf, sizeof buf, "%s%s%u", to, sep, arc4random()); if (rename(from, buf) == -1) { /* ENOTDIR has actually 2 meanings, and incorrect input * could lead to an infinite loop. Consider that after * 20 tries something is hopelessly wrong. */ if (errno == ENOTEMPTY || errno == EISDIR || errno == ENOTDIR) { if ((retry++) >= 20) return (-1); goto again; } return -1; } return 0; } int mktmpfile(void) { char path[PATH_MAX]; int fd; if (!bsnprintf(path, sizeof(path), "%s/smtpd.XXXXXXXXXX", PATH_TEMPORARY)) { log_warn("snprintf"); fatal("exiting"); } if ((fd = mkstemp(path)) == -1) { log_warn("cannot create temporary file %s", path); fatal("exiting"); } unlink(path); return (fd); } /* Close file, signifying temporary error condition (if any) to the caller. */ int safe_fclose(FILE *fp) { if (ferror(fp)) { fclose(fp); return 0; } if (fflush(fp)) { fclose(fp); if (errno == ENOSPC) return 0; fatal("safe_fclose: fflush"); } if (fsync(fileno(fp))) fatal("safe_fclose: fsync"); if (fclose(fp)) fatal("safe_fclose: fclose"); return 1; } int hostname_match(const char *hostname, const char *pattern) { while (*pattern != '\0' && *hostname != '\0') { if (*pattern == '*') { while (*pattern == '*') pattern++; while (*hostname != '\0' && tolower((unsigned char)*hostname) != tolower((unsigned char)*pattern)) hostname++; continue; } if (tolower((unsigned char)*pattern) != tolower((unsigned char)*hostname)) return 0; pattern++; hostname++; } return (*hostname == '\0' && *pattern == '\0'); } int mailaddr_match(const struct mailaddr *maddr1, const struct mailaddr *maddr2) { struct mailaddr m1 = *maddr1; struct mailaddr m2 = *maddr2; char *p; /* catchall */ if (m2.user[0] == '\0' && m2.domain[0] == '\0') return 1; if (m2.domain[0] && !hostname_match(m1.domain, m2.domain)) return 0; if (m2.user[0]) { /* if address from table has a tag, we must respect it */ if (strchr(m2.user, *env->sc_subaddressing_delim) == NULL) { /* otherwise, strip tag from session address if any */ p = strchr(m1.user, *env->sc_subaddressing_delim); if (p) *p = '\0'; } if (strcasecmp(m1.user, m2.user)) return 0; } return 1; } int valid_localpart(const char *s) { #define IS_ATEXT(c) (isalnum((unsigned char)(c)) || strchr(MAILADDR_ALLOWED, (c))) nextatom: if (!IS_ATEXT(*s) || *s == '\0') return 0; while (*(++s) != '\0') { if (*s == '.') break; if (IS_ATEXT(*s)) continue; return 0; } if (*s == '.') { s++; goto nextatom; } return 1; } int valid_domainpart(const char *s) { struct in_addr ina; struct in6_addr ina6; char *c, domain[SMTPD_MAXDOMAINPARTSIZE]; const char *p; size_t dlen; if (*s == '[') { if (strncasecmp("[IPv6:", s, 6) == 0) p = s + 6; else p = s + 1; if (strlcpy(domain, p, sizeof domain) >= sizeof domain) return 0; c = strchr(domain, ']'); if (!c || c[1] != '\0') return 0; *c = '\0'; if (inet_pton(AF_INET6, domain, &ina6) == 1) return 1; if (inet_pton(AF_INET, domain, &ina) == 1) return 1; return 0; } if (*s == '\0') return 0; dlen = strlen(s); if (dlen >= sizeof domain) return 0; if (s[dlen - 1] == '.') return 0; return res_hnok(s); } #define LABELCHR(c) ((c) == '-' || (c) == '_' || isalpha((unsigned char)(c)) || isdigit((unsigned char)(c))) #define LABELMAX 63 #define DNAMEMAX 253 int valid_domainname(const char *str) { const char *label, *s; /* * Expect a sequence of dot-separated labels, possibly with a trailing * dot. The empty string is rejected, as well a single dot. */ for (s = str; *s; s++) { /* Start of a new label. */ label = s; while (LABELCHR(*s)) s++; /* Must have at least one char and at most LABELMAX. */ if (s == label || s - label > LABELMAX) return 0; /* If last label, stop here. */ if (*s == '\0') break; /* Expect a dot as label separator or last char. */ if (*s != '.') return 0; } /* Must have at leat one label and no more than DNAMEMAX chars. */ if (s == str || s - str > DNAMEMAX) return 0; return 1; } int valid_smtp_response(const char *s) { if (strlen(s) < 5) return 0; if ((s[0] < '2' || s[0] > '5') || (s[1] < '0' || s[1] > '9') || (s[2] < '0' || s[2] > '9') || (s[3] != ' ')) return 0; return 1; } int valid_xtext(const char *s) { for (; *s != '\0'; ++s) { if (*s < '!' || *s > '~' || *s == '=') return 0; if (*s != '+') continue; s++; if (!isdigit((unsigned char)*s) && !(*s >= 'A' && *s <= 'F')) return 0; s++; if (!isdigit((unsigned char)*s) && !(*s >= 'A' && *s <= 'F')) return 0; } return 1; } int secure_file(int fd, char *path, char *userdir, uid_t uid, int mayread) { char buf[PATH_MAX]; char homedir[PATH_MAX]; struct stat st; char *cp; if (realpath(path, buf) == NULL) return 0; if (realpath(userdir, homedir) == NULL) homedir[0] = '\0'; /* Check the open file to avoid races. */ if (fstat(fd, &st) == -1 || !S_ISREG(st.st_mode) || st.st_uid != uid || (st.st_mode & (mayread ? 022 : 066)) != 0) return 0; /* For each component of the canonical path, walking upwards. */ for (;;) { if ((cp = dirname(buf)) == NULL) return 0; (void)strlcpy(buf, cp, sizeof(buf)); if (stat(buf, &st) == -1 || (st.st_uid != 0 && st.st_uid != uid) || (st.st_mode & 022) != 0) return 0; /* We can stop checking after reaching homedir level. */ if (strcmp(homedir, buf) == 0) break; /* * dirname should always complete with a "/" path, * but we can be paranoid and check for "." too */ if ((strcmp("/", buf) == 0) || (strcmp(".", buf) == 0)) break; } return 1; } void addargs(arglist *args, char *fmt, ...) { va_list ap; char *cp; uint nalloc; int r; char **tmp; va_start(ap, fmt); r = vasprintf(&cp, fmt, ap); va_end(ap); if (r == -1) fatal("addargs: argument too long"); nalloc = args->nalloc; if (args->list == NULL) { nalloc = 32; args->num = 0; } else if (args->num+2 >= nalloc) nalloc *= 2; tmp = reallocarray(args->list, nalloc, sizeof(char *)); if (tmp == NULL) fatal("addargs: reallocarray"); args->list = tmp; args->nalloc = nalloc; args->list[args->num++] = cp; args->list[args->num] = NULL; } int lowercase(char *buf, const char *s, size_t len) { if (len == 0) return 0; if (strlcpy(buf, s, len) >= len) return 0; while (*buf != '\0') { *buf = tolower((unsigned char)*buf); buf++; } return 1; } int uppercase(char *buf, const char *s, size_t len) { if (len == 0) return 0; if (strlcpy(buf, s, len) >= len) return 0; while (*buf != '\0') { *buf = toupper((unsigned char)*buf); buf++; } return 1; } void xlowercase(char *buf, const char *s, size_t len) { if (len == 0) fatalx("lowercase: len == 0"); if (!lowercase(buf, s, len)) fatalx("lowercase: truncation"); } uint64_t generate_uid(void) { static uint32_t id; static uint8_t inited; uint64_t uid; if (!inited) { id = arc4random(); inited = 1; } while ((uid = ((uint64_t)(id++) << 32 | arc4random())) == 0) ; return (uid); } int session_socket_error(int fd) { int error; socklen_t len; len = sizeof(error); if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &len) == -1) fatal("session_socket_error: getsockopt"); return (error); } const char * parse_smtp_response(char *line, size_t len, char **msg, int *cont) { if (len >= LINE_MAX) return "line too long"; if (len > 3) { if (msg) *msg = line + 4; if (cont) *cont = (line[3] == '-'); } else if (len == 3) { if (msg) *msg = line + 3; if (cont) *cont = 0; } else return "line too short"; /* validate reply code */ if (line[0] < '2' || line[0] > '5' || !isdigit((unsigned char)line[1]) || !isdigit((unsigned char)line[2])) return "reply code out of range"; return NULL; } static int parse_mailname_file(char *hostname, size_t len) { FILE *fp; char *buf = NULL; size_t bufsz = 0; ssize_t buflen; if ((fp = fopen(MAILNAME_FILE, "r")) == NULL) return 1; if ((buflen = getline(&buf, &bufsz, fp)) == -1) goto error; if (buf[buflen - 1] == '\n') buf[buflen - 1] = '\0'; if (strlcpy(hostname, buf, len) >= len) { fprintf(stderr, MAILNAME_FILE " entry too long"); goto error; } return 0; error: fclose(fp); free(buf); return 1; } int getmailname(char *hostname, size_t len) { struct addrinfo hints, *res = NULL; int error; /* Try MAILNAME_FILE first */ if (parse_mailname_file(hostname, len) == 0) return 0; /* Next, gethostname(3) */ if (gethostname(hostname, len) == -1) { fprintf(stderr, "getmailname: gethostname() failed\n"); return -1; } if (strchr(hostname, '.') != NULL) return 0; /* Canonicalize if domain part is missing */ memset(&hints, 0, sizeof hints); hints.ai_family = PF_UNSPEC; hints.ai_flags = AI_CANONNAME; error = getaddrinfo(hostname, NULL, &hints, &res); if (error) return 0; /* Continue with non-canon hostname */ if (strlcpy(hostname, res->ai_canonname, len) >= len) { fprintf(stderr, "hostname too long"); freeaddrinfo(res); return -1; } freeaddrinfo(res); return 0; } int base64_encode(unsigned char const *src, size_t srclen, char *dest, size_t destsize) { return __b64_ntop(src, srclen, dest, destsize); } int base64_decode(char const *src, unsigned char *dest, size_t destsize) { return __b64_pton(src, dest, destsize); } int base64_encode_rfc3548(unsigned char const *src, size_t srclen, char *dest, size_t destsize) { size_t i; int ret; if ((ret = base64_encode(src, srclen, dest, destsize)) == -1) return -1; for (i = 0; i < destsize; ++i) { if (dest[i] == '/') dest[i] = '_'; else if (dest[i] == '+') dest[i] = '-'; } return ret; } void log_trace0(const char *emsg, ...) { va_list ap; va_start(ap, emsg); vlog(LOG_DEBUG, emsg, ap); va_end(ap); } void log_trace_verbose(int v) { tracing = v; /* Set debug logging in log.c */ log_setverbose(v & TRACE_DEBUG ? 2 : foreground_log); } int parse_table_line(FILE *fp, char **line, size_t *linesize, int *type, char **key, char **val, int *malformed) { char *keyp, *valp; ssize_t linelen; *key = NULL; *val = NULL; *malformed = 0; if ((linelen = getline(line, linesize, fp)) == -1) return (-1); keyp = *line; while (isspace((unsigned char)*keyp)) { ++keyp; --linelen; } if (*keyp == '\0') return 0; while (linelen > 0 && isspace((unsigned char)keyp[linelen - 1])) keyp[--linelen] = '\0'; if (*keyp == '#') { if (*type == T_NONE) { keyp++; while (isspace((unsigned char)*keyp)) ++keyp; if (!strcmp(keyp, "@list")) *type = T_LIST; } return 0; } if (*keyp == '[') { if ((valp = strchr(keyp, ']')) == NULL) { *malformed = 1; return (0); } valp++; } else valp = keyp + strcspn(keyp, " \t:"); if (*type == T_NONE) *type = (*valp == '\0') ? T_LIST : T_HASH; if (*type == T_LIST) { *key = keyp; return (0); } /* T_HASH */ if (*valp != '\0') { *valp++ = '\0'; valp += strspn(valp, " \t"); } if (*valp == '\0') *malformed = 1; *key = keyp; *val = valp; return (0); }