diff options
Diffstat (limited to 'smtpd/util.c')
-rw-r--r-- | smtpd/util.c | 870 |
1 files changed, 870 insertions, 0 deletions
diff --git a/smtpd/util.c b/smtpd/util.c new file mode 100644 index 00000000..b2b1458c --- /dev/null +++ b/smtpd/util.c @@ -0,0 +1,870 @@ +/* $OpenBSD: util.c,v 1.151 2020/02/24 23:54:28 millert Exp $ */ + +/* + * Copyright (c) 2000,2001 Markus Friedl. All rights reserved. + * Copyright (c) 2008 Gilles Chehade <gilles@poolp.org> + * Copyright (c) 2009 Jacek Masiulaniec <jacekm@dobremiasto.net> + * Copyright (c) 2012 Eric Faurot <eric@openbsd.org> + * + * 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 "includes.h" + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/resource.h> + +#include <netinet/in.h> +#include <arpa/inet.h> + +#include <ctype.h> +#include <errno.h> +#include <event.h> +#include <fcntl.h> +#include <fts.h> +#include <imsg.h> +#include <inttypes.h> +#include <libgen.h> +#include <netdb.h> +#include <pwd.h> +#include <limits.h> +#include <resolv.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <syslog.h> +#include <time.h> +#include <unistd.h> + +#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 || ret >= (int)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 +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_trace(int mask, const char *emsg, ...) +{ + va_list ap; + + if (tracing & mask) { + 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); +} + +void +xclosefrom(int lowfd) +{ +#if defined HAVE_CLOSEFROM_INT + if (closefrom(lowfd) == -1) + err(1, "closefrom"); +#else + closefrom(lowfd); +#endif +} + +void +portable_freeaddrinfo(struct addrinfo *ai) +{ + struct addrinfo *p; + + do { + p = ai; + ai = ai->ai_next; + free(p->ai_canonname); + free(p); + } while (ai); +} |