/* This file is part of gacopyz.
Copyright (C) 2007-2020 Sergey Poznyakoff
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3, or (at your option)
any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see . */
#include
typedef struct gacopyz_macro_def *gacopyz_macro_def_t;
struct gacopyz_macro_def {
char *name;
char *value;
};
#define _SRV_CONNECTED 0x0100
#define _SRV_READY 0x0200
#define _SRV_CLRDIS 0x0400
#define _SRV_SYS_MASK 0xff00
struct gacopyz_srv {
char *id; /* Server identifier (not used yet) */
char *portspec; /* Port spec */
struct gacopyz_iod iod;
int flags;
gacopyz_macro_def_t def;
size_t ndefs;
size_t maxdefs;
struct sockaddr *source_addr;
socklen_t source_addr_len;
int onerror;
void (*memerror)(gacopyz_srv_t, const char *, unsigned int);
unsigned long version;
unsigned long acts;
unsigned long proto;
char **req_macros[gacopyz_stage_max]; /* Required macros */
int (*cb_reply)(gacopyz_srv_t srv, int cmd, int rcmd, void *data);
void *cb_data;
char *buf;
size_t bufsize;
char *resp_ptr;
size_t resp_size;
};
void
default_memerror(gacopyz_srv_t srv, const char *file, unsigned int line)
{
gacopyz_io_log(&srv->iod, SMI_LOG_FATAL,
_("%s:%lu: not enough memory"),
file, line);
abort();
}
#define GACOPYZ_ASSERT(e) \
if (!(e)) { \
fprintf(stderr, "%s:%d: GACOPYZ usage error: assertion failed: %s\n", \
__FILE__, __LINE__, #e); \
abort(); \
}
#define GACOPYZ_ALLOC(srv, expr) do \
if (!(expr)) { \
srv->memerror(srv, __FILE__, __LINE__); \
if (!(expr)) \
default_memerror(srv, __FILE__, __LINE__); \
} while (0)
static struct gacopyz_macro_def *
find_def(gacopyz_srv_t srv, const char *name, size_t len)
{
size_t i;
for (i = 0; i < srv->ndefs; i++)
if (strncmp(srv->def[i].name, name, len) == 0)
return &srv->def[i];
return NULL;
}
static void
add_def(gacopyz_srv_t srv, const char *name, size_t len, const char *value)
{
struct gacopyz_macro_def *def;
if (def = find_def(srv, name, len))
free(def->value);
else {
if (srv->ndefs == srv->maxdefs) {
size_t maxdefs = srv->maxdefs + 64;
GACOPYZ_ALLOC(srv,
def = realloc(srv->def,
sizeof(srv->def[0]) * maxdefs));
srv->maxdefs = maxdefs;
srv->def = def;
}
def = srv->def + srv->ndefs++;
GACOPYZ_ALLOC(srv, def->name = malloc(len + 1));
memcpy(def->name, name, len);
def->name[len] = 0;
}
GACOPYZ_ALLOC(srv, def->value = strdup(value));
}
void
del_def(gacopyz_srv_t srv, const char *name, size_t len)
{
struct gacopyz_macro_def *def = find_def(srv, name, len);
if (!def)
return;
free(def->name);
free(def->value);
srv->ndefs--;
memmove(def, def + 1,
sizeof(srv->def[0]) *(srv->ndefs - (def - srv->def)));
}
static void
srv_ensure_space(gacopyz_srv_t srv, size_t size)
{
if (size > srv->bufsize) {
char *p;
GACOPYZ_ALLOC(srv, p = realloc(srv->buf, size));
srv->buf = p;
srv->bufsize = size;
}
}
size_t
macro_size(gacopyz_srv_t srv)
{
size_t i;
size_t size = 0;
struct gacopyz_macro_def *def;
for (i = 0, def = srv->def; i < srv->ndefs; i++, def++) {
size_t len = strlen(def->name);
size += len;
if (len > 1 && def->name[0] != '{')
size += 2; /* for {} */
size += 1 + strlen(def->value) + 1;
}
return size;
}
void
srv_format_macros(gacopyz_srv_t srv, unsigned char cmd, size_t *psize)
{
size_t i;
char *p;
size_t size = macro_size(srv) + 1;
struct gacopyz_macro_def *def;
srv_ensure_space(srv, size);
p = srv->buf;
*p++ = cmd;
for (i = 0, def = srv->def; i < srv->ndefs; i++, def++) {
size_t len = strlen(def->name);
if (len > 1 && def->name[0] != '{') {
*p++ = '{';
memcpy(p, def->name, len);
p += len;
*p++ = '}';
*p++ = 0;
} else {
len++;
memcpy(p, def->name, len);
p += len;
}
len = strlen(def->value) + 1;
memcpy(p, def->value, len);
p += len;
}
*psize = size;
}
#define GETNAMELEN(name, len) \
len = strlen(name); \
if (name[0] == '{' && name[len-1] == '}') { \
name++; \
len -= 2; \
}
int
gacopyz_srv_find_macro(gacopyz_srv_t srv, const char *name, const char **pval)
{
struct gacopyz_macro_def *def;
size_t len;
GACOPYZ_ASSERT(srv);
GETNAMELEN(name, len);
def = find_def(srv, name, len);
if (def) {
*pval = def->value;
return MI_SUCCESS;
}
return MI_FAILURE;
}
void
gacopyz_srv_define_macro(gacopyz_srv_t srv,
const char *name, const char *value)
{
size_t len;
GACOPYZ_ASSERT(srv);
GETNAMELEN(name, len);
add_def(srv, name, len, value);
}
void
gacopyz_srv_del_macro(gacopyz_srv_t srv, const char *name)
{
size_t len;
GACOPYZ_ASSERT(srv);
GETNAMELEN(name, len);
del_def(srv, name, len);
}
void
gacopyz_srv_clear_macros(gacopyz_srv_t srv)
{
size_t i;
GACOPYZ_ASSERT(srv);
for (i = 0; i < srv->ndefs; i++) {
free(srv->def[i].name);
free(srv->def[i].value);
}
srv->ndefs = 0;
}
void
gacopyz_srv_clear_macros_pred(gacopyz_srv_t srv,
int (*pred)(const char*, void*),
void *data)
{
size_t i, j;
GACOPYZ_ASSERT(srv);
for (i = j = 0; i < srv->ndefs; i++) {
if (pred(srv->def[i].name, data)) {
free(srv->def[i].name);
free(srv->def[i].value);
} else
srv->def[j++] = srv->def[i];
}
srv->ndefs = j;
}
void
gacopyz_srv_iterate_macros(gacopyz_srv_t srv,
int (*func)(const char *name, const char *value,
void *data),
void *data)
{
size_t i;
GACOPYZ_ASSERT(srv);
for (i = 0; i < srv->ndefs; i++)
if (func(srv->def[i].name, srv->def[i].value, data))
break;
}
void
gacopyz_srv_count_macros(gacopyz_srv_t srv, size_t *count)
{
GACOPYZ_ASSERT(srv);
*count = srv->ndefs;
}
const char **
gacopyz_srv_get_required_macros(gacopyz_srv_t srv, enum gacopyz_stage stage)
{
return (const char **) srv->req_macros[stage];
}
const char *
gacopyz_srv_get_id(gacopyz_srv_t srv)
{
return srv->id;
}
const char *
gacopyz_srv_get_portspec(gacopyz_srv_t srv)
{
return srv->portspec;
}
int
gacopyz_srv_get_flags(gacopyz_srv_t srv)
{
return srv->flags & ~_SRV_SYS_MASK;
}
int
gacopyz_srv_flags(gacopyz_srv_t srv, int flags, enum gacopyz_flag_op op)
{
if (flags & _SRV_SYS_MASK)
return EINVAL;
switch (op) {
case gacopyz_flag_rpl:
srv->flags = (srv->flags & _SRV_SYS_MASK) | flags;
break;
case gacopyz_flag_set:
srv->flags |= flags;
break;
case gacopyz_flag_clr:
srv->flags &= ~flags;
}
/* Whether set or cleared, the GACOPYZ_SRV_DISABLED bit
implies clearing _SRV_CLRDIS. */
if (flags & GACOPYZ_SRV_DISABLED)
flags &= ~_SRV_CLRDIS;
return 0;
}
int
gacopyz_srv_get_logmask(gacopyz_srv_t srv)
{
return srv->iod.logmask;
}
int
gacopyz_srv_get_fd(gacopyz_srv_t srv)
{
return srv->iod.sd;
}
int
gacopyz_srv_get_onerr(gacopyz_srv_t srv)
{
return srv->onerror;
}
struct timeval default_gacopyz_timeout[GACOPYZ_TO_COUNT] = {
{ GACOPYZ_WRITE_TIMEOUT, 0 },
{ GACOPYZ_READ_TIMEOUT, 0 },
{ GACOPYZ_EOM_TIMEOUT, 0 },
{ GACOPYZ_CONNECT_TIMEOUT, 0 }
};
int
gacopyz_srv_create(gacopyz_srv_t *p, const char *name,
const char *portspec, unsigned logmask)
{
gacopyz_srv_t srv = calloc(1, sizeof(*srv));
if (!srv)
return MI_FAILURE;
srv->id = strdup(name);
srv->portspec = strdup(portspec);
srv->iod.sd = -1;
srv->iod.logmask = logmask;
memcpy(srv->iod.timeout, default_gacopyz_timeout,
sizeof srv->iod.timeout);
srv->flags = 0;
srv->onerror = SMFIR_TEMPFAIL;
srv->version = SMFI_VERSION;
srv->proto = SMFI_DEFAULT_PROT;
srv->acts = SMFI_DEFAULT_ACTS;
srv->memerror = default_memerror;
*p = srv;
return MI_SUCCESS;
}
static int
parse_X_spec(const char *spec, char *xspec, char **id, char **port,
struct timeval to[], int *onerr, int debug)
{
char *p, *q;
int havef = 0, havet = 0;
p = strtok(xspec, ",");
if (!p) {
if (debug)
gacopyz_log(SMI_LOG_DEBUG,
_("%s: not a valid X spec"),
spec);
return MI_FAILURE;
}
if (p[1] != '=') {
*id = p;
p = strtok(NULL, ",");
if (!p) {
if (debug)
gacopyz_log(SMI_LOG_DEBUG,
_("%s: not a valid X spec"),
spec);
return MI_FAILURE;
}
}
do {
while (*p && isspace(*p))
p++;
if (!*p)
break;
if (p[1] != '=') {
if (debug)
gacopyz_log(SMI_LOG_DEBUG,
_("%s: not a valid X spec, "
"missing = near %s"),
spec, p);
return MI_FAILURE;
}
switch (p[0]) {
case 'S':
if (*port) {
if (debug)
gacopyz_log(SMI_LOG_DEBUG,
_("%s: not a valid X spec, "
"duplicate field %s"),
spec, p);
return MI_FAILURE;
}
*port = p + 2;
break;
case 'F':
if (havef) {
if (debug)
gacopyz_log(SMI_LOG_DEBUG,
_("%s: not a valid X spec, "
"duplicate field %s"),
spec, p);
return MI_FAILURE;
}
havef = 1;
if (strlen(p) != 3) {
if (debug)
gacopyz_log(SMI_LOG_DEBUG,
_("%s: not a valid X spec, "
"invalid field %s"),
spec, p);
return MI_FAILURE;
}
switch (p[2]) {
case 'R':
*onerr = SMFIR_REJECT;
break;
case 'T':
*onerr = SMFIR_TEMPFAIL;
break;
default:
if (debug)
gacopyz_log(SMI_LOG_DEBUG,
_("%s: not a valid X spec, "
"invalid field %s"),
spec, p);
return MI_FAILURE;
}
break;
case 'T':
if (havet) {
if (debug)
gacopyz_log(SMI_LOG_DEBUG,
_("%s: not a valid X spec, "
"duplicate field %s"),
spec, p);
return MI_FAILURE;
}
havet = 1;
for (q = p + 2; *q; q++) {
int n;
unsigned long t;
char *ep;
switch (*q) {
case 'C':
n = GACOPYZ_TO_CONNECT;
break;
case 'S':
n = GACOPYZ_TO_WRITE;
break;
case 'R':
n = GACOPYZ_TO_READ;
break;
case 'E':
n = GACOPYZ_TO_EOM;
break;
default:
if (debug)
gacopyz_log(SMI_LOG_DEBUG,
_("%s: not a valid X spec, "
"invalid T field near %s"),
spec, q);
return MI_FAILURE;
}
if (*++q != ':') {
if (debug)
gacopyz_log(SMI_LOG_DEBUG,
_("%s: not a valid X spec, "
"invalid T field near %s"),
spec, q);
return MI_FAILURE;
}
t = strtoul(++q, &ep, 0);
switch (*ep) {
case 0:
case ';':
break;
case 's':
ep++;
break;
case 'm':
t *= 60;
ep++;
break;
default:
if (debug)
gacopyz_log(SMI_LOG_DEBUG,
_("%s: not a valid X spec, "
"invalid T field near %s"),
spec, q);
return MI_FAILURE;
}
to[n].tv_sec = t;
to[n].tv_usec = 0;
q = ep;
if (*q == 0)
break;
else if (*q != ';') {
if (debug)
gacopyz_log(SMI_LOG_DEBUG,
_("%s: not a valid X spec, "
"invalid delimiter near %s"),
spec, q);
return MI_FAILURE;
}
}
}
} while (p = strtok(NULL, ","));
return MI_SUCCESS;
}
int
gacopyz_srv_create_X(gacopyz_srv_t *p, const char *spec, unsigned logmask)
{
int rc;
char *xspec = strdup(spec);
char *id = "default";
char *port = NULL;
struct timeval to[GACOPYZ_TO_COUNT];
int onerr = SMFIR_CONTINUE;
memcpy(to, default_gacopyz_timeout, sizeof to);
if (parse_X_spec(spec, xspec, &id, &port, to, &onerr,
logmask & SMI_LOG_MASK(SMI_LOG_DEBUG)) == MI_FAILURE) {
free(xspec);
return MI_FAILURE;
}
if (!port) {
if (logmask & SMI_LOG_MASK(SMI_LOG_DEBUG))
gacopyz_log(SMI_LOG_DEBUG,
_("%s: not a valid X spec, "
"missing port"),
spec);
free(xspec);
return MI_FAILURE;
}
rc = gacopyz_srv_create(p, id, port, logmask);
free(xspec);
if (rc)
return MI_FAILURE;
gacopyz_srv_set_all_timeouts(*p, to);
gacopyz_srv_onerror(*p, onerr);
return MI_SUCCESS;
}
int
gacopyz_srv_onerror(gacopyz_srv_t srv, int code)
{
GACOPYZ_ASSERT(srv);
switch (code) {
case SMFIR_CONTINUE:
case SMFIR_DISCARD:
case SMFIR_REJECT:
case SMFIR_TEMPFAIL:
case SMFIR_SHUTDOWN:
srv->onerror = code;
return MI_SUCCESS;
}
return MI_FAILURE;
}
void
gacopyz_srv_set_logmask(gacopyz_srv_t srv, int logmask)
{
GACOPYZ_ASSERT(srv);
srv->iod.logmask = logmask;
}
void
gacopyz_srv_set_callback(gacopyz_srv_t srv,
int (*cb)(gacopyz_srv_t, int, int, void *))
{
GACOPYZ_ASSERT(srv);
srv->cb_reply = cb;
return;
}
void
gacopyz_srv_set_callback_data(gacopyz_srv_t srv, void *data)
{
GACOPYZ_ASSERT(srv);
srv->cb_data = data;
return;
}
void
gacopyz_srv_set_memerror(gacopyz_srv_t srv,
void (*memerror)(gacopyz_srv_t, const char *,
unsigned int))
{
GACOPYZ_ASSERT(srv);
srv->memerror = memerror;
}
void
gacopyz_srv_set_all_timeouts(gacopyz_srv_t srv, struct timeval *tvp)
{
GACOPYZ_ASSERT(srv);
memcpy(srv->iod.timeout, tvp, sizeof srv->iod.timeout);
}
int
gacopyz_srv_set_timeout(gacopyz_srv_t srv, unsigned n, struct timeval *tvp)
{
GACOPYZ_ASSERT(srv);
if (n >= GACOPYZ_TO_COUNT)
return MI_FAILURE;
srv->iod.timeout[n] = *tvp;
return MI_SUCCESS;
}
int
gacopyz_srv_set_timeout_sec(gacopyz_srv_t srv, unsigned n, time_t t)
{
struct timeval tv;
tv.tv_usec = 0;
tv.tv_sec = t;
return gacopyz_srv_set_timeout(srv, n, &tv);
}
int
gacopyz_srv_set_source(gacopyz_srv_t srv, struct sockaddr *sa, socklen_t len)
{
GACOPYZ_ASSERT(srv);
srv->source_addr = malloc(len);
if (!srv->source_addr)
return MI_FAILURE;
memcpy (srv->source_addr, sa, len);
srv->source_addr_len = len;
return MI_SUCCESS;
}
int
gacopyz_srv_set_version(gacopyz_srv_t srv, unsigned long version)
{
GACOPYZ_ASSERT(srv);
srv->version = version;
return MI_SUCCESS;
}
int
gacopyz_srv_set_protocol(gacopyz_srv_t srv, unsigned long proto)
{
GACOPYZ_ASSERT(srv);
srv->proto = proto;
return MI_SUCCESS;
}
int
gacopyz_srv_set_actions(gacopyz_srv_t srv, unsigned long acts)
{
GACOPYZ_ASSERT(srv);
srv->acts = acts;
return MI_SUCCESS;
}
void
gacopyz_srv_destroy(gacopyz_srv_t *p)
{
int i;
gacopyz_srv_t srv = *p;
gacopyz_srv_clear_macros(srv);
free(srv->portspec);
free(srv->id);
free(srv->def);
free(srv->source_addr);
for (i = 0; i < gacopyz_stage_max; i++)
if (srv->req_macros[i]) {
int j;
for (j = 0; srv->req_macros[i][j]; j++)
free(srv->req_macros[i][j]);
free(srv->req_macros[i]);
}
free(srv->buf);
free(srv);
*p = 0;
}
static int
srv_connect(gacopyz_srv_t srv, char *proto, char *port, char *path)
{
milter_sockaddr_t addr;
int socklen;
int fd = -1;
struct timeval start, connect_tv;
if (!proto
|| strcmp(proto, "unix") == 0 || strcmp(proto, "local") == 0) {
struct stat st;
if (port) {
gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
_("invalid connection type: %s; "
"port is meaningless for UNIX sockets"),
srv->portspec);
return -1;
}
if (strlen(path) >= sizeof addr.sunix.sun_path) {
errno = EINVAL;
gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
_("%s: UNIX socket name too long"),
path);
return -1;
}
addr.sa.sa_family = PF_UNIX;
socklen = sizeof(addr.sunix);
strcpy(addr.sunix.sun_path, path);
if (stat(path, &st)) {
gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
_("%s: cannot stat socket: %s"),
path, strerror(errno));
return -1;
} else {
/* FIXME: Check permissions? */
if (!S_ISSOCK(st.st_mode)) {
gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
_("%s: not a socket"),
path);
return -1;
}
}
} else if (strcmp(proto, "inet") == 0) {
short pnum;
long num;
char *p;
addr.sa.sa_family = PF_INET;
socklen = sizeof(addr.sin);
if (!port) {
gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
_("invalid connection type: %s; "
"missing port number"),
srv->portspec);
return -1;
}
num = pnum = strtol(port, &p, 0);
if (*p == 0) {
if (num != pnum) {
gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
_("invalid connection type: %s; "
"bad port number"),
srv->portspec);
return -1;
}
pnum = htons(pnum);
} else {
struct servent *sp = getservbyname(port, "tcp");
if (!sp) {
gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
_("invalid connection type: %s; "
"unknown port name"),
srv->portspec);
return -1;
}
pnum = sp->s_port;
}
if (!path)
addr.sin.sin_addr.s_addr = INADDR_ANY;
else {
struct hostent *hp = gethostbyname(path);
if (!hp) {
gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
_("unknown host name %s"),
path);
return -1;
}
addr.sa.sa_family = hp->h_addrtype;
switch (hp->h_addrtype) {
case AF_INET:
memmove(&addr.sin.sin_addr, hp->h_addr, 4);
addr.sin.sin_port = pnum;
break;
default:
gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
_("invalid connection type: %s; "
"unsupported address family"),
srv->portspec);
return -1;
}
}
#ifdef GACOPYZ_IPV6
} else if (strcmp(proto, "inet6") == 0) {
int rc;
struct addrinfo hints;
struct addrinfo *res;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_INET6;
hints.ai_socktype = SOCK_STREAM;
if (!path)
hints.ai_flags |= AI_PASSIVE;
rc = getaddrinfo(path, port, &hints, &res);
switch (rc) {
case 0:
break;
case EAI_SYSTEM:
gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
_("%s:%s: cannot parse address: %s"),
path, port, strerror(errno));
return -1;
case EAI_BADFLAGS:
case EAI_SOCKTYPE:
gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
_("%s:%d: internal error "
"converting %s:%s"),
__FILE__, __LINE__, path, port);
return -1;
case EAI_MEMORY:
gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
"not enogh memory");
return -1;
default:
gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
"%s:%s: %s",
path, port, gai_strerror(rc));
return -1;
}
if (res->ai_addrlen > sizeof(addr)) {
gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
_("%s:%s: address length too big (%lu)"),
path, port,
(unsigned long) res->ai_addrlen);
freeaddrinfo(res);
return -1;
}
if (res->ai_next) {
char host[NI_MAXHOST], serv[NI_MAXSERV];
rc = getnameinfo(res->ai_addr, res->ai_addrlen,
host, sizeof host,
serv, sizeof serv,
NI_NUMERICHOST|NI_NUMERICSERV);
gacopyz_io_log(&srv->iod, SMI_LOG_WARN,
_("%s:%s resolves to several addresses; "
"using %s:%s"),
path, port, host, serv);
}
memcpy(&addr, res->ai_addr, res->ai_addrlen);
socklen = sizeof(addr.sin6);
freeaddrinfo(res);
#endif
} else {
gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
_("unsupported protocol: %s"),
proto);
return -1;
}
fd = socket(addr.sa.sa_family, SOCK_STREAM, 0);
if (fd == -1) {
gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
_("unable to create new socket: %s"),
strerror(errno));
return -1;
}
if (srv->source_addr && addr.sa.sa_family != PF_UNIX) {
if (bind(fd, srv->source_addr, srv->source_addr_len) < 0) {
gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
_("cannot bind to local address: %s"),
strerror(errno));
close(fd);
return -1;
}
}
connect_tv = srv->iod.timeout[GACOPYZ_TO_CONNECT];
gettimeofday(&start, NULL);
for (;;) {
int rc = connect(fd, &addr.sa, socklen);
if (rc == 0)
break;
else if (errno == ECONNREFUSED) {
struct timeval tv, now;
fd_set rset, wset, xset;
gettimeofday(&now, NULL);
timersub(&now, &start, &tv);
if (timercmp(&tv, &connect_tv, < )) {
FD_ZERO(&rset);
FD_SET(fd, &rset);
FD_ZERO(&wset);
FD_SET(fd, &wset);
FD_ZERO(&xset);
FD_SET(fd, &xset);
select(fd + 1, &rset, &wset, &xset, &tv);
continue;
}
}
gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
_("cannot connect to %s: %s"),
srv->portspec, strerror(errno));
close(fd);
return -1;
}
return fd;
}
int
gacopyz_srv_open(gacopyz_srv_t srv)
{
char *proto;
char *port;
char *path;
GACOPYZ_ASSERT(srv);
gacopyz_io_log(&srv->iod, SMI_LOG_DEBUG,
_("connecting to %s"), srv->portspec);
if (srv->flags & _SRV_CONNECTED) {
gacopyz_io_log(&srv->iod, SMI_LOG_DEBUG,
_("%s: already connected, disconnect first"),
srv->id);
return MI_FAILURE;
}
GACOPYZ_ALLOC(srv,
gacopyz_parse_connection(srv->portspec,
&proto, &port, &path)==0);
srv->iod.sd = srv_connect(srv, proto, port, path);
free(proto);
free(port);
free(path);
if (srv->iod.sd == -1)
return MI_FAILURE;
srv->flags |= _SRV_CONNECTED;
return MI_SUCCESS;
}
#define ISWS(c) ((c) == ' ' || (c) == '\t')
static void
parse_macros(gacopyz_srv_t srv, enum gacopyz_stage stage, char *buf)
{
char *p;
char **argv;
size_t count;
count = 0;
for (p = buf; *p;) {
while (*p && ISWS(*p))
p++;
if (*p) {
count++;
while (*p && !ISWS(*p))
p++;
}
}
if (count == 0)
return;
GACOPYZ_ALLOC(srv, argv = calloc(count + 1, sizeof(argv[0])));
count = 0;
for (p = buf; *p;) {
while (*p && ISWS(*p))
p++;
if (*p) {
size_t i;
char *str = p;
for (i = 0; *p && !ISWS(*p); p++, i++)
;
if (i > 0) {
char *newstr;
if (*str == '{' && str[i-1] == '}') {
str++;
i -= 2;
}
GACOPYZ_ALLOC(srv, newstr = malloc(i + 1));
memcpy(newstr, str, i);
newstr[i] = 0;
argv[count++] = newstr;
}
}
}
argv[count] = NULL;
srv->req_macros[stage] = argv;
}
static void
read_macros(gacopyz_srv_t srv)
{
char *buf = srv->buf + 3 * sizeof(gacopyz_uint32_t);
size_t size = srv->bufsize - 3 * sizeof(gacopyz_uint32_t);
while (size > sizeof(gacopyz_uint32_t)) {
gacopyz_uint32_t v;
unsigned n;
size_t len;
memcpy(&v, buf, sizeof(gacopyz_uint32_t));
n = ntohl(v);
if (n >= gacopyz_stage_max) {
gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
_("received invalid stage number"));
break;
}
buf += sizeof(gacopyz_uint32_t);
size -= sizeof(gacopyz_uint32_t);
if (size == 0)
break;
len = strlen(buf) + 1;
if (len > size) {
gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
_("invalid macro list"));
break;
}
gacopyz_io_log(&srv->iod, SMI_LOG_DEBUG,
_("macros for stage \"%s\" (%d): %s"),
gacopyz_stage_name[n], n, buf);
parse_macros(srv, n, buf);
buf += len;
size -= len;
}
}
int
gacopyz_srv_negotiate(gacopyz_srv_t srv)
{
gacopyz_uint32_t ibuf[3];
size_t size;
unsigned char cmd;
unsigned long milter_version, milter_acts, milter_proto;
GACOPYZ_ASSERT(srv);
if (!(srv->flags & _SRV_CONNECTED)) {
gacopyz_io_log(&srv->iod, SMI_LOG_DEBUG,
_("%s: server not connected, cannot negotiate"),
srv->id);
return MI_FAILURE;
}
if (srv->flags & _SRV_READY) {
gacopyz_io_log(&srv->iod, SMI_LOG_DEBUG,
_("%s: server already passed negotiation"),
srv->id);
return MI_FAILURE;
}
ibuf[0] = htonl(srv->version);
ibuf[1] = htonl(srv->acts);
ibuf[2] = htonl(srv->proto);
if (gacopyz_send_command(&srv->iod, SMFIC_OPTNEG, ibuf, sizeof ibuf))
return MI_FAILURE;
if (gacopyz_read_command(&srv->iod, GACOPYZ_TO_READ, &cmd, &size,
&srv->buf, &srv->bufsize)) {
gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
_("error reading reply to SMFIC_OPTNEG: %s"),
strerror(errno));
return MI_FAILURE;
}
if (cmd != SMFIC_OPTNEG) {
gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
_("gacopyz_srv_negotiate: got %c instead of %c"),
cmd, SMFIC_OPTNEG);
return MI_FAILURE;
}
if (size < sizeof ibuf) {
gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
_("gacopyz_srv_negotiate: not enough data returned"));
return MI_FAILURE;
}
milter_version = ntohl(((gacopyz_uint32_t*)srv->buf)[0]);
milter_acts = ntohl(((gacopyz_uint32_t*)srv->buf)[1]);
milter_proto = ntohl(((gacopyz_uint32_t*)srv->buf)[2]);
/* Check version */
if (milter_version == 1 || milter_version > SMFI_VERSION
|| milter_version > srv->version) {
gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
_("client requested unsupported milter "
"version: %#lx"), milter_version);
return MI_FAILURE;
}
/* Check action capabilities */
if ((milter_acts & srv->acts) != milter_acts) {
gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
_("filter abilities %#lx do not match MTA "
"milter abilities %#lx"),
milter_acts, srv->acts);
return MI_FAILURE;
}
/* Check protocol */
if ((milter_proto & srv->proto) != milter_proto) {
gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
_("protocol abilities %#lx do not match MTA "
"milter abilities %#lx"),
milter_proto, srv->proto);
return MI_FAILURE;
}
if (milter_version > 3
&& srv->bufsize > 3 * sizeof(gacopyz_uint32_t))
read_macros(srv);
srv->version = milter_version;
srv->acts = milter_acts;
srv->proto = milter_proto;
srv->flags |= _SRV_READY;
return MI_SUCCESS;
}
static struct command_assoc {
unsigned char cmd;
int pflag;
const char *action;
const char *code;
} command_tab[] = {
{ SMFIC_CONNECT, SMFIP_NOCONNECT, "connect", "554" },
{ SMFIC_HELO, SMFIP_NOHELO, "helo", "550" },
{ SMFIC_MAIL, SMFIP_NOMAIL, "mail", "550 5.7.1" },
{ SMFIC_RCPT, SMFIP_NORCPT, "rcpt", "550 5.7.1" },
{ SMFIC_HEADER, SMFIP_NOHDRS, "header", "550 5.7.1" },
{ SMFIC_BODY, SMFIP_NOBODY, "body", "554 5.7.1" },
{ SMFIC_EOH, SMFIP_NOEOH, "eoh", "550 5.7.1" },
{ SMFIC_UNKNOWN, SMFIP_NOUNKNOWN, "unknown", "550 5.7.1" },
{ SMFIC_DATA, SMFIP_NODATA, "data", "550 5.7.1" },
{ 0, 0, "default", "550 5.7.1" }
};
static struct command_assoc *
find_cmd_assoc(unsigned char cmd)
{
struct command_assoc *cp;
for (cp = command_tab; cp->cmd; cp++)
if (cp->cmd == cmd)
break;
return cp;
}
#define is_rfc822_reject_code(s) \
(((s)[0] == '4' || (s)[0] == '5') \
&& isascii((s)[1]) && isdigit((s)[1]) \
&& isascii((s)[2]) && isdigit((s)[2]))
#define DEFREPLY "Command rejected"
static void
verify_reply(gacopyz_srv_t srv, size_t size, const char *reject_code)
{
if (size < 3 || !is_rfc822_reject_code(srv->buf)) {
srv_ensure_space(srv,
strlen(reject_code) + 1
+ sizeof(DEFREPLY));
strcpy(srv->buf, reject_code);
strcat(srv->buf, " ");
strcat(srv->buf, DEFREPLY);
}
}
int
gacopyz_srv_send_macros(gacopyz_srv_t srv, unsigned char cmd)
{
size_t size;
switch (cmd) {
case SMFIC_CONNECT:
case SMFIC_HELO:
case SMFIC_MAIL:
case SMFIC_RCPT:
case SMFIC_DATA:
case SMFIC_BODYEOB:
case SMFIC_EOH:
srv_format_macros(srv, cmd, &size);
if (gacopyz_send_command(&srv->iod, SMFIC_MACRO,
srv->buf, size))
return MI_FAILURE;
}
return MI_SUCCESS;
}
int
gacopyz_srv_send_command(gacopyz_srv_t srv, unsigned char cmd,
const void *data, size_t datasize)
{
unsigned char rcmd;
size_t size;
struct command_assoc *cp;
int again;
int t;
gacopyz_io_log(&srv->iod, SMI_LOG_DEBUG,
_("gacopyz_srv_send_command: size=%lu, cmd=%c"),
datasize, cmd);
cp = find_cmd_assoc(cmd);
srv->resp_ptr = NULL;
srv->resp_size = 0;
if (cp->pflag && (srv->proto & cp->pflag))
return SMFIR_CONTINUE;
if (gacopyz_srv_send_macros(srv, cmd))
return srv->onerror;
if (gacopyz_send_command(&srv->iod, cmd, data, datasize))
return srv->onerror;
if (cmd == SMFIC_HEADER && (srv->proto & SMFIP_NOHREPL))
return SMFIR_CONTINUE;
if (cmd == SMFIC_BODYEOB)
t = GACOPYZ_TO_EOM;
else
t = GACOPYZ_TO_READ;
do {
again = 0;
if (gacopyz_read_command(&srv->iod, t, &rcmd, &size,
&srv->buf, &srv->bufsize)) {
gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
_("error reading reply to '%c': %s"),
cmd, strerror(errno));
return srv->onerror;
}
switch (rcmd) {
case SMFIR_REPLYCODE:
verify_reply(srv, size, cp->code);
gacopyz_io_log(&srv->iod, SMI_LOG_DEBUG,
"action=%s, reject=%s",
cp->action, srv->buf);
srv->resp_ptr = srv->buf;
srv->resp_size = size;
switch (cmd) {
case SMFIC_BODY:
case SMFIC_HEADER:
case SMFIC_EOH:
srv->flags |= GACOPYZ_SRV_DISABLED
| _SRV_CLRDIS;
}
break;
case SMFIR_REJECT:
gacopyz_io_log(&srv->iod, SMI_LOG_DEBUG,
"action=%s, reject",
cp->action);
break;
case SMFIR_DISCARD:
gacopyz_io_log(&srv->iod, SMI_LOG_DEBUG,
"action=%s, discard",
cp->action);
srv->flags |= GACOPYZ_SRV_DISABLED | _SRV_CLRDIS;
break;
case SMFIR_TEMPFAIL:
gacopyz_io_log(&srv->iod, SMI_LOG_DEBUG,
"action=%s, tempfail",
cp->action);
break;
case SMFIR_ACCEPT:
gacopyz_io_log(&srv->iod, SMI_LOG_DEBUG,
"action=%s, accept",
cp->action);
srv->flags |= GACOPYZ_SRV_DISABLED | _SRV_CLRDIS;
break;
case SMFIR_CONTINUE:
gacopyz_io_log(&srv->iod, SMI_LOG_DEBUG,
"action=%s, continue",
cp->action);
break;
case SMFIR_ADDRCPT:
case SMFIR_DELRCPT:
case SMFIR_REPLBODY:
case SMFIR_CHGHEADER:
case SMFIR_ADDHEADER:
case SMFIR_INSHEADER:
again = 1;
srv->resp_ptr = srv->buf;
srv->resp_size = size;
gacopyz_io_log(&srv->iod, SMI_LOG_DEBUG,
"action=%s, response=%c",
cp->action, rcmd);
break;
case SMFIR_PROGRESS:
if (cmd == SMFIC_BODYEOB) {
gacopyz_io_log(&srv->iod, SMI_LOG_DEBUG,
"action=%s, progress",
cp->action);
again = 1;
break;
}
/* fall through */
default:
gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
"action=%s, unhandled response=%c",
cp->action, rcmd);
/*FIXME: Change to error state? */
rcmd = srv->onerror;
break;
}
if (srv->cb_reply)
srv->cb_reply(srv, cmd, rcmd, srv->cb_data);
} while (again);
return rcmd;
}
int
gacopyz_srv_reply(gacopyz_srv_t srv, char **pmsg, size_t *psize)
{
size_t i, j;
char *buf;
if (!srv->resp_ptr) {
errno = ENOMSG;
return MI_FAILURE;
}
buf = malloc(srv->resp_size + 1);
if (!buf)
return MI_FAILURE;
for (i = j = 0; i < srv->resp_size; i++, j++) {
if (srv->resp_ptr[i] == '%' && srv->resp_ptr[i+1] == '%')
i++;
buf[j] = srv->resp_ptr[i];
}
buf[j] = 0;
*pmsg = buf;
if (psize)
*psize = j;
return 0;
}
void
gacopyz_srv_reply_raw(gacopyz_srv_t srv, char **msg, size_t *size)
{
*msg = srv->resp_ptr;
*size = srv->resp_size;
}
int
gacopyz_srv_abort(gacopyz_srv_t srv)
{
GACOPYZ_ASSERT(srv);
gacopyz_io_log(&srv->iod, SMI_LOG_DEBUG, "gacopyz_srv_abort");
if (srv->flags & _SRV_CLRDIS)
gacopyz_srv_flags(srv, GACOPYZ_SRV_DISABLED, gacopyz_flag_clr);
return gacopyz_send_command(&srv->iod, SMFIC_ABORT, NULL, 0);
}
int
gacopyz_srv_quit(gacopyz_srv_t srv)
{
GACOPYZ_ASSERT(srv);
gacopyz_io_log(&srv->iod, SMI_LOG_DEBUG, "gacopyz_srv_quit");
srv->flags = _SRV_CONNECTED;
return gacopyz_send_command(&srv->iod, SMFIC_QUIT, NULL, 0);
}
#define _SRV_ASSERT_FLAG(srv, mask, fun) \
do { \
if (!((srv)->flags & mask)) { \
gacopyz_io_log(&srv->iod, SMI_LOG_DEBUG, \
_("%s: %s called with flags=%x"), \
(srv)->id, mask); \
return srv->onerror; \
} \
} while (0)
static void
format_argv(gacopyz_srv_t srv, char **argv, char **buf, size_t *psize)
{
size_t len;
char **ap;
char *p;
for (len = 1, ap = argv; *ap; ap++)
len += strlen(*ap) + 1;
GACOPYZ_ALLOC(srv, *buf = malloc(len));
for (p = *buf, ap = argv; *ap; ap++) {
size_t len = strlen(*ap) + 1;
memcpy(p, *ap, len);
p += len;
}
*p = 0;
*psize = len;
}
int
gacopyz_srv_connect(gacopyz_srv_t srv, const char *hostname,
struct sockaddr *sa)
{
int rc;
int family = SMFIA_UNKNOWN;
char *arg;
size_t argsize, len;
size_t size;
char *s = "";
char *sbuf = NULL;
char *buf;
unsigned short port = 0;
GACOPYZ_ASSERT(srv);
_SRV_ASSERT_FLAG(srv, _SRV_READY, "gacopyz_srv_connect");
if (srv->flags & GACOPYZ_SRV_DISABLED)
return SMFIR_CONTINUE;
if (sa) {
switch (sa->sa_family) {
case AF_UNIX: {
struct sockaddr_un *sptr = (struct sockaddr_un *) sa;
family = SMFIA_UNIX;
port = 0;
len = strlen(sptr->sun_path);
s = sptr->sun_path;
break;
}
case AF_INET: {
struct sockaddr_in *sptr = (struct sockaddr_in *) sa;
family = SMFIA_INET;
port = sptr->sin_port;
s = inet_ntoa(sptr->sin_addr);
if (!s) {
gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
"inet_ntoa: %s",
strerror(errno));
return MI_FAILURE;
}
break;
}
#ifdef GACOPYZ_IPV6
case AF_INET6: {
char host[NI_MAXHOST];
char service[NI_MAXSERV];
int rc;
rc = getnameinfo(sa, sizeof(struct sockaddr_in6),
host, sizeof(host),
service, sizeof(service),
NI_NUMERICHOST|NI_NUMERICSERV);
if (rc) {
gacopyz_io_log(&srv->iod, SMI_LOG_ERR,
"getnameinfo: %s",
gai_strerror(rc));
return MI_FAILURE;
}
family = SMFIA_INET6;
port = htons(atoi(service));
GACOPYZ_ALLOC(srv,
sbuf = malloc(GACOPYZ_IPV6PREFIX_LEN +
strlen(host) + 1));
s = sbuf;
strcat(strcpy(s, GACOPYZ_IPV6PREFIX_STR), host);
break;
}
#endif
default:
break;
}
}
len = strlen(s);
argsize = 2 + sizeof(port) + len;
GACOPYZ_ALLOC(srv, arg = malloc(argsize));
arg[0] = family;
memcpy(arg + 1, &port, sizeof(port));
memcpy(arg + 1 + sizeof(port), s, len + 1);
free(sbuf);
len = strlen(hostname);
size = 1 + len + 1 + argsize;
GACOPYZ_ALLOC(srv, buf = malloc(size));
memcpy(buf, hostname, len + 1);
memcpy(buf + len + 1, arg, argsize);
buf[len + argsize + 1] = 0;
free(arg);
rc = gacopyz_srv_send_command(srv, SMFIC_CONNECT, buf, size);
free(buf);
return rc;
}
int
gacopyz_srv_helo(gacopyz_srv_t srv, const char *domain)
{
GACOPYZ_ASSERT(srv);
_SRV_ASSERT_FLAG(srv, _SRV_READY, "gacopyz_srv_helo");
if (srv->flags & GACOPYZ_SRV_DISABLED)
return SMFIR_CONTINUE;
return gacopyz_srv_send_command(srv, SMFIC_HELO,
domain, strlen(domain) + 1);
}
int
gacopyz_srv_envfrom(gacopyz_srv_t srv, char **argv)
{
int rc;
size_t size;
char *buf;
GACOPYZ_ASSERT(srv);
_SRV_ASSERT_FLAG(srv, _SRV_READY, "gacopyz_srv_envfrom");
if (srv->flags & GACOPYZ_SRV_DISABLED)
return SMFIR_CONTINUE;
format_argv(srv, argv, &buf, &size);
rc = gacopyz_srv_send_command(srv, SMFIC_MAIL, buf, size);
free(buf);
return rc;
}
int
gacopyz_srv_envrcpt(gacopyz_srv_t srv, char **argv)
{
int rc;
size_t size;
char *buf;
GACOPYZ_ASSERT(srv);
_SRV_ASSERT_FLAG(srv, _SRV_READY, "gacopyz_srv_envrcpt");
if (srv->flags & GACOPYZ_SRV_DISABLED)
return SMFIR_CONTINUE;
format_argv(srv, argv, &buf, &size);
rc = gacopyz_srv_send_command(srv, SMFIC_RCPT, buf, size);
free(buf);
return rc;
}
int
gacopyz_srv_data(gacopyz_srv_t srv)
{
GACOPYZ_ASSERT(srv);
_SRV_ASSERT_FLAG(srv, _SRV_READY, "gacopyz_srv_data");
if (srv->flags & GACOPYZ_SRV_DISABLED)
return SMFIR_CONTINUE;
return gacopyz_srv_send_command(srv, SMFIC_DATA, NULL, 0);
}
int
gacopyz_srv_header(gacopyz_srv_t srv, char *name, char *value)
{
int rc;
size_t nlen, vlen, size;
char *buf;
GACOPYZ_ASSERT(srv && name && value);
_SRV_ASSERT_FLAG(srv, _SRV_READY, "gacopyz_srv_header");
if (srv->flags & GACOPYZ_SRV_DISABLED)
return SMFIR_CONTINUE;
nlen = strlen(name);
vlen = strlen(value);
size = nlen + vlen + 2;
GACOPYZ_ALLOC(srv, buf = malloc(size));
memcpy(buf, name, nlen + 1);
memcpy(buf + nlen + 1, value, vlen + 1);
rc = gacopyz_srv_send_command(srv, SMFIC_HEADER, buf, size);
free(buf);
return rc;
}
int
gacopyz_srv_eoh(gacopyz_srv_t srv)
{
GACOPYZ_ASSERT(srv);
_SRV_ASSERT_FLAG(srv, _SRV_READY, "gacopyz_srv_eoh");
if (srv->flags & GACOPYZ_SRV_DISABLED)
return SMFIR_CONTINUE;
return gacopyz_srv_send_command(srv, SMFIC_EOH, NULL, 0);
}
int
gacopyz_srv_body(gacopyz_srv_t srv, unsigned char *str, size_t size)
{
GACOPYZ_ASSERT(srv);
_SRV_ASSERT_FLAG(srv, _SRV_READY, "gacopyz_srv_body");
if (srv->flags & GACOPYZ_SRV_DISABLED)
return SMFIR_CONTINUE;
return gacopyz_srv_send_command(srv, SMFIC_BODY, str, size);
}
int
gacopyz_srv_eom(gacopyz_srv_t srv, unsigned char *chunk, size_t size)
{
int disabled;
GACOPYZ_ASSERT(srv);
_SRV_ASSERT_FLAG(srv, _SRV_READY, "gacopyz_srv_eom");
disabled = srv->flags & GACOPYZ_SRV_DISABLED;
if (srv->flags & _SRV_CLRDIS)
gacopyz_srv_flags(srv, GACOPYZ_SRV_DISABLED, gacopyz_flag_clr);
if (disabled)
return SMFIR_CONTINUE;
return gacopyz_srv_send_command(srv, SMFIC_BODYEOB, chunk, size);
}
int
gacopyz_srv_close(gacopyz_srv_t srv)
{
GACOPYZ_ASSERT(srv);
_SRV_ASSERT_FLAG(srv, _SRV_CONNECTED, "gacopyz_srv_close");
close(srv->iod.sd);
srv->flags = 0;
return MI_SUCCESS;
}