diff options
Diffstat (limited to 'usr.sbin/smtpd/table.c')
-rw-r--r-- | usr.sbin/smtpd/table.c | 709 |
1 files changed, 709 insertions, 0 deletions
diff --git a/usr.sbin/smtpd/table.c b/usr.sbin/smtpd/table.c new file mode 100644 index 00000000..469eeee1 --- /dev/null +++ b/usr.sbin/smtpd/table.c @@ -0,0 +1,709 @@ +/* $OpenBSD: table.c,v 1.48 2019/01/10 07:40:52 eric Exp $ */ + +/* + * Copyright (c) 2013 Eric Faurot <eric@openbsd.org> + * Copyright (c) 2008 Gilles Chehade <gilles@poolp.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 <netinet/in.h> +#include <arpa/inet.h> +#include <net/if.h> + +#include <errno.h> +#include <event.h> +#include <imsg.h> +#include <stdio.h> +#include <stdlib.h> +#include <regex.h> +#include <limits.h> +#include <string.h> +#include <unistd.h> + +#include "smtpd.h" +#include "log.h" + +struct table_backend *table_backend_lookup(const char *); + +extern struct table_backend table_backend_static; +#ifdef HAVE_DB_API +extern struct table_backend table_backend_db; +#endif +extern struct table_backend table_backend_getpwnam; +extern struct table_backend table_backend_proc; + +static const char * table_service_name(enum table_service); +static int table_parse_lookup(enum table_service, const char *, const char *, + union lookup *); +static int parse_sockaddr(struct sockaddr *, int, const char *); + +static unsigned int last_table_id = 0; + +static struct table_backend *backends[] = { + &table_backend_static, +#ifdef HAVE_DB_API + &table_backend_db, +#endif + &table_backend_getpwnam, + &table_backend_proc, + NULL +}; + +struct table_backend * +table_backend_lookup(const char *backend) +{ + int i; + + if (!strcmp(backend, "file")) + backend = "static"; + + for (i = 0; backends[i]; i++) + if (!strcmp(backends[i]->name, backend)) + return (backends[i]); + + return NULL; +} + +static const char * +table_service_name(enum table_service s) +{ + switch (s) { + case K_NONE: return "NONE"; + case K_ALIAS: return "ALIAS"; + case K_DOMAIN: return "DOMAIN"; + case K_CREDENTIALS: return "CREDENTIALS"; + case K_NETADDR: return "NETADDR"; + case K_USERINFO: return "USERINFO"; + case K_SOURCE: return "SOURCE"; + case K_MAILADDR: return "MAILADDR"; + case K_ADDRNAME: return "ADDRNAME"; + case K_MAILADDRMAP: return "MAILADDRMAP"; + case K_RELAYHOST: return "RELAYHOST"; + case K_STRING: return "STRING"; + case K_REGEX: return "REGEX"; + } + return "???"; +} + +struct table * +table_find(struct smtpd *conf, const char *name) +{ + return dict_get(conf->sc_tables_dict, name); +} + +int +table_match(struct table *table, enum table_service kind, const char *key) +{ + return table_lookup(table, kind, key, NULL); +} + +int +table_lookup(struct table *table, enum table_service kind, const char *key, + union lookup *lk) +{ + char lkey[1024], *buf = NULL; + int r; + + r = -1; + if (table->t_backend->lookup == NULL) + errno = ENOTSUP; + else if (!lowercase(lkey, key, sizeof lkey)) { + log_warnx("warn: lookup key too long: %s", key); + errno = EINVAL; + } + else + r = table->t_backend->lookup(table, kind, lkey, lk ? &buf : NULL); + + if (r == 1) { + log_trace(TRACE_LOOKUP, "lookup: %s \"%s\" as %s in table %s:%s -> %s%s%s", + lk ? "lookup" : "match", + key, + table_service_name(kind), + table->t_backend->name, + table->t_name, + lk ? "\"" : "", + lk ? buf : "true", + lk ? "\"" : ""); + if (buf) + r = table_parse_lookup(kind, lkey, buf, lk); + } + else + log_trace(TRACE_LOOKUP, "lookup: %s \"%s\" as %s in table %s:%s -> %s%s", + lk ? "lookup" : "match", + key, + table_service_name(kind), + table->t_backend->name, + table->t_name, + (r == -1) ? "error: " : (lk ? "none" : "false"), + (r == -1) ? strerror(errno) : ""); + + free(buf); + + return (r); +} + +int +table_fetch(struct table *table, enum table_service kind, union lookup *lk) +{ + char *buf = NULL; + int r; + + r = -1; + if (table->t_backend->fetch == NULL) + errno = ENOTSUP; + else + r = table->t_backend->fetch(table, kind, &buf); + + if (r == 1) { + log_trace(TRACE_LOOKUP, "lookup: fetch %s from table %s:%s -> \"%s\"", + table_service_name(kind), + table->t_backend->name, + table->t_name, + buf); + r = table_parse_lookup(kind, NULL, buf, lk); + } + else + log_trace(TRACE_LOOKUP, "lookup: fetch %s from table %s:%s -> %s%s", + table_service_name(kind), + table->t_backend->name, + table->t_name, + (r == -1) ? "error: " : "none", + (r == -1) ? strerror(errno) : ""); + + free(buf); + + return (r); +} + +struct table * +table_create(struct smtpd *conf, const char *backend, const char *name, + const char *config) +{ + struct table *t; + struct table_backend *tb; + char path[LINE_MAX]; + size_t n; + struct stat sb; + + if (name && table_find(conf, name)) + fatalx("table_create: table \"%s\" already defined", name); + + if ((tb = table_backend_lookup(backend)) == NULL) { + if ((size_t)snprintf(path, sizeof(path), PATH_LIBEXEC"/table-%s", + backend) >= sizeof(path)) { + fatalx("table_create: path too long \"" + PATH_LIBEXEC"/table-%s\"", backend); + } + if (stat(path, &sb) == 0) { + tb = table_backend_lookup("proc"); + (void)strlcpy(path, backend, sizeof(path)); + if (config) { + (void)strlcat(path, ":", sizeof(path)); + if (strlcat(path, config, sizeof(path)) + >= sizeof(path)) + fatalx("table_create: config file path too long"); + } + config = path; + } + } + + if (tb == NULL) + fatalx("table_create: backend \"%s\" does not exist", backend); + + t = xcalloc(1, sizeof(*t)); + t->t_backend = tb; + + if (config) { + if (strlcpy(t->t_config, config, sizeof t->t_config) + >= sizeof t->t_config) + fatalx("table_create: table config \"%s\" too large", + t->t_config); + } + + if (strcmp(tb->name, "static") != 0) + t->t_type = T_DYNAMIC; + + if (name == NULL) + (void)snprintf(t->t_name, sizeof(t->t_name), "<dynamic:%u>", + last_table_id++); + else { + n = strlcpy(t->t_name, name, sizeof(t->t_name)); + if (n >= sizeof(t->t_name)) + fatalx("table_create: table name too long"); + } + + dict_set(conf->sc_tables_dict, t->t_name, t); + + return (t); +} + +void +table_destroy(struct smtpd *conf, struct table *t) +{ + dict_xpop(conf->sc_tables_dict, t->t_name); + free(t); +} + +int +table_config(struct table *t) +{ + if (t->t_backend->config == NULL) + return (1); + return (t->t_backend->config(t)); +} + +void +table_add(struct table *t, const char *key, const char *val) +{ + if (t->t_backend->add == NULL) + fatalx("table_add: cannot add to table"); + + if (t->t_backend->add(t, key, val) == 0) + log_warnx("warn: failed to add \"%s\" in table \"%s\"", key, t->t_name); +} + +void +table_dump(struct table *t) +{ + const char *type; + char buf[LINE_MAX]; + + switch(t->t_type) { + case T_NONE: + type = "NONE"; + break; + case T_DYNAMIC: + type = "DYNAMIC"; + break; + case T_LIST: + type = "LIST"; + break; + case T_HASH: + type = "HASH"; + break; + default: + type = "???"; + break; + } + + if (t->t_config[0]) + snprintf(buf, sizeof(buf), " config=\"%s\"", t->t_config); + else + buf[0] = '\0'; + + log_debug("TABLE \"%s\" backend=%s type=%s%s", t->t_name, + t->t_backend->name, type, buf); + + if (t->t_backend->dump) + t->t_backend->dump(t); +} + +int +table_check_type(struct table *t, uint32_t mask) +{ + return t->t_type & mask; +} + +int +table_check_service(struct table *t, uint32_t mask) +{ + return t->t_backend->services & mask; +} + +int +table_check_use(struct table *t, uint32_t tmask, uint32_t smask) +{ + return table_check_type(t, tmask) && table_check_service(t, smask); +} + +int +table_open(struct table *t) +{ + if (t->t_backend->open == NULL) + return (1); + return (t->t_backend->open(t)); +} + +void +table_close(struct table *t) +{ + if (t->t_backend->close) + t->t_backend->close(t); +} + +int +table_update(struct table *t) +{ + if (t->t_backend->update == NULL) + return (1); + return (t->t_backend->update(t)); +} + + +/* + * quick reminder: + * in *_match() s1 comes from session, s2 comes from table + */ + +int +table_domain_match(const char *s1, const char *s2) +{ + return hostname_match(s1, s2); +} + +int +table_mailaddr_match(const char *s1, const char *s2) +{ + struct mailaddr m1; + struct mailaddr m2; + + if (!text_to_mailaddr(&m1, s1)) + return 0; + if (!text_to_mailaddr(&m2, s2)) + return 0; + return mailaddr_match(&m1, &m2); +} + +static int table_match_mask(struct sockaddr_storage *, struct netaddr *); +static int table_inet4_match(struct sockaddr_in *, struct netaddr *); +static int table_inet6_match(struct sockaddr_in6 *, struct netaddr *); + +int +table_netaddr_match(const char *s1, const char *s2) +{ + struct netaddr n1; + struct netaddr n2; + + if (strcasecmp(s1, s2) == 0) + return 1; + if (!text_to_netaddr(&n1, s1)) + return 0; + if (!text_to_netaddr(&n2, s2)) + return 0; + if (n1.ss.ss_family != n2.ss.ss_family) + return 0; + if (SS_LEN(&n1.ss) != SS_LEN(&n2.ss)) + return 0; + return table_match_mask(&n1.ss, &n2); +} + +static int +table_match_mask(struct sockaddr_storage *ss, struct netaddr *ssmask) +{ + if (ss->ss_family == AF_INET) + return table_inet4_match((struct sockaddr_in *)ss, ssmask); + + if (ss->ss_family == AF_INET6) + return table_inet6_match((struct sockaddr_in6 *)ss, ssmask); + + return (0); +} + +static int +table_inet4_match(struct sockaddr_in *ss, struct netaddr *ssmask) +{ + in_addr_t mask; + int i; + + /* a.b.c.d/8 -> htonl(0xff000000) */ + mask = 0; + for (i = 0; i < ssmask->bits; ++i) + mask = (mask >> 1) | 0x80000000; + mask = htonl(mask); + + /* (addr & mask) == (net & mask) */ + if ((ss->sin_addr.s_addr & mask) == + (((struct sockaddr_in *)ssmask)->sin_addr.s_addr & mask)) + return 1; + + return 0; +} + +static int +table_inet6_match(struct sockaddr_in6 *ss, struct netaddr *ssmask) +{ + struct in6_addr *in; + struct in6_addr *inmask; + struct in6_addr mask; + int i; + + memset(&mask, 0, sizeof(mask)); + for (i = 0; i < ssmask->bits / 8; i++) + mask.s6_addr[i] = 0xff; + i = ssmask->bits % 8; + if (i) + mask.s6_addr[ssmask->bits / 8] = 0xff00 >> i; + + in = &ss->sin6_addr; + inmask = &((struct sockaddr_in6 *)&ssmask->ss)->sin6_addr; + + for (i = 0; i < 16; i++) { + if ((in->s6_addr[i] & mask.s6_addr[i]) != + (inmask->s6_addr[i] & mask.s6_addr[i])) + return (0); + } + + return (1); +} + +int +table_regex_match(const char *string, const char *pattern) +{ + regex_t preg; + int cflags = REG_EXTENDED|REG_NOSUB; + + if (strncmp(pattern, "(?i)", 4) == 0) { + cflags |= REG_ICASE; + pattern += 4; + } + + if (regcomp(&preg, pattern, cflags) != 0) + return (0); + + if (regexec(&preg, string, 0, NULL, 0) != 0) + return (0); + + return (1); +} + +void +table_dump_all(struct smtpd *conf) +{ + struct table *t; + void *iter; + + iter = NULL; + while (dict_iter(conf->sc_tables_dict, &iter, NULL, (void **)&t)) + table_dump(t); +} + +void +table_open_all(struct smtpd *conf) +{ + struct table *t; + void *iter; + + iter = NULL; + while (dict_iter(conf->sc_tables_dict, &iter, NULL, (void **)&t)) + if (!table_open(t)) + fatalx("failed to open table %s", t->t_name); +} + +void +table_close_all(struct smtpd *conf) +{ + struct table *t; + void *iter; + + iter = NULL; + while (dict_iter(conf->sc_tables_dict, &iter, NULL, (void **)&t)) + table_close(t); +} + +static int +table_parse_lookup(enum table_service service, const char *key, + const char *line, union lookup *lk) +{ + char buffer[LINE_MAX], *p; + size_t len; + + len = strlen(line); + + switch (service) { + case K_ALIAS: + lk->expand = calloc(1, sizeof(*lk->expand)); + if (lk->expand == NULL) + return (-1); + if (!expand_line(lk->expand, line, 1)) { + expand_free(lk->expand); + return (-1); + } + return (1); + + case K_DOMAIN: + if (strlcpy(lk->domain.name, line, sizeof(lk->domain.name)) + >= sizeof(lk->domain.name)) + return (-1); + return (1); + + case K_CREDENTIALS: + + /* credentials are stored as user:password */ + if (len < 3) + return (-1); + + /* too big to fit in a smtp session line */ + if (len >= LINE_MAX) + return (-1); + + p = strchr(line, ':'); + if (p == NULL) { + if (strlcpy(lk->creds.username, key, sizeof (lk->creds.username)) + >= sizeof (lk->creds.username)) + return (-1); + if (strlcpy(lk->creds.password, line, sizeof(lk->creds.password)) + >= sizeof(lk->creds.password)) + return (-1); + return (1); + } + + if (p == line || p == line + len - 1) + return (-1); + + memmove(lk->creds.username, line, p - line); + lk->creds.username[p - line] = '\0'; + + if (strlcpy(lk->creds.password, p+1, sizeof(lk->creds.password)) + >= sizeof(lk->creds.password)) + return (-1); + + return (1); + + case K_NETADDR: + if (!text_to_netaddr(&lk->netaddr, line)) + return (-1); + return (1); + + case K_USERINFO: + if (!bsnprintf(buffer, sizeof(buffer), "%s:%s", key, line)) + return (-1); + if (!text_to_userinfo(&lk->userinfo, buffer)) + return (-1); + return (1); + + case K_SOURCE: + if (parse_sockaddr((struct sockaddr *)&lk->source.addr, + PF_UNSPEC, line) == -1) + return (-1); + return (1); + + case K_MAILADDR: + if (!text_to_mailaddr(&lk->mailaddr, line)) + return (-1); + return (1); + + case K_MAILADDRMAP: + lk->maddrmap = calloc(1, sizeof(*lk->maddrmap)); + if (lk->maddrmap == NULL) + return (-1); + maddrmap_init(lk->maddrmap); + if (!mailaddr_line(lk->maddrmap, line)) { + maddrmap_free(lk->maddrmap); + return (-1); + } + return (1); + + case K_ADDRNAME: + if (parse_sockaddr((struct sockaddr *)&lk->addrname.addr, + PF_UNSPEC, key) == -1) + return (-1); + if (strlcpy(lk->addrname.name, line, sizeof(lk->addrname.name)) + >= sizeof(lk->addrname.name)) + return (-1); + return (1); + + case K_RELAYHOST: + if (strlcpy(lk->relayhost, line, sizeof(lk->relayhost)) + >= sizeof(lk->relayhost)) + return (-1); + return (1); + + default: + return (-1); + } +} + +static int +parse_sockaddr(struct sockaddr *sa, int family, const char *str) +{ + struct in_addr ina; + struct in6_addr in6a; + struct sockaddr_in *sin; + struct sockaddr_in6 *sin6; + char *cp, *str2; + const char *errstr; + + switch (family) { + case PF_UNSPEC: + if (parse_sockaddr(sa, PF_INET, str) == 0) + return (0); + return parse_sockaddr(sa, PF_INET6, str); + + case PF_INET: + if (inet_pton(PF_INET, str, &ina) != 1) + return (-1); + + sin = (struct sockaddr_in *)sa; + memset(sin, 0, sizeof *sin); +#ifdef HAVE_STRUCT_SOCKADDR_IN_SIN_LEN + sin->sin_len = sizeof(struct sockaddr_in); +#endif + sin->sin_family = PF_INET; + sin->sin_addr.s_addr = ina.s_addr; + return (0); + + case PF_INET6: + if (strncasecmp("ipv6:", str, 5) == 0) + str += 5; + cp = strchr(str, SCOPE_DELIMITER); + if (cp) { + str2 = strdup(str); + if (str2 == NULL) + return (-1); + str2[cp - str] = '\0'; + if (inet_pton(PF_INET6, str2, &in6a) != 1) { + free(str2); + return (-1); + } + cp++; + free(str2); + } else if (inet_pton(PF_INET6, str, &in6a) != 1) + return (-1); + + sin6 = (struct sockaddr_in6 *)sa; + memset(sin6, 0, sizeof *sin6); +#ifdef HAVE_STRUCT_SOCKADDR_IN6_SIN6_LEN + sin6->sin6_len = sizeof(struct sockaddr_in6); +#endif + sin6->sin6_family = PF_INET6; + sin6->sin6_addr = in6a; + + if (cp == NULL) + return (0); + + if (IN6_IS_ADDR_LINKLOCAL(&in6a) || + IN6_IS_ADDR_MC_LINKLOCAL(&in6a) || + IN6_IS_ADDR_MC_NODELOCAL(&in6a)) + if ((sin6->sin6_scope_id = if_nametoindex(cp))) + return (0); + + sin6->sin6_scope_id = strtonum(cp, 0, UINT32_MAX, &errstr); + if (errstr) + return (-1); + return (0); + + default: + break; + } + + return (-1); +} |