diff options
Diffstat (limited to 'src/qmail-smtpam.c')
-rw-r--r-- | src/qmail-smtpam.c | 631 |
1 files changed, 631 insertions, 0 deletions
diff --git a/src/qmail-smtpam.c b/src/qmail-smtpam.c new file mode 100644 index 0000000..c0c6550 --- /dev/null +++ b/src/qmail-smtpam.c @@ -0,0 +1,631 @@ +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/stat.h> +#include <netinet/in.h> +#include <arpa/inet.h> +#include <unistd.h> +#include "sig.h" +#include "genalloc.h" +#include "stralloc.h" +#include "buffer.h" +#include "scan.h" +#include "case.h" +#include "byte.h" +#include "error.h" +#include "auto_qmail.h" +#include "control.h" +#include "dns.h" +#include "alloc.h" +#include "quote.h" +#include "ip.h" +#include "ipalloc.h" +#include "ipme.h" +#include "str.h" +#include "now.h" +#include "exit.h" +#include "constmap.h" +#include "tcpto.h" +#include "socket_if.h" +#include "ucspissl.h" +#include "timeout.h" +#include "timeoutconn.h" +#include "tls_remote.h" +#include "tls_errors.h" +#include "tls_timeoutio.h" +#include "uint_t.h" + +#define MAX_SIZE 200000000 +#define HUGESMTPTEXT 5000 +#define PORT_SMTP 25 /* silly rabbit, /etc/services is for users */ +#define PORT_SMTPS 465 +#define VERIFYDEPTH 1 +#define FDPAM 3 + +#define WHO "qmail-smtpam" + +/** @file qmail-smtpam.c -- TLS enabled SMTP PAM to check mailbox at remote MX + */ + +int flagauth = 0; /* 1 = login; 2 = plain; 3 =crammd5 */ +int flagsmtps = 0; /* RFC 8314 - 'implicit TLS' */ +int flagtls = 0; /* -2 = rejected; -1 = not; 0 = no, default; + > 0 see tls_remote.c + +10 = SMTPS; +20 = QMTPS; 100 = active TLS connection */ +int flagverify = 0; /* 1 = verify Cert against CA ; -1 = Cert pinning */ +int flagutf8mail = 0; + +unsigned long port = PORT_SMTP; + +GEN_ALLOC_typedef(saa,stralloc,sa,len,a) +GEN_ALLOC_readyplus(saa,stralloc,sa,len,a,i,n,x,10,saa_readyplus) + +stralloc helohost = {0}; +stralloc host = {0}; +stralloc ports = {0}; +stralloc remotehost = {0}; +stralloc sender = {0}; +stralloc canonhost = {0}; +stralloc canonbox = {0}; +stralloc sendip = {0}; +stralloc recipient = {0}; + +stralloc domainips = {0}; +struct constmap mapdomainips; +char ip4[4]; +char ip6[16]; +uint32 ifidx = 0; + +stralloc routes = {0}; +struct constmap maproutes; + +struct ip_mx partner; + +SSL *ssl; +SSL_CTX *ctx; + +void out(char *s) { if (buffer_puts(buffer_1small,s) == -1) _exit(111); } +void zero() { if (buffer_put(buffer_1small,"\0",1) == -1) _exit(111); } +void zerodie() { zero(); buffer_flush(buffer_1small); _exit(111); } +void outsafe(stralloc *sa) +{ + int i; + char ch; + for (i = 0; i < sa->len; ++i) { + ch = sa->s[i]; + if (ch < 33) ch = '?'; + if (ch > 126) ch = '?'; + if (buffer_put(buffer_1small,&ch,1) == -1) _exit(111); + } +} + +void temp_noip() +{ + out("Zinvalid ipaddr in control/domainips (#4.3.0)\n"); + zerodie(); +} +void temp_nomem() +{ + out("ZOut of memory. (#4.3.0)\n"); + zerodie(); +} +void temp_oserr() +{ + out("ZSystem resources temporarily unavailable. (#4.3.0)\n"); + zerodie(); +} +void temp_osip() +{ + out("ZCan't bind to local ip address: "); + outsafe(&sendip); + out(". (#4.3.0)\n"); + zerodie(); +} +void temp_noconn() +{ + out("ZSorry, I wasn't able to establish an SMTP connection. (#4.4.1)\n"); + zerodie(); +} +void temp_dnscanon() +{ + out("ZCNAME lookup failed temporarily for: "); + outsafe(&canonhost); + out(". (#4.4.3)\n"); + zerodie(); +} +void temp_dns() +{ + out("ZSorry, I couldn't find any host named: "); + outsafe(&host); + out(". (#4.1.2)\n"); + zerodie(); +} +void temp_chdir() +{ + out("ZUnable to switch to home directory. (#4.3.0)\n"); + zerodie(); +} +void temp_control() +{ + out("ZUnable to read control files. (#4.3.0)\n"); + zerodie(); +} +void perm_usage() +{ + out("Dqmail-smtpam was invoked improperly. (#5.3.5)\n"); + zerodie(); +} +void perm_dns() +{ + out("DSorry, I couldn't find any host named: "); + outsafe(&host); + out(". (#5.1.2)\n"); + zerodie(); +} +void outhost() +{ + char ipaddr[IPFMT]; + int len; + + switch (partner.af) { + case AF_INET: + len = ip4_fmt(ipaddr,(char *)&partner.addr.ip4.d); break; + case AF_INET6: + len = ip6_fmt(ipaddr,(char *)&partner.addr.ip6.d); break; + } + if (buffer_put(buffer_1small,ipaddr,len) == -1) _exit(0); +} + +int flagcritical = 0; + +void dropped() +{ + out("ZConnected to "); + outhost(); + out(" but connection died. "); + if (flagcritical) out("Possible duplicate! "); + out("(#4.4.2)\n"); + zerodie(); +} + +int timeoutconnect = 60; +int smtpfd; +int timeout = 1200; + +ssize_t saferead(int fd,char *buf,int len) +{ + int r; + r = timeoutread(timeout,smtpfd,buf,len); + if (r <= 0) dropped(); + return r; +} + +ssize_t safewrite(int fd,char *buf,int len) +{ + int r; + r = timeoutwrite(timeout,smtpfd,buf,len); + if (r <= 0) dropped(); + return r; +} + +char outbuf[1450]; +buffer bo = BUFFER_INIT(safewrite,-1,outbuf,sizeof(outbuf)); +char frombuf[128]; +buffer bi = BUFFER_INIT(saferead,-1,frombuf,sizeof(frombuf)); + +stralloc smtptext = {0}; + +void get(char *ch) +{ + buffer_get(&bi,ch,1); + if (*ch != '\r') + if (smtptext.len < HUGESMTPTEXT) + if (!stralloc_append(&smtptext,ch)) temp_nomem(); +} + +unsigned long smtpcode() +{ + unsigned char ch; + unsigned long code; + + if (!stralloc_copys(&smtptext,"")) temp_nomem(); + + get(&ch); code = ch - '0'; + get(&ch); code = code * 10 + (ch - '0'); + get(&ch); code = code * 10 + (ch - '0'); + for (;;) { + get(&ch); + if (ch != '-') break; + while (ch != '\n') get(&ch); + get(&ch); + get(&ch); + get(&ch); + } + while (ch != '\n') get(&ch); + + return code; +} + +void outsmtptext() +{ + int i; + if (smtptext.s) if (smtptext.len) { + out("Remote host said: "); + for (i = 0; i < smtptext.len; ++i) + if (!smtptext.s[i]) smtptext.s[i] = '?'; + if (buffer_put(buffer_1small,smtptext.s,smtptext.len) == -1) _exit(111); + smtptext.len = 0; + } +} + +void quit(char *prepend,char *append) +{ + buffer_putsflush(&bo,"QUIT\r\n"); + /* waiting for remote side is just too ridiculous */ + out(prepend); + outhost(); + out(append); + out(".\n"); + outsmtptext(); + zerodie(); +} + +stralloc recip = {0}; + +/* this file is too long -------------------------------------- client TLS */ + +stralloc cafile = {0}; +stralloc cadir = {0}; +stralloc certfile = {0}; +stralloc keyfile = {0}; +stralloc keypwd = {0}; +stralloc ciphers = {0}; +stralloc tlsdest = {0}; + +char *tlsdestinfo = 0; +char *tlsdomaininfo = 0; + +stralloc domaincerts = {0}; +struct constmap mapdomaincerts; +stralloc tlsdestinations = {0}; +struct constmap maptlsdestinations; +unsigned long verifydepth = VERIFYDEPTH; + +void tls_init() +{ +/* Client CTX */ + + ctx = ssl_client(); + ssl_errstr(); + if (!ctx) temp_tlsctx(); + +/* Fetch CA infos for dest */ + + if (flagverify > 0) + if (cafile.len || cadir.len) + if (!ssl_ca(ctx,cafile.s,cadir.s,(int) verifydepth)) temp_tlsca(); + + if (ciphers.len) + if (!ssl_ciphers(ctx,ciphers.s)) temp_tlscipher(); + +/* Set SSL Context */ + + ssl = ssl_new(ctx,smtpfd); + if (!ssl) temp_tlsctx(); + +/* Setup SSL FDs */ + + if (!tls_conn(ssl,smtpfd)) temp_tlscon(); + +/* Go on in none-blocking mode */ + + if (tls_timeoutconn(timeout,smtpfd,smtpfd,ssl) <= 0) + temp_tlserr(); +} + +int starttls_peer() +{ + int i = 0; + + while ( (i += str_chr(smtptext.s + i,'\n') + 1) && + (i < smtptext.len - 8) ) { + if (!str_diffn(smtptext.s + i + 4,"STARTTLS",8)) return 1; } + + return 0; +} + +void tls_peercheck() +{ + X509 *cert; + + cert = SSL_get_peer_certificate(ssl); + if (!cert) { flagtls = 100; return; } + + if (flagverify < 0) { + if (cafile.len) case_lowerb(cafile.s,cafile.len); + switch (tls_fingerprint(cert,cafile.s + 1,cafile.len - 1)) { + case -1: temp_tlspeercert(); + case -2: temp_tlsdigest(); + case -3: temp_invaliddigest(); + case 1: temp_tlscertfp(); + } + } else { + switch (tls_checkpeer(ssl,cert,remotehost,flagtls,flagverify)) { + case -1: temp_tlspeercert(); + case -2: temp_tlspeerverify(); + case -3: temp_tlspeervalid(); + case 1: flagtls = 101; break; + case 2: flagtls = 102; break; + case 3: flagtls = 103; break; + } + } + + if (flagtls < 100) flagtls = 100; + + X509_free(cert); + + return; +} + +int utf8flag(unsigned char *ch,int len) +{ + int i = 0; + while (i < len) + if (ch[i++] > 127) return 1; + return 0; +} + +/* this file is too long -------------------------------------- SMTP connection */ + +unsigned long code; + +void smtp_greeting() +{ + buffer_puts(&bo,"EHLO "); + buffer_put(&bo,helohost.s,helohost.len); + buffer_puts(&bo,"\r\n"); + buffer_flush(&bo); + + if (smtpcode() != 250) { + buffer_puts(&bo,"HELO "); + buffer_put(&bo,helohost.s,helohost.len); + buffer_puts(&bo,"\r\n"); + buffer_flush(&bo); + code = smtpcode(); + if (code >= 500) quit("DConnected to"," but my name was rejected"); + if (code != 250) quit("ZConnected to"," but my name was rejected"); + } +} + +void smtp_starttls() +{ + buffer_puts(&bo,"STARTTLS\r\n"); + buffer_flush(&bo); + if (smtpcode() == 220) { + tls_init(); + tls_peercheck(); + smtp_greeting(); + } else { + flagtls = -2; + quit("ZConnected to"," but STARTTLS was rejected"); + } +} + +void smtp() +{ + + if (flagtls > 10 && flagtls < 20) { /* SMTPS */ + tls_init(); + tls_peercheck(); + } + + code = smtpcode(); + if (code >= 500) quit("DConnected to "," but sender was rejected"); + if (code >= 400) quit("ZConnected to "," but sender was probably greylisted"); + + smtp_greeting(); + + if (flagutf8mail) buffer_puts(&bo," SMTPUTF8"); + + if (flagtls > 0 && flagtls < 10) /* STARTTLS */ + if (starttls_peer()) { + smtp_starttls(); + } else if (flagtls > 2) { + temp_tlshost(); + } + + buffer_puts(&bo,"MAIL FROM:<>"); + if (flagutf8mail) + buffer_puts(&bo," SMTPUTF8"); + buffer_puts(&bo,"\r\n"); + buffer_flush(&bo); + code = smtpcode(); + if (code >= 500) quit("DConnected to "," but sender was rejected"); + if (code >= 400) quit("ZConnected to "," but sender was rejected"); + + buffer_puts(&bo,"RCPT TO:<"); + buffer_put(&bo,recipient.s,recipient.len); + buffer_puts(&bo,">\r\n"); + buffer_flush(&bo); + code = smtpcode(); + close(smtpfd); + if (code == 250) _exit(0); + _exit(1); +} + +void getcontrols() +{ + if (control_init() == -1) temp_control(); + if (control_readint(&timeout,"control/timeoutremote") == -1) temp_control(); + if (control_readint(&timeoutconnect,"control/timeoutconnect") == -1) + temp_control(); + if (control_rldef(&helohost,"control/helohost",1,(char *) 0) != 1) + temp_control(); + switch (control_readfile(&domainips,"control/domainips",0)) { + case -1: temp_control(); + case 0: if (!constmap_init(&mapdomainips,"",0,1)) temp_nomem(); break; + case 1: if (!constmap_init(&mapdomainips,domainips.s,domainips.len,1)) temp_nomem(); break; + } + switch (control_readfile(&tlsdestinations,"control/tlsdestinations",0)) { + case -1: temp_control(); + case 0: if (!constmap_init(&maptlsdestinations,"",0,1)) temp_nomem(); break; + case 1: if (!constmap_init(&maptlsdestinations,tlsdestinations.s,tlsdestinations.len,1)) temp_nomem(); break; + } + +} + +char up[513]; +int uplen; + +int main(int argc,char **argv) +{ + static ipalloc ip = {0}; + stralloc netif = {0}; + int i, j, k; + int r; /* reserved for return code */ + int p; /* reserved for port */ + char *localip = 0; + char *tlsdestinfo = 0; + + sig_pipeignore(); + if (argc < 2) perm_usage(); + if (chdir(auto_qmail) == -1) temp_chdir(); + getcontrols(); + + if (!stralloc_copys(&host,argv[1])) temp_nomem(); + + if (argv[2]) { + if (!stralloc_copys(&ports,argv[2])) temp_nomem(); + if (*ports.s == 's') { ports.s++; flagsmtps = 1; } + scan_ulong(ports.s,&port); + } + + if (ipme_init() != 1) temp_oserr(); + +/* this file is too long -------------------------------------- set domain ip + helohost */ + + if (!localip) + localip = constmap(&mapdomainips,"*",1); /* one for all */ + + if (localip) { + j = str_chr(localip,'%'); + if (localip[j] != '%') j = 0; + k = str_chr(localip,'|'); + if (localip[k] != '|') k = 0; + if (k) { /* helohost */ + if (!stralloc_copys(&helohost,localip + k + 1)) temp_nomem(); + localip[k] = 0; + } + if (j) { /* if index */ + localip[j] = 0; + if (!stralloc_copys(&netif,localip + j + 1)) temp_nomem(); + if (!stralloc_0(&netif)) temp_nomem(); + } + } + + +/* this file is too long -------------------------------------- TLS destinations */ + + flagtls = tls_destination((const stralloc) host); // un-terminated + + if (flagtls > 0) { + if (tlsdestinfo) { + i = str_chr(tlsdestinfo,'|'); /* ca file or cert fingerprint */ + if (tlsdestinfo[i] == '|') { + tlsdestinfo[i] = 0; + j = str_chr(tlsdestinfo+i+1,'|'); /* cipher */ + if (tlsdestinfo[i + j + 1] == '|') { + tlsdestinfo[i + j + 1] = 0; + k = str_chr(tlsdestinfo + i + j + 2,'|'); /* cone domain */ + if (tlsdestinfo[i + j + k + 2] == '|') { + tlsdestinfo[i + j + k + 2] = 0; + if (str_diffn(tlsdestinfo + j + k + 3,canonhost.s,canonhost.len)) flagtls = 0; + } + p = str_chr(tlsdestinfo + i + j + 2,';'); /* verifydepth;port */ + if (tlsdestinfo[i + j + p + 2] == ';') { + if (tlsdestinfo[i + j + p + 3] == 's') { flagsmtps = 1; p++; } + tlsdestinfo[i + j + p + 2] = 0; + if (p > 0) scan_ulong(tlsdestinfo+i+j + 2,&verifydepth); + scan_ulong(tlsdestinfo+i+j + p + 3,&port); + } + } + if (!stralloc_copys(&ciphers,tlsdestinfo + i + 1)) temp_nomem(); + } + if (!stralloc_copys(&cafile,tlsdestinfo)) temp_nomem(); + } + +/* cafile starts with '=' => it is a fingerprint + cafile ends with '/' => consider it as cadir */ + + if (cafile.len) { + flagverify = 1; + if (cafile.s[cafile.len] == '/') { + cafile.len = 0; + flagverify = 2; + if (!stralloc_copys(&cadir,tlsdestinfo)) temp_nomem(); + if (!stralloc_0(&cadir)) temp_nomem(); + } else { + if (cafile.s[0] == '%') flagverify = -1; + if (!stralloc_0(&cafile)) temp_nomem(); + } + } else { + cafile.len = cadir.len = ciphers.len = p = 0; + } + + if (port == PORT_SMTPS || flagsmtps) flagtls = flagtls + 10; + } + +/* this file is too long -------------------------------------- Setup connection */ + + uplen = 0; + for (;;) { + do + r = read(FDPAM,up + uplen,sizeof(up) - uplen); + while ((r == -1) && (errno == EINTR)); + if (r == -1) _exit(111); + if (r == 0) break; + uplen += r; + if (uplen >= sizeof(up)) _exit(111); + } + close(FDPAM); + + if (!stralloc_copyb(&recipient,up,uplen)) temp_nomem(); + if (!stralloc_0(&recipient)) temp_nomem(); + if (!stralloc_0(&host)) temp_nomem(); + if (!stralloc_copys(&remotehost,host.s)) temp_nomem(); + + flagutf8mail = utf8flag(recipient.s,recipient.len); + + switch (dns_ip(&ip,&remotehost)) { + case DNS_MEM: temp_nomem(); + case DNS_ERR: temp_dns(); + case DNS_COM: temp_dnscanon(); + default: if (ip.len <= 0) perm_dns(); + } + + smtpfd = socket(ip.ix[i].af,SOCK_STREAM,0); + if (smtpfd == -1) temp_oserr(); + + if (localip) { /* set domain ip */ + if (!stralloc_copyb(&sendip,localip,str_len(localip))) temp_nomem(); + j = str_chr(localip,':'); + if (j && localip[j] == ':') { /* IPv6 */ + if (!ip6_scan(localip,ip6)) temp_noip(); + ifidx = socket_getifidx(netif.s); + if (socket_bind6(smtpfd,ip6,0,ifidx) < 0) temp_osip(); + } else { /* IPv4 */ + if (!ip4_scan(localip,ip4)) temp_noip(); + if (socket_bind4(smtpfd,ip4,0) < 0) temp_osip(); + } + } + + r = timeoutconn(smtpfd,&ip.ix[i].addr,(unsigned int) port,timeoutconnect,ifidx); + if (r == 0) { + tcpto_err(&ip.ix[i],0); + partner = ip.ix[i]; + smtp(); /* does not return */ + } + tcpto_err(&ip.ix[i],errno == ETIMEDOUT); + close(smtpfd); + + temp_noconn(); +} |