File: [local] / src / usr.sbin / smtpd / util.c (download)
Revision 1.158, Mon May 13 06:48:26 2024 UTC (2 weeks, 5 days ago) by jsg
Branch: MAIN
CVS Tags: HEAD Changes since 1.157: +11 -9 lines
fix some leaks; ok op@
|
/* $OpenBSD: util.c,v 1.158 2024/05/13 06:48:26 jsg 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 <sys/stat.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <errno.h>
#include <fts.h>
#include <libgen.h>
#include <resolv.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.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 || (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;
buflen = getline(&buf, &bufsz, fp);
fclose(fp);
if (buflen == -1) {
free(buf);
return 1;
}
if (buf[buflen - 1] == '\n')
buf[buflen - 1] = '\0';
bufsz = strlcpy(hostname, buf, len);
free(buf);
if (bufsz >= len) {
fprintf(stderr, MAILNAME_FILE " entry too long");
return 1;
}
return 0;
}
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);
}