/* $OpenBSD: util.c,v 1.97 2013/07/19 08:28:43 eric 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 #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 *); void * xmalloc(size_t size, const char *where) { void *r; if ((r = malloc(size)) == NULL) errx(1, "%s: malloc(%zu)", where, size); return (r); } void * xcalloc(size_t nmemb, size_t size, const char *where) { void *r; if ((r = calloc(nmemb, size)) == NULL) errx(1, "%s: calloc(%zu, %zu)", where, nmemb, size); return (r); } char * xstrdup(const char *str, const char *where) { char *r; if ((r = strdup(str)) == NULL) errx(1, "%s: strdup(%p)", where, str); return (r); } void * xmemdup(const void *ptr, size_t size, const char *where) { void *r; if ((r = malloc(size)) == NULL) errx(1, "%s: malloc(%zu)", where, size); memmove(r, ptr, size); return (r); } #if !defined(NO_IO) void iobuf_xinit(struct iobuf *io, size_t size, size_t max, const char *where) { if (iobuf_init(io, size, max) == -1) errx(1, "%s: iobuf_init(%p, %zu, %zu)", where, io, size, max); } void iobuf_xfqueue(struct iobuf *io, const char *where, const char *fmt, ...) { va_list ap; int len; va_start(ap, fmt); len = iobuf_vfqueue(io, fmt, ap); va_end(ap); if (len == -1) errx(1, "%s: iobuf_xfqueue(%p, %s, ...)", where, io, fmt); } #endif char * strip(char *s) { size_t l; while (*s == ' ' || *s == '\t') s++; for (l = strlen(s); l; l--) { if (s[l-1] != ' ' && s[l-1] != '\t') 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 == -1 || ret >= (int)size) return 0; return 1; } static int mkdirs_component(char *path, mode_t mode) { struct stat sb; if (stat(path, &sb) == -1) { if (errno != ENOENT) return 0; if (mkdir(path, mode | S_IWUSR | S_IXUSR) == -1) return 0; } else if (! S_ISDIR(sb.st_mode)) return 0; return 1; } int mkdirs(char *path, mode_t mode) { char buf[SMTPD_MAXPATHLEN]; int i = 0; int done = 0; char *p; /* absolute path required */ if (*path != '/') return 0; /* make sure we don't exceed SMTPD_MAXPATHLEN */ if (strlen(path) >= sizeof buf) return 0; bzero(buf, sizeof buf); for (p = path; *p; p++) { if (*p == '/') { if (buf[0] != '\0') if (! mkdirs_component(buf, mode)) return 0; while (*p == '/') p++; buf[i++] = '/'; buf[i++] = *p; if (*p == '\0' && ++done) break; continue; } buf[i++] = *p; } if (! done) if (! mkdirs_component(buf, mode)) return 0; if (chmod(path, mode) == -1) 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) { warn("stat: %s", path); return (0); } /* chmod is deferred to avoid umask effect */ if (mkdir(path, 0) == -1) { warn("mkdir: %s", path); return (0); } if (chown(path, owner, group) == -1) { warn("chown: %s", path); return (0); } if (chmod(path, mode) == -1) { warn("chmod: %s", path); return (0); } if (stat(path, &sb) == -1) { warn("stat: %s", path); return (0); } } ret = 1; /* check if it's a directory */ if (!S_ISDIR(sb.st_mode)) { ret = 0; warnx("%s is not a directory", path); } /* check that it is owned by owner/group */ if (sb.st_uid != owner) { ret = 0; warnx("%s is not owned by uid %d", path, owner); } if (sb.st_gid != group) { ret = 0; 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'; 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) { 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) { warn("rmdir: %s", e->fts_path); ret = -1; } break; case FTS_F: if (unlink(e->fts_path) == -1) { 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[SMTPD_MAXPATHLEN]; if ((n = strlen(to)) == 0) fatalx("to is empty"); sep = (to[n - 1] == '/') ? "" : "/"; retry = 0; again: 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[SMTPD_MAXPATHLEN]; int fd; mode_t omode; if (! bsnprintf(path, sizeof(path), "%s/smtpd.XXXXXXXXXX", PATH_TEMPORARY)) err(1, "snprintf"); omode = umask(7077); if ((fd = mkstemp(path)) == -1) err(1, "cannot create temporary file %s", path); umask(omode); 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((int)*hostname) != tolower((int)*pattern)) hostname++; continue; } if (tolower((int)*pattern) != tolower((int)*hostname)) return 0; pattern++; hostname++; } return (*hostname == '\0' && *pattern == '\0'); } int valid_localpart(const char *s) { /* * RFC 5322 defines theses characters as valid: !#$%&'*+-/=?^_`{|}~ * some of them are potentially dangerous, and not so used after all. */ #define IS_ATEXT(c) (isalnum((int)(c)) || strchr("!%+-/=_", (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; 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, (int)']'); 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; } nextsub: if (!isalnum((int)*s)) return 0; while (*(++s) != '\0') { if (*s == '.') break; if (isalnum((int)*s) || *s == '-') continue; return 0; } if (s[-1] == '-') return 0; if (*s == '.') { s++; goto nextsub; } return 1; } /* * Check file for security. Based on usr.bin/ssh/auth.c. */ 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) < 0 || !S_ISREG(st.st_mode) || (st.st_uid != 0 && 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; strlcpy(buf, cp, sizeof(buf)); if (stat(buf, &st) < 0 || (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; 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; if (SIZE_T_MAX / nalloc < sizeof(char *)) fatalx("addargs: nalloc * size > SIZE_T_MAX"); args->list = realloc(args->list, nalloc * sizeof(char *)); if (args->list == NULL) fatal("addargs: realloc"); 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((int)*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((int)*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; uint64_t uid; while ((uid = ((uint64_t)(id++) << 32 | arc4random())) == 0) ; return (uid); } void fdlimit(double percent) { struct rlimit rl; if (percent < 0 || percent > 1) fatalx("fdlimit: parameter out of range"); if (getrlimit(RLIMIT_NOFILE, &rl) == -1) fatal("fdlimit: getrlimit"); rl.rlim_cur = percent * rl.rlim_max; if (setrlimit(RLIMIT_NOFILE, &rl) == -1) fatal("fdlimit: setrlimit"); } void session_socket_blockmode(int fd, enum blockmodes bm) { int flags; if ((flags = fcntl(fd, F_GETFL, 0)) == -1) fatal("fcntl F_GETFL"); if (bm == BM_NONBLOCK) flags |= O_NONBLOCK; else flags &= ~O_NONBLOCK; if ((flags = fcntl(fd, F_SETFL, flags)) == -1) fatal("fcntl F_SETFL"); } void session_socket_no_linger(int fd) { struct linger lng; bzero(&lng, sizeof(lng)); if (setsockopt(fd, SOL_SOCKET, SO_LINGER, &lng, sizeof(lng)) == -1) fatal("session_socket_no_linger"); } 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) { size_t i; if (len >= SMTPD_MAXLINESIZE) 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(line[1]) || !isdigit(line[2])) return "reply code out of range"; /* validate reply message */ for (i = 0; i < len; i++) if (!isprint(line[i])) return "non-printable character in reply"; return NULL; }