#include <sys_defs.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#ifdef STRCASECMP_IN_STRINGS_H
#include <strings.h>
#endif
#include <msg.h>
#include <mymalloc.h>
#include <vstring.h>
#include <vstream.h>
#include <stringops.h>
#include <valid_hostname.h>
#include <name_code.h>
#include <mime_state.h>
#include <debug_peer.h>
#include <mail_params.h>
#include <maps.h>
#include "smtp.h"
#include "smtp_sasl.h"
#ifdef USE_TLS
static MAPS *tls_policy;
static MAPS *tls_per_site;
void smtp_tls_list_init(void)
{
if (*var_smtp_tls_policy) {
tls_policy = maps_create(VAR_SMTP_TLS_POLICY, var_smtp_tls_policy,
DICT_FLAG_LOCK | DICT_FLAG_FOLD_FIX);
if (*var_smtp_tls_per_site)
msg_warn("%s ignored when %s is not empty.",
VAR_SMTP_TLS_PER_SITE, VAR_SMTP_TLS_POLICY);
return;
}
if (*var_smtp_tls_per_site) {
tls_per_site = maps_create(VAR_SMTP_TLS_PER_SITE, var_smtp_tls_per_site,
DICT_FLAG_LOCK | DICT_FLAG_FOLD_FIX);
}
}
static const char *policy_name(int tls_level)
{
const char *name = str_tls_level(tls_level);
if (name == 0)
name = "unknown";
return name;
}
static void tls_site_lookup(int *site_level, const char *site_name,
const char *site_class)
{
const char *lookup;
if ((lookup = maps_find(tls_per_site, site_name, 0)) != 0) {
if (!strcasecmp(lookup, "NONE")) {
if (*site_level <= TLS_LEV_MAY)
*site_level = TLS_LEV_NONE;
} else if (!strcasecmp(lookup, "MAY")) {
if (*site_level < TLS_LEV_NONE)
*site_level = TLS_LEV_MAY;
} else if (!strcasecmp(lookup, "MUST_NOPEERMATCH")) {
if (*site_level < TLS_LEV_ENCRYPT)
*site_level = TLS_LEV_ENCRYPT;
} else if (!strcasecmp(lookup, "MUST")) {
if (*site_level < TLS_LEV_VERIFY)
*site_level = TLS_LEV_VERIFY;
} else {
msg_warn("Table %s: ignoring unknown TLS policy '%s' for %s %s",
var_smtp_tls_per_site, lookup, site_class, site_name);
}
}
}
static int tls_policy_lookup_one(SMTP_SESSION *session, int *site_level,
const char *site_name,
const char *site_class)
{
const char *lookup;
char *policy;
char *saved_policy;
char *tok;
const char *err;
char *name;
char *val;
static VSTRING *cbuf;
#undef FREE_RETURN
#define FREE_RETURN(x) do { myfree(saved_policy); return (x); } while (0)
if ((lookup = maps_find(tls_policy, site_name, 0)) == 0)
return (0);
if (cbuf == 0)
cbuf = vstring_alloc(10);
#define WHERE \
vstring_str(vstring_sprintf(cbuf, "TLS policy table, %s \"%s\"", \
site_class, site_name))
saved_policy = policy = mystrdup(lookup);
if ((tok = mystrtok(&policy, "\t\n\r ,")) == 0) {
msg_warn("%s: invalid empty policy", WHERE);
*site_level = TLS_LEV_INVALID;
FREE_RETURN(1);
}
*site_level = tls_level_lookup(tok);
if (*site_level == TLS_LEV_INVALID) {
msg_warn("%s: invalid security level \"%s\"", WHERE, tok);
FREE_RETURN(1);
}
if (*site_level < TLS_LEV_MAY) {
while ((tok = mystrtok(&policy, "\t\n\r ,")) != 0)
msg_warn("%s: ignoring attribute \"%s\" with TLS disabled",
WHERE, tok);
FREE_RETURN(1);
}
while ((tok = mystrtok(&policy, "\t\n\r ,")) != 0) {
if ((err = split_nameval(tok, &name, &val)) != 0) {
*site_level = TLS_LEV_INVALID;
msg_warn("%s: malformed attribute/value pair \"%s\": %s",
WHERE, tok, err);
break;
}
if (!strcasecmp(name, "ciphers")) {
if (*site_level < TLS_LEV_ENCRYPT) {
msg_warn("%s: attribute \"%s\" invalid at security level \"%s\"",
WHERE, name, policy_name(*site_level));
*site_level = TLS_LEV_INVALID;
break;
}
if (*val == 0) {
msg_warn("%s: attribute \"%s\" has empty value", WHERE, name);
*site_level = TLS_LEV_INVALID;
break;
}
if (session->tls_grade) {
msg_warn("%s: attribute \"%s\" is specified multiple times",
WHERE, name);
*site_level = TLS_LEV_INVALID;
break;
}
session->tls_grade = mystrdup(val);
continue;
}
if (!strcasecmp(name, "protocols")) {
if (*site_level < TLS_LEV_ENCRYPT) {
msg_warn("%s: attribute \"%s\" invalid at security level \"%s\"",
WHERE, name, policy_name(*site_level));
*site_level = TLS_LEV_INVALID;
break;
}
if (session->tls_protocols) {
msg_warn("%s: attribute \"%s\" is specified multiple times",
WHERE, name);
*site_level = TLS_LEV_INVALID;
break;
}
session->tls_protocols = mystrdup(val);
continue;
}
if (!strcasecmp(name, "match")) {
char *delim = *site_level == TLS_LEV_FPRINT ? "|" : ":";
if (*site_level <= TLS_LEV_ENCRYPT) {
msg_warn("%s: attribute \"%s\" invalid at security level \"%s\"",
WHERE, name, policy_name(*site_level));
*site_level = TLS_LEV_INVALID;
break;
}
if (*val == 0) {
msg_warn("%s: attribute \"%s\" has empty value", WHERE, name);
*site_level = TLS_LEV_INVALID;
break;
}
if (session->tls_matchargv == 0)
session->tls_matchargv = argv_split(val, delim);
else
argv_split_append(session->tls_matchargv, val, delim);
continue;
} else {
msg_warn("%s: invalid attribute name: \"%s\"", WHERE, name);
*site_level = TLS_LEV_INVALID;
break;
}
}
FREE_RETURN(1);
}
static void tls_policy_lookup(SMTP_SESSION *session, int *site_level,
const char *site_name,
const char *site_class)
{
if (!valid_hostname(site_name, DONT_GRIPE)) {
tls_policy_lookup_one(session, site_level, site_name, site_class);
return;
}
while (1) {
if (tls_policy_lookup_one(session, site_level, site_name, site_class))
return;
if ((site_name = strchr(site_name + 1, '.')) == 0)
return;
}
}
static void set_cipher_grade(SMTP_SESSION *session)
{
const char *mand_exclude = "";
const char *also_exclude = "";
switch (session->tls_level) {
case TLS_LEV_INVALID:
case TLS_LEV_NONE:
return;
case TLS_LEV_MAY:
session->tls_grade = mystrdup("export");
break;
case TLS_LEV_ENCRYPT:
if (session->tls_grade == 0)
session->tls_grade = mystrdup(var_smtp_tls_mand_ciph);
mand_exclude = var_smtp_tls_mand_excl;
also_exclude = "eNULL";
break;
case TLS_LEV_FPRINT:
case TLS_LEV_VERIFY:
case TLS_LEV_SECURE:
if (session->tls_grade == 0)
session->tls_grade = mystrdup(var_smtp_tls_mand_ciph);
mand_exclude = var_smtp_tls_mand_excl;
also_exclude = "aNULL";
break;
}
#define ADD_EXCLUDE(vstr, str) \
do { \
if (*(str)) \
vstring_sprintf_append((vstr), "%s%s", \
VSTRING_LEN(vstr) ? " " : "", (str)); \
} while (0)
if (session->tls_exclusions == 0) {
session->tls_exclusions = vstring_alloc(10);
ADD_EXCLUDE(session->tls_exclusions, var_smtp_tls_excl_ciph);
ADD_EXCLUDE(session->tls_exclusions, mand_exclude);
}
ADD_EXCLUDE(session->tls_exclusions, also_exclude);
}
static void session_tls_init(SMTP_SESSION *session, const char *dest,
const char *host, int flags)
{
const char *myname = "session_tls_init";
int global_level;
int site_level;
session->tls_context = 0;
session->tls_nexthop = 0;
session->tls_level = TLS_LEV_NONE;
session->tls_retry_plain = 0;
session->tls_protocols = 0;
session->tls_grade = 0;
session->tls_exclusions = 0;
session->tls_matchargv = 0;
if (*var_smtp_tls_level) {
global_level = tls_level_lookup(var_smtp_tls_level);
if (global_level == TLS_LEV_INVALID)
msg_panic("%s: invalid TLS security level: \"%s\"",
myname, var_smtp_tls_level);
} else if (var_smtp_enforce_tls) {
global_level = var_smtp_tls_enforce_peername ?
TLS_LEV_VERIFY : TLS_LEV_ENCRYPT;
} else {
global_level = var_smtp_use_tls ?
TLS_LEV_MAY : TLS_LEV_NONE;
}
if (msg_verbose)
msg_info("%s TLS level: %s", "global", policy_name(global_level));
site_level = TLS_LEV_NOTFOUND;
if (tls_policy) {
tls_policy_lookup(session, &site_level, dest, "next-hop destination");
} else if (tls_per_site) {
tls_site_lookup(&site_level, dest, "next-hop destination");
if (strcasecmp(dest, host) != 0)
tls_site_lookup(&site_level, host, "server hostname");
if (msg_verbose)
msg_info("%s TLS level: %s", "site", policy_name(site_level));
if (site_level == TLS_LEV_MAY && global_level > TLS_LEV_MAY)
site_level = global_level;
}
if (site_level == TLS_LEV_NOTFOUND)
session->tls_level = global_level;
else
session->tls_level = site_level;
if (session->tls_level > TLS_LEV_NONE && session->tls_protocols == 0)
session->tls_protocols =
mystrdup((session->tls_level == TLS_LEV_MAY) ?
"" : var_smtp_tls_mand_proto);
set_cipher_grade(session);
if (session->tls_matchargv == 0) {
switch (session->tls_level) {
case TLS_LEV_INVALID:
case TLS_LEV_NONE:
case TLS_LEV_MAY:
case TLS_LEV_ENCRYPT:
break;
case TLS_LEV_FPRINT:
session->tls_matchargv =
argv_split(var_smtp_tls_fpt_cmatch, "\t\n\r, |");
break;
case TLS_LEV_VERIFY:
session->tls_matchargv =
argv_split(var_smtp_tls_vfy_cmatch, "\t\n\r, :");
break;
case TLS_LEV_SECURE:
session->tls_matchargv =
argv_split(var_smtp_tls_sec_cmatch, "\t\n\r, :");
break;
default:
msg_panic("unexpected TLS security level: %d",
session->tls_level);
}
}
if (msg_verbose && (tls_policy || tls_per_site))
msg_info("%s TLS level: %s", "effective",
policy_name(session->tls_level));
}
#endif
SMTP_SESSION *smtp_session_alloc(VSTREAM *stream, const char *dest,
const char *host, const char *addr,
unsigned port, time_t start,
int flags)
{
SMTP_SESSION *session;
session = (SMTP_SESSION *) mymalloc(sizeof(*session));
session->stream = stream;
session->dest = mystrdup(dest);
session->host = mystrdup(host);
session->addr = mystrdup(addr);
session->namaddr = concatenate(host, "[", addr, "]", (char *) 0);
session->helo = 0;
session->port = port;
session->features = 0;
session->size_limit = 0;
session->error_mask = 0;
session->buffer = vstring_alloc(100);
session->scratch = vstring_alloc(100);
session->scratch2 = vstring_alloc(100);
smtp_chat_init(session);
session->mime_state = 0;
if (session->port) {
vstring_sprintf(session->buffer, "%s:%d",
session->namaddr, ntohs(session->port));
session->namaddrport = mystrdup(STR(session->buffer));
} else
session->namaddrport = mystrdup(session->namaddr);
session->sndbufsize = 0;
session->send_proto_helo = 0;
if (flags & SMTP_MISC_FLAG_CONN_STORE)
CACHE_THIS_SESSION_UNTIL(start + var_smtp_reuse_time);
else
DONT_CACHE_THIS_SESSION;
session->reuse_count = 0;
USE_NEWBORN_SESSION;
#ifdef USE_SASL_AUTH
smtp_sasl_connect(session);
#endif
#ifdef USE_TLS
session_tls_init(session, dest, host, flags);
#endif
session->state = 0;
debug_peer_check(host, addr);
return (session);
}
void smtp_session_free(SMTP_SESSION *session)
{
#ifdef USE_TLS
if (session->stream) {
vstream_fflush(session->stream);
if (session->tls_context)
tls_client_stop(smtp_tls_ctx, session->stream,
var_smtp_starttls_tmout, 0, session->tls_context);
}
if (session->tls_protocols)
myfree(session->tls_protocols);
if (session->tls_grade)
myfree(session->tls_grade);
if (session->tls_exclusions)
vstring_free(session->tls_exclusions);
if (session->tls_matchargv)
argv_free(session->tls_matchargv);
#endif
if (session->stream)
vstream_fclose(session->stream);
myfree(session->dest);
myfree(session->host);
myfree(session->addr);
myfree(session->namaddr);
myfree(session->namaddrport);
if (session->helo)
myfree(session->helo);
vstring_free(session->buffer);
vstring_free(session->scratch);
vstring_free(session->scratch2);
if (session->history)
smtp_chat_reset(session);
if (session->mime_state)
mime_state_free(session->mime_state);
#ifdef USE_SASL_AUTH
smtp_sasl_cleanup(session);
#endif
debug_peer_restore();
myfree((char *) session);
}
int smtp_session_passivate(SMTP_SESSION *session, VSTRING *dest_prop,
VSTRING *endp_prop)
{
int fd;
vstring_sprintf(dest_prop, "%u",
session->features & SMTP_FEATURE_DESTINATION_MASK);
vstring_sprintf(endp_prop, "%u\n%s\n%s\n%s\n%u\n%u\n%lu\n%u",
session->reuse_count,
session->dest, session->host,
session->addr, session->port,
session->features & SMTP_FEATURE_ENDPOINT_MASK,
(long) session->expire_time,
session->sndbufsize);
#ifdef notdef
if (smtp_sasl_enable)
smtp_sasl_passivate(endp_prop, session);
#endif
fd = vstream_fileno(session->stream);
vstream_fdclose(session->stream);
session->stream = 0;
smtp_session_free(session);
return (fd);
}
SMTP_SESSION *smtp_session_activate(int fd, VSTRING *dest_prop,
VSTRING *endp_prop)
{
const char *myname = "smtp_session_activate";
SMTP_SESSION *session;
char *dest_props;
char *endp_props;
const char *prop;
const char *dest;
const char *host;
const char *addr;
unsigned port;
unsigned features;
time_t expire_time;
unsigned reuse_count;
unsigned sndbufsize;
endp_props = STR(endp_prop);
if ((prop = mystrtok(&endp_props, "\n")) == 0 || !alldig(prop)) {
msg_warn("%s: bad cached session reuse count property", myname);
return (0);
}
reuse_count = atoi(prop);
if ((dest = mystrtok(&endp_props, "\n")) == 0) {
msg_warn("%s: missing cached session destination property", myname);
return (0);
}
if ((host = mystrtok(&endp_props, "\n")) == 0) {
msg_warn("%s: missing cached session hostname property", myname);
return (0);
}
if ((addr = mystrtok(&endp_props, "\n")) == 0) {
msg_warn("%s: missing cached session address property", myname);
return (0);
}
if ((prop = mystrtok(&endp_props, "\n")) == 0 || !alldig(prop)) {
msg_warn("%s: bad cached session port property", myname);
return (0);
}
port = atoi(prop);
if ((prop = mystrtok(&endp_props, "\n")) == 0 || !alldig(prop)) {
msg_warn("%s: bad cached session features property", myname);
return (0);
}
features = atoi(prop);
if ((prop = mystrtok(&endp_props, "\n")) == 0 || !alldig(prop)) {
msg_warn("%s: bad cached session expiration time property", myname);
return (0);
}
#ifdef MISSING_STRTOUL
expire_time = strtol(prop, 0, 10);
#else
expire_time = strtoul(prop, 0, 10);
#endif
if ((prop = mystrtok(&endp_props, "\n")) == 0 || !alldig(prop)) {
msg_warn("%s: bad cached session sndbufsize property", myname);
return (0);
}
sndbufsize = atoi(prop);
if (dest_prop && VSTRING_LEN(dest_prop)) {
dest_props = STR(dest_prop);
if ((prop = mystrtok(&dest_props, "\n")) == 0 || !alldig(prop)) {
msg_warn("%s: bad cached destination features property", myname);
return (0);
}
features |= atoi(prop);
}
#define NO_FLAGS 0
session = smtp_session_alloc(vstream_fdopen(fd, O_RDWR), dest, host,
addr, port, (time_t) 0, NO_FLAGS);
session->features = (features | SMTP_FEATURE_FROM_CACHE);
CACHE_THIS_SESSION_UNTIL(expire_time);
session->reuse_count = ++reuse_count;
session->sndbufsize = sndbufsize;
if (msg_verbose)
msg_info("%s: dest=%s host=%s addr=%s port=%u features=0x%x, "
"ttl=%ld, reuse=%d, sndbuf=%u",
myname, dest, host, addr, ntohs(port), features,
(long) (expire_time - time((time_t *) 0)), reuse_count,
sndbufsize);
#ifdef notdef
if (smtp_sasl_enable && smtp_sasl_activate(session, endp_props) < 0) {
vstream_fdclose(session->stream);
session->stream = 0;
smtp_session_free(session);
return (0);
}
#endif
return (session);
}