/* 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; }