diff --git Makefile Makefile index 0f0e31a..245a3fc 100644 --- Makefile +++ Makefile @@ -4,6 +4,251 @@ SHELL=/bin/sh default: it +ACCEPTUTILS_BIN=authup checknotroot \ + fixsmtpio qmail-qfilter-addtlsheader +ACCEPTUTILS_MAN=authup.8 checknotroot.8 \ + fixsmtpio.8 qmail-qfilter-addtlsheader.8 + +acceptutils: \ +${ACCEPTUTILS_BIN} ${ACCEPTUTILS_MAN} + +acceptutils_base64.o: \ +compile99 acceptutils_base64.c acceptutils_base64.h + ./compile99 acceptutils_base64.c + +acceptutils_pfilter.o: \ +compile99 acceptutils_pfilter.c acceptutils_pfilter.h hasblacklist.h + ./compile99 acceptutils_pfilter.c + +acceptutils_stralloc.o: \ +compile99 acceptutils_stralloc.c acceptutils_stralloc.h + ./compile99 acceptutils_stralloc.c + +acceptutils_ucspitls.o: \ +compile99 acceptutils_ucspitls.c acceptutils_ucspitls.h acceptutils_stralloc.h \ +case.h env.h fd.h readwrite.h scan.h substdio.h + ./compile99 acceptutils_ucspitls.c + +acceptutils_unistd.o: \ +compile99 acceptutils_unistd.c acceptutils_unistd.h + ./compile99 acceptutils_unistd.c + +acceptutils-install: \ +acceptutils + cp ${ACCEPTUTILS_BIN} `head -1 conf-qmail`/bin && \ + cp ${ACCEPTUTILS_MAN} `head -1 conf-qmail`/man/man8 + +acceptutils-memcheck: \ +test_fixsmtpio + valgrind --track-origins=yes --leak-check=full --error-exitcode=99 ./test_fixsmtpio >/dev/null + #valgrind --track-origins=yes --leak-check=full --error-exitcode=88 ./fixsmtpio echo hi >/dev/null + +acceptutils-tests: \ +test_fixsmtpio + +acceptutils-tests-run: \ +acceptutils-tests + @prove -v -e '' ./test_fixsmtpio | grep -v '^ok' + +authup: \ +load authup.o auto_qmail.o commands.o control.o timeoutread.o timeoutwrite.o \ +now.o case.a env.a fd.a getln.a open.a sig.a wait.a stralloc.a alloc.a \ +getopt.a substdio.a error.a str.a fs.a \ +acceptutils_base64.o acceptutils_pfilter.o acceptutils_stralloc.o \ +acceptutils_unistd.o acceptutils_ucspitls.o \ +socket.lib blacklist.lib + ./load authup auto_qmail.o acceptutils_base64.o acceptutils_pfilter.o \ + acceptutils_stralloc.o acceptutils_unistd.o acceptutils_ucspitls.o \ + commands.o control.o timeoutread.o timeoutwrite.o now.o \ + case.a env.a fd.a getln.a open.a sig.a wait.a stralloc.a alloc.a \ + getopt.a substdio.a error.a str.a fs.a \ + `cat socket.lib` `cat blacklist.lib` + +authup.o: \ +compile99 authup.c auto_qmail.h commands.h sig.h substdio.h wait.h \ +str.h byte.h now.h fmt.h scan.h readwrite.h timeoutread.h timeoutwrite.h \ +case.h env.h control.h error.h sgetopt.h \ +acceptutils_base64.h acceptutils_pfilter.h acceptutils_stralloc.h \ +acceptutils_ucspitls.h acceptutils_unistd.h + ./compile99 authup.c + +blacklist.lib: \ +tryblist.c compile load + ( ( ./compile tryblist.c && ./load tryblist -lblacklist ) >/dev/null \ + 2>&1 \ + && echo || exit 0 ) > blacklist.lib + rm -f tryblist.o tryblist + +check.h: \ +conf-check check_stdint.h + cat `head -1 conf-check`/include/check.h \ + | sed 's}}"check_stdint.h"}g' \ + > check.h + +check_stdint.h: \ +conf-check + cp `head -1 conf-check`/include/check_stdint.h . + +checknotroot: \ +load checknotroot.o acceptutils_unistd.o substdio.a error.a str.a + ./load checknotroot acceptutils_unistd.o substdio.a error.a str.a + +checknotroot.o: \ +compile99 checknotroot.c exit.h readwrite.h substdio.h acceptutils_unistd.h + ./compile99 checknotroot.c + +fixsmtpio: \ +load fixsmtpio.o fixsmtpio_control.o fixsmtpio_die.o fixsmtpio_filter.o \ +fixsmtpio_eventq.o fixsmtpio_readwrite.o fixsmtpio_munge.o fixsmtpio_glob.o \ +fixsmtpio_proxy.o acceptutils_ucspitls.o auto_qmail.o control.o \ +acceptutils_unistd.o acceptutils_stralloc.o \ +getln.a substdio.a stralloc.a env.a str.a error.a fd.a sig.a \ +alloc.a wait.a case.a open.a fs.a + ./load fixsmtpio fixsmtpio_control.o fixsmtpio_die.o fixsmtpio_filter.o \ + fixsmtpio_eventq.o fixsmtpio_readwrite.o fixsmtpio_munge.o fixsmtpio_glob.o \ + fixsmtpio_proxy.o acceptutils_ucspitls.o auto_qmail.o control.o \ + acceptutils_unistd.o acceptutils_stralloc.o \ + getln.a substdio.a stralloc.a env.a str.a error.a fd.a sig.a \ + alloc.a wait.a case.a open.a fs.a + +fixsmtpio.o: \ +compile99 fixsmtpio.c fixsmtpio.h fixsmtpio_die.h fixsmtpio_filter.h \ +fixsmtpio_proxy.h alloc.h auto_qmail.h case.h control.h env.h \ +fd.h scan.h str.h substdio.h wait.h acceptutils_stralloc.h acceptutils_unistd.h + ./compile99 fixsmtpio.c + +fixsmtpio_die.o: \ +compile99 fixsmtpio_die.c fixsmtpio.h fixsmtpio_die.h readwrite.h \ +acceptutils_stralloc.h acceptutils_unistd.h + ./compile99 fixsmtpio_die.c + +test_acceptutils_stralloc.o: \ +compile99 acceptutils_stralloc.c fixsmtpio.h acceptutils_stralloc.h readwrite.h \ +check.h \ +test_acceptutils_stralloc.c + ./compile99 test_acceptutils_stralloc.c + +fixsmtpio_control.o: \ +compile99 fixsmtpio_control.c fixsmtpio.h fixsmtpio_control.h \ +acceptutils_stralloc.h + ./compile99 fixsmtpio_control.c + +test_fixsmtpio_control.o: \ +compile99 fixsmtpio_control.c fixsmtpio.h fixsmtpio_control.h \ +check.h \ +test_fixsmtpio_control.c + ./compile99 test_fixsmtpio_control.c + +fixsmtpio_eventq.o: \ +compile99 fixsmtpio_eventq.c alloc.h str.h fixsmtpio.h fixsmtpio_die.h fixsmtpio_eventq.h + ./compile99 fixsmtpio_eventq.c + +test_fixsmtpio_eventq.o: \ +compile99 fixsmtpio_eventq.c alloc.h str.h fixsmtpio.h fixsmtpio_die.h fixsmtpio_eventq.h \ +check.h \ +test_fixsmtpio_eventq.c + ./compile99 test_fixsmtpio_eventq.c + +fixsmtpio_filter.o: \ +compile99 fixsmtpio_filter.c fixsmtpio_filter.h fixsmtpio_die.h fixsmtpio_munge.h fixsmtpio_glob.h \ +acceptutils_stralloc.h + ./compile99 fixsmtpio_filter.c + +test_fixsmtpio_filter.o: \ +compile99 fixsmtpio_filter.c fixsmtpio_filter.h fixsmtpio_die.h fixsmtpio_munge.h fixsmtpio_glob.h \ +check.h \ +test_fixsmtpio_filter.c + ./compile99 test_fixsmtpio_filter.c + +fixsmtpio_glob.o: \ +compile99 fixsmtpio_glob.c fixsmtpio_glob.h + ./compile99 fixsmtpio_glob.c + +test_fixsmtpio_glob.o: \ +compile99 fixsmtpio_glob.c fixsmtpio_glob.h \ +check.h \ +test_fixsmtpio_glob.c + ./compile99 test_fixsmtpio_glob.c + +fixsmtpio_munge.o: \ +compile99 fixsmtpio_munge.c fixsmtpio_munge.h fixsmtpio_die.h \ +acceptutils_stralloc.h + ./compile99 fixsmtpio_munge.c + +test_fixsmtpio_munge.o: \ +compile99 fixsmtpio_munge.c fixsmtpio_munge.h fixsmtpio_die.h \ +check.h \ +test_fixsmtpio_munge.c + ./compile99 test_fixsmtpio_munge.c + +fixsmtpio_proxy.o: \ +compile99 fixsmtpio_proxy.c fixsmtpio_proxy.h fixsmtpio_readwrite.h \ +fixsmtpio_die.h fixsmtpio_eventq.h fixsmtpio_filter.h fixsmtpio_die.h \ +acceptutils_stralloc.h acceptutils_unistd.h fmt.h + ./compile99 fixsmtpio_proxy.c + +test_fixsmtpio_proxy.o: \ +compile99 fixsmtpio_proxy.c fixsmtpio_proxy.h fixsmtpio_readwrite.h \ +fixsmtpio_die.h fixsmtpio_eventq.h fixsmtpio_filter.h \ +acceptutils_stralloc.h \ +check.h \ +test_fixsmtpio_proxy.c + ./compile99 test_fixsmtpio_proxy.c + +fixsmtpio_readwrite.o: \ +compile99 fixsmtpio_readwrite.c fixsmtpio_readwrite.h fixsmtpio_die.h error.h readwrite.h select.h \ +acceptutils_stralloc.h + ./compile99 fixsmtpio_readwrite.c + +test_fixsmtpio: \ +fixsmtpio \ +load test_fixsmtpio.o fixsmtpio_control.o fixsmtpio_die.o acceptutils_unistd.o \ +fixsmtpio_eventq.o fixsmtpio_readwrite.o fixsmtpio_munge.o fixsmtpio_glob.o \ +fixsmtpio_filter.o fixsmtpio_proxy.o acceptutils_stralloc.o acceptutils_ucspitls.o \ +test_fixsmtpio_control.o \ +test_acceptutils_stralloc.o test_fixsmtpio_eventq.o test_fixsmtpio_filter.o \ +test_fixsmtpio_glob.o test_fixsmtpio_munge.o test_fixsmtpio_proxy.o \ +auto_qmail.o control.o getln.a \ +substdio.a stralloc.a env.a str.a error.a fd.a sig.a alloc.a wait.a \ +case.a open.a fs.a \ +libcheck.a rt.lib + ./load test_fixsmtpio fixsmtpio_control.o fixsmtpio_die.o acceptutils_unistd.o \ + fixsmtpio_eventq.o fixsmtpio_readwrite.o fixsmtpio_munge.o fixsmtpio_glob.o \ + fixsmtpio_filter.o fixsmtpio_proxy.o acceptutils_stralloc.o acceptutils_ucspitls.o \ + test_fixsmtpio_control.o \ + test_acceptutils_stralloc.o test_fixsmtpio_eventq.o test_fixsmtpio_filter.o \ + test_fixsmtpio_glob.o test_fixsmtpio_munge.o test_fixsmtpio_proxy.o \ + auto_qmail.o control.o getln.a \ + substdio.a stralloc.a env.a str.a error.a fd.a sig.a alloc.a wait.a \ + case.a open.a fs.a \ + libcheck.a -lpthread -lm `cat rt.lib` + +test_fixsmtpio.o: \ +compile99 test_fixsmtpio.c fixsmtpio.h check.h + ./compile99 test_fixsmtpio.c + +libcheck.a: \ +conf-check + cp `head -1 conf-check`/lib/libcheck.a . + +qmail-qfilter-addtlsheader: \ +load qmail-qfilter-addtlsheader.o date822fmt.o now.o \ +datetime.a substdio.a env.a error.a str.a fs.a + ./load qmail-qfilter-addtlsheader date822fmt.o now.o \ + datetime.a substdio.a env.a error.a str.a fs.a + +qmail-qfilter-addtlsheader.o: \ +compile99 qmail-qfilter-addtlsheader.c datetime.h date822fmt.h env.h \ +now.h readwrite.h substdio.h + ./compile99 qmail-qfilter-addtlsheader.c + +rt.lib: \ +compile load + ( ( echo 'main() { ; }' > tryrt.c && ./compile tryrt.c && \ + ./load tryrt -lrt ) >/dev/null 2>&1 \ + && echo -lrt || exit 0 ) > rt.lib + rm -f tryrt.c tryrt.o tryrt + addresses.0: \ addresses.5 nroff -man addresses.5 > addresses.0 @@ -303,6 +548,7 @@ exit.h auto_spawn.h clean: \ TARGETS rm -f `cat TARGETS` + git checkout -- INSTALL SENDMAIL coe.o: \ compile coe.c coe.h @@ -319,6 +565,12 @@ make-compile warn-auto.sh systype compile chmod 755 compile +compile99: \ +make-compile99 warn-auto.sh systype + ( cat warn-auto.sh; ./make-compile99 "`cat systype`" ) > \ + compile99 + chmod 755 compile99 + condredirect: \ load condredirect.o qmail.o strerr.a fd.a sig.a wait.a seek.a env.a \ substdio.a error.a str.a fs.a auto_qmail.o @@ -633,6 +885,13 @@ gfrom.o: \ compile gfrom.c str.h gfrom.h ./compile gfrom.c +hasblacklist.h: \ +tryblist.c compile load + ( ( ./compile tryblist.c && ./load tryblist -lblacklist ) >/dev/null \ + 2>&1 \ + && echo || exit 0 ) > hasblacklist.h + rm -f tryblist.o tryblist + hasflock.h: \ tryflock.c compile load ( ( ./compile tryflock.c && ./load tryflock ) >/dev/null \ @@ -908,6 +1167,11 @@ make-compile.sh auto-ccld.sh cat auto-ccld.sh make-compile.sh > make-compile chmod 755 make-compile +make-compile99: \ +make-compile99.sh auto-ccld.sh + cat auto-ccld.sh make-compile99.sh > make-compile99 + chmod 755 make-compile99 + make-load: \ make-load.sh auto-ccld.sh cat auto-ccld.sh make-load.sh > make-load diff --git TARGETS TARGETS index facdad7..8e50f83 100644 --- TARGETS +++ TARGETS @@ -385,3 +385,41 @@ forgeries.0 man setup check +make-compile99 +compile99 +acceptutils_base64.o +acceptutils_pfilter.o +acceptutils_stralloc.o +acceptutils_ucspitls.o +acceptutils_unistd.o +authup +authup.o +checknotroot +checknotroot.o +fixsmtpio +fixsmtpio.o +test_fixsmtpio +test_fixsmtpio.o +fixsmtpio_filter.o +fixsmtpio_proxy.o +fixsmtpio_die.o +fixsmtpio_eventq.o +fixsmtpio_readwrite.o +fixsmtpio_munge.o +fixsmtpio_glob.o +fixsmtpio_control.o +check.h +check_stdint.h +libcheck.a +rt.lib +hasblacklist.h +blacklist.lib +test_acceptutils_stralloc.o +test_fixsmtpio_control.o +test_fixsmtpio_eventq.o +test_fixsmtpio_filter.o +test_fixsmtpio_glob.o +test_fixsmtpio_munge.o +test_fixsmtpio_proxy.o +qmail-qfilter-addtlsheader +qmail-qfilter-addtlsheader.o diff --git acceptutils_base64.c acceptutils_base64.c new file mode 100644 index 0000000..8b9d622 --- /dev/null +++ acceptutils_base64.c @@ -0,0 +1,119 @@ +#include "acceptutils_base64.h" +#include "stralloc.h" +#include "substdio.h" +#include "str.h" + +static char *b64alpha = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; +#define B64PAD '=' + +/* returns 0 ok, 1 illegal, -1 problem */ + +int b64decode(const unsigned char *in,int l,stralloc *out) +/* stralloc *out => not null terminated */ +{ + int p = 0; + int n; + unsigned int x; + int i, j; + char *s; + unsigned char b[3]; + + if (l == 0) { + if (!stralloc_copys(out,"")) return -1; + return 0; + } + + while (in[l-1] == B64PAD) { + p ++; + l--; + } + + n = (l + p) / 4; + i = (n * 3) - p; + if (!stralloc_ready(out,i)) return -1; + out->len = i; + s = out->s; + + for (i = 0; i < n - 1; i++) { + x = 0; + for (j = 0; j < 4; j++) { + if (in[j] >= 'A' && in[j] <= 'Z') + x = (x << 6) + (unsigned int)(in[j] - 'A' + 0); + else if (in[j] >= 'a' && in[j] <= 'z') + x = (x << 6) + (unsigned int)(in[j] - 'a' + 26); + else if (in[j] >= '0' && in[j] <= '9') + x = (x << 6) + (unsigned int)(in[j] - '0' + 52); + else if (in[j] == '+') + x = (x << 6) + 62; + else if (in[j] == '/') + x = (x << 6) + 63; + else if (in[j] == '=') + x = (x << 6); + } + + s[2] = (unsigned char)(x & 255); x >>= 8; + s[1] = (unsigned char)(x & 255); x >>= 8; + s[0] = (unsigned char)(x & 255); x >>= 8; + s += 3; in += 4; + } + + x = 0; + for (j = 0; j < 4; j++) { + if (in[j] >= 'A' && in[j] <= 'Z') + x = (x << 6) + (unsigned int)(in[j] - 'A' + 0); + else if (in[j] >= 'a' && in[j] <= 'z') + x = (x << 6) + (unsigned int)(in[j] - 'a' + 26); + else if (in[j] >= '0' && in[j] <= '9') + x = (x << 6) + (unsigned int)(in[j] - '0' + 52); + else if (in[j] == '+') + x = (x << 6) + 62; + else if (in[j] == '/') + x = (x << 6) + 63; + else if (in[j] == '=') + x = (x << 6); + } + + b[2] = (unsigned char)(x & 255); x >>= 8; + b[1] = (unsigned char)(x & 255); x >>= 8; + b[0] = (unsigned char)(x & 255); x >>= 8; + + for (i = 0; i < 3 - p; i++) + s[i] = b[i]; + + return 0; +} + +int b64encode(stralloc *in,stralloc *out) +{ + unsigned char a, b, c; + int i; + char *s; + + if (in->len == 0) + { + if (!stralloc_copys(out,"")) return -1; + return 0; + } + + i = in->len / 3 * 4 + 4; + if (!stralloc_ready(out,i)) return -1; + s = out->s; + + for (i = 0; i < in->len; i += 3) { + a = in->s[i]; + b = i + 1 < in->len ? in->s[i + 1] : 0; + c = i + 2 < in->len ? in->s[i + 2] : 0; + + *s++ = b64alpha[a >> 2]; + *s++ = b64alpha[((a & 3 ) << 4) | (b >> 4)]; + + if (i + 1 >= in->len) *s++ = B64PAD; + else *s++ = b64alpha[((b & 15) << 2) | (c >> 6)]; + + if (i + 2 >= in->len) *s++ = B64PAD; + else *s++ = b64alpha[c & 63]; + } + out->len = s - out->s; + return 0; +} diff --git acceptutils_base64.h acceptutils_base64.h new file mode 100644 index 0000000..4d6ab92 --- /dev/null +++ acceptutils_base64.h @@ -0,0 +1,9 @@ +#ifndef BASE64_H +#define BASE64_H + +#include "stralloc.h" + +int b64decode(const unsigned char *,int,stralloc *); +int b64encode(stralloc *,stralloc *); + +#endif diff --git acceptutils_pfilter.c acceptutils_pfilter.c new file mode 100644 index 0000000..9ffab18 --- /dev/null +++ acceptutils_pfilter.c @@ -0,0 +1,63 @@ +#include "hasblacklist.h" + +#if HASBLACKLIST + +#include +#include +#include +#include +#include +#include + +#define GEN_FILL_SOCKADDR(funcname,family,structname,familyfield,portfield,addrfield) \ +static int funcname(const struct sockaddr_storage *ss,socklen_t *slen,char *ip,char *port) { \ + struct structname *sock = (struct structname *)ss; \ + sock->familyfield = family; \ + sock->portfield = htons(atoi(port)); \ + *slen = sizeof(*sock); \ + return inet_pton(sock->familyfield, ip, &sock->addrfield); \ +} + +GEN_FILL_SOCKADDR(ip6,AF_INET6,sockaddr_in6,sin6_family,sin6_port,sin6_addr); +GEN_FILL_SOCKADDR(ip4,AF_INET,sockaddr_in,sin_family,sin_port,sin_addr); + +static void fill_sockaddr_ip6(const struct sockaddr_storage *ss,socklen_t *slen) { + char *ip = getenv("TCP6REMOTEIP"); char *port = getenv("TCP6LOCALPORT"); + if (!ip || !port || 0 == ip6(ss,slen,ip,port)) + (void)ip6(ss,slen,getenv("TCPREMOTEIP"),getenv("TCPLOCALPORT")); +} + +static void fill_sockaddr_maybe_ip4(const struct sockaddr_storage *ss,socklen_t *slen) { + char *ip = getenv("TCPREMOTEIP"); char *port = getenv("TCPLOCALPORT"); + if (!ip || !port) return; + if (0 == ip6(ss,slen,ip,port)) + (void)ip4(ss,slen,ip,port); +} + +static void fill_sockaddr_info(const struct sockaddr_storage *ss,socklen_t *slen) { + char *proto = getenv("PROTO"); + memset((void *)ss, 0, *slen); + if (proto && 0 == strcmp(proto,"TCP6")) + fill_sockaddr_ip6(ss,slen); + else + fill_sockaddr_maybe_ip4(ss,slen); +} + +void pfilter_notify(int action,int fd,const char *msg,const char *pidstr) { + const struct sockaddr_storage ss; + socklen_t slen = sizeof(ss); + + fill_sockaddr_info(&ss,&slen); + if (0 == blacklist_sa(action, fd, (void *)&ss, slen, msg)) + fprintf(stderr,"%s %s blacklist_sa(%d, %d...)\n", msg, pidstr, action, fd); + else + fprintf(stderr,"%s %s blacklist_sa(%d, %d...) failed with errno %d\n", msg, pidstr, action, fd, errno); +} + +#else + +void pfilter_notify(int action,int fd,const char *msg) { + ; +} + +#endif diff --git acceptutils_pfilter.h acceptutils_pfilter.h new file mode 100644 index 0000000..ba12c8d --- /dev/null +++ acceptutils_pfilter.h @@ -0,0 +1 @@ +void pfilter_notify(int,int,const char *,const char *); diff --git acceptutils_stralloc.c acceptutils_stralloc.c new file mode 100644 index 0000000..947e40c --- /dev/null +++ acceptutils_stralloc.c @@ -0,0 +1,44 @@ +#include "acceptutils_stralloc.h" + +static void (*die_sa)(const char *,const char *); + +void stralloc_set_die(void (*die_nomem)(const char *,const char *)) { + die_sa = die_nomem; +} + +void contextlogging_append(const char *caller,stralloc *to,char *from) { + if (!stralloc_append(to,from)) die_sa(caller,__func__); +} +void contextlogging_append0(const char *caller,stralloc *to) { + if (!stralloc_0(to)) die_sa(caller,__func__); +} +void contextlogging_cat(const char *caller,stralloc *to,stralloc *from) { + if (!stralloc_cat(to,from)) die_sa(caller,__func__); +} +void contextlogging_catb(const char *caller,stralloc *to,char *buf,int len) { + if (!stralloc_catb(to,buf,len)) die_sa(caller,__func__); +} +void contextlogging_cats(const char *caller,stralloc *to,char *from) { + if (!stralloc_cats(to,from)) die_sa(caller,__func__); +} +void contextlogging_copy(const char *caller,stralloc *to,stralloc *from) { + if (!stralloc_copy(to,from)) die_sa(caller,__func__); +} +void contextlogging_copyb(const char *caller,stralloc *to,char *buf,int len) { + if (!stralloc_copyb(to,buf,len)) die_sa(caller,__func__); +} +void contextlogging_copys(const char *caller,stralloc *to,char *from) { + if (!stralloc_copys(to,from)) die_sa(caller,__func__); +} + +void prepends(stralloc *to,char *from) { + stralloc tmp = {0}; + copy(&tmp,to); + copys(to,(char *)from); + cat(to,&tmp); +} +int starts(stralloc *haystack,char *needle) { return stralloc_starts(haystack,needle); } +int ends_with_newline(stralloc *sa) { + return sa->len > 0 && sa->s[sa->len - 1] == '\n'; +} +void blank(stralloc *sa) { copys(sa,""); } diff --git acceptutils_stralloc.h acceptutils_stralloc.h new file mode 100644 index 0000000..6dbb1bc --- /dev/null +++ acceptutils_stralloc.h @@ -0,0 +1,25 @@ +#include "stralloc.h" + +void stralloc_set_die(void (*)(const char *,const char *)); + +void contextlogging_append(const char *,stralloc *,char *); +#define append(a,b) contextlogging_append(__func__,a,b) +void contextlogging_append0(const char *,stralloc *); +#define append0(a) contextlogging_append0(__func__,a) +void contextlogging_cat(const char *,stralloc *,stralloc *); +#define cat(a,b) contextlogging_cat(__func__,a,b) +void contextlogging_catb(const char *,stralloc *,char *,int); +#define catb(a,b,c) contextlogging_catb(__func__,a,b,c) +void contextlogging_cats(const char *,stralloc *,char *); +#define cats(a,b) contextlogging_cats(__func__,a,b) +void contextlogging_copy(const char *,stralloc *,stralloc *); +#define copy(a,b) contextlogging_copy(__func__,a,b) +void contextlogging_copyb(const char *,stralloc *,char *,int); +#define copyb(a,b,c) contextlogging_copyb(__func__,a,b,c) +void contextlogging_copys(const char *,stralloc *,char *); +#define copys(a,b) contextlogging_copys(__func__,a,b) + +void prepends(stralloc *,char *); +int starts(stralloc *,char *); +int ends_with_newline(stralloc *); +void blank(stralloc *); diff --git acceptutils_ucspitls.c acceptutils_ucspitls.c new file mode 100644 index 0000000..1551143 --- /dev/null +++ acceptutils_ucspitls.c @@ -0,0 +1,111 @@ +#include "case.h" +#include "env.h" +#include "fd.h" +#include "readwrite.h" +#include "scan.h" +#include "substdio.h" + +#include "acceptutils_stralloc.h" +#include "acceptutils_ucspitls.h" + +int ucspitls_level_configured(void) { + char *ucspitls = env_get("UCSPITLS"); + char *disabletls = env_get("DISABLETLS"); + env_unset("UCSPITLS"); + env_unset("DISABLETLS"); + if (disabletls || !ucspitls || !case_diffs(ucspitls,"-")) + return UCSPITLS_UNAVAILABLE; + if (!case_diffs(ucspitls,"!")) return UCSPITLS_REQUIRED; + return UCSPITLS_AVAILABLE; +} + +static int get_fd_for(char *name) { + unsigned long fd; + char *fdstr; + + if (!(fdstr = env_get(name))) return 0; + if (!scan_ulong(fdstr,&fd)) return 0; + + return (int)fd; +} + +static int notify_control_socket() { + unsigned int fd = get_fd_for("SSLCTLFD"); + + if (!fd) return 0; + if (write(fd, "Y", 1) < 1) return 0; + + return 1; +} + +static int adjust_read_fd() { + unsigned int fd = get_fd_for("SSLREADFD"); + + if (!fd) return 0; + if (fd_move(0,fd) == -1) return 0; + + return 1; +} + +static int adjust_write_fd() { + unsigned int fd = get_fd_for("SSLWRITEFD"); + + if (!fd) return 0; + if (fd_move(1,fd) == -1) return 0; + + return 1; +} + +int tls_init(void) { + return notify_control_socket() && adjust_read_fd() && adjust_write_fd(); +} + +int tls_info(void (*die_nomem)(const char *caller,const char *alloc_fn)) { + unsigned long fd; + char envbuf[SUBSTDIO_INSIZE]; + char *x; + int j; + + stralloc ssl_env = {0}; + stralloc ssl_parm = {0}; + stralloc ssl_value = {0}; + + fd = get_fd_for("SSLCTLFD"); + if (!fd) return 0; + + while ((j = read(fd,envbuf,SUBSTDIO_INSIZE)) > 0) { + catb(&ssl_env,envbuf,j); + if (ssl_env.len >= 2 && ssl_env.s[ssl_env.len-2] == 0 && ssl_env.s[ssl_env.len-1] == 0) + break; + } + if (j < 0) die_nomem(__func__,"read"); + if (ssl_env.len == 0) return 0; + + x = ssl_env.s; + + for (j=0;j < ssl_env.len-1;++j) { + if ( *x != '=' ) { + catb(&ssl_parm,x,1); + x++; + } else { + append0(&ssl_parm); + x++; + + for (;j < ssl_env.len-j-1;++j) { + if ( *x != '\0' ) { + catb(&ssl_value,x,1); + x++; + } else { + append0(&ssl_value); + x++; + if (!env_put2(ssl_parm.s,ssl_value.s)) + die_nomem(__func__,"env_put2"); + ssl_parm.len = 0; + ssl_value.len = 0; + break; + } + } + } + } + return j; +} diff --git acceptutils_ucspitls.h acceptutils_ucspitls.h new file mode 100644 index 0000000..e7384f1 --- /dev/null +++ acceptutils_ucspitls.h @@ -0,0 +1,7 @@ +#define UCSPITLS_UNAVAILABLE 0 +#define UCSPITLS_AVAILABLE 1 +#define UCSPITLS_REQUIRED 2 + +int ucspitls_level_configured(void); +int tls_init(void); +int tls_info(void (*)(const char *,const char *)); diff --git acceptutils_unistd.c acceptutils_unistd.c new file mode 100644 index 0000000..40315b0 --- /dev/null +++ acceptutils_unistd.c @@ -0,0 +1,19 @@ +#include + +int unistd_chdir(const char *path) { return chdir(path); } + +int unistd_close(int fildes) { return close(fildes); } + +int unistd_execvp(const char *file, char *const argv[]) { + return execvp(file, argv); +} + +void unistd_exit(int status) { return _exit(status); } + +int unistd_fork(void) { return fork(); } + +int unistd_getpid(void) { return getpid(); } + +int unistd_getuid(void) { return getuid(); } + +int unistd_pipe(int fildes[2]) { return pipe(fildes); } diff --git acceptutils_unistd.h acceptutils_unistd.h new file mode 100644 index 0000000..067b964 --- /dev/null +++ acceptutils_unistd.h @@ -0,0 +1,8 @@ +int unistd_chdir(const char *); +int unistd_close(int); +int unistd_execvp(const char *,char *const[]); +void unistd_exit(int); +int unistd_fork(void); +int unistd_getpid(void); +int unistd_getuid(void); +int unistd_pipe(int [2]); diff --git authup.8 authup.8 new file mode 100644 index 0000000..134f7d8 --- /dev/null +++ authup.8 @@ -0,0 +1,165 @@ +.TH AUTHUP 8 2020-12-11 +.SH NAME +authup \- offer SMTP or POP3 authentication +.SH SYNOPSIS +.B authup +[ +.B \-t \fItries +] +.I protocol +.I checkpassword +.I prog +.SH DESCRIPTION +.B authup +speaks just enough SMTP (AUTH LOGIN or PLAIN) +or POP3 (USER-PASS) +to parse a username and password and pass them to +.IR checkpassword . +.B authup +is most commonly invoked as root so +.I checkpassword +can change to the UID of the authenticated user before running +.IR prog . +.P +Common combinations of +.I protocol +and +.IR prog : +.TP 5 +.B smtp +and +.B "checknotroot fixsmtpio ofmipd" +.TP 5 +.B pop3 +and +.B "checknotroot qmail-pop3d" +.P +.B authup +waits for +.I checkpassword +and +.I prog +to finish, and prints an error message if either of them crashes or exits nonzero. +.SH "OPTIONS" +.TP +.B \-t \fItries +Disconnect after +.I tries +failed authentication attempts. +Default: 5 for SMTP, 1 for POP3. +.SH "ENVIRONMENT VARIABLES" +When running under +.B "sslserver -n" +or +.BR "s6-ucspitlsd" , +.B authup +can offer TLS. +Set +.B UCSPITLS +to the special value +.B ! +to require that clients negotiate TLS before authenticating. +.P +If +.B DISABLETLS +is set, the presence and value of +.B UCSPITLS +will be ignored. +.P +If +.B AUTHUP_SASL_BROKEN_CLIENTS +is set, +.B authup +will additionally advertise AUTH support in a second, non-standard way +to interoperate with Outlook Express 4, Exchange 5, +and other SMTP clients that implement an obsolete version of the AUTH command. +.P +If authentication succeeds, +.IR prog 's +environment will contain the username in +.BR AUTHUP_USER . +.P +Since +.I prog +will run with the privileges of the authenticated user, so will +any +.B qmail-queue +wrapper configured as +.BR QMAILQUEUE . +This can be useful for user-controlled filtering. +.SH "CONTROL FILES" +.TP 5 +.I smtpgreeting / pop3greeting +SMTP and POP3 greeting messages. +Default: +.IR me , +if that is supplied; +otherwise +.B authup +will refuse to run. +The first word of the greeting +should be the current host's name. +.TP 5 +.I smtpcapabilities / pop3capabilities +SMTP and POP3 capabilities (one per line) to advertise in +.I EHLO +and +.IR CAPA , +respectively. +Default: +.IR none . +Without the needed file, +.B authup +will refuse to run. + +Typical POP3 capabilities: TOP, UIDL. (Omit STLS and USER.) + +Typical SMTP capabilities: PIPELINING, 8BITMIME. (Omit STARTTLS and AUTH.) + +Correct values for your system depend on what +.I prog +offers. +For instance, to inspect your installed +.BR qmail-smtpd : + +$ echo EHLO | qmail-smtpd \\ + | sed -e '1,2d' -e 's|^....||' | egrep -v '^(STARTTLS|AUTH)' + +.B authup +will automatically advertise TLS support when run under +.B "sslserver -n" +or +.BR "s6-ucspitlsd" . +.TP 5 +.I timeoutsmtpd / timeoutpop3d +Number of seconds +.B authup +will wait for each new buffer of data from the remote SMTP or POP3 client. +Default: 1200. +.SH "COMPATIBILITY" +While +.B CRAM-MD5 +is available in most SMTP AUTH patches, and +.B APOP +is available in +.BR qmail-popup , +neither is currently supported by +.BR authup . +Nor is authenticating with a client certificate. +If you rely on any of these, please share your use case with the author. +.SH "EXAMPLES" +See +.IR https://schmonz.com/qmail/acceptutils . +.SH "AUTHOR" +.B Amitai Schleier +.SH "SEE ALSO" +checknotroot(8), +fixsmtpio(8), +sslserver(1), +s6-ucspitlsd, +ucspi-tls(2), +qmail-qfilter-queue(8), +ofmipd(8), +qmail-smtpd(8), +qmail-pop3d(8), +qmail-popup(8). diff --git authup.c authup.c new file mode 100644 index 0000000..561b17e --- /dev/null +++ authup.c @@ -0,0 +1,664 @@ +#include "auto_qmail.h" +#include "commands.h" +#include "sig.h" +#include "substdio.h" +#include "wait.h" +#include "str.h" +#include "byte.h" +#include "now.h" +#include "fmt.h" +#include "scan.h" +#include "readwrite.h" +#include "timeoutread.h" +#include "timeoutwrite.h" +#include "case.h" +#include "env.h" +#include "control.h" +#include "error.h" +#include "sgetopt.h" + +#include "acceptutils_base64.h" +#include "acceptutils_pfilter.h" +#include "acceptutils_stralloc.h" +#include "acceptutils_ucspitls.h" +#include "acceptutils_unistd.h" + +#define HOMEPAGE "https://schmonz.com/qmail/acceptutils" +#define PROGNAME "authup" + +#define EXITCODE_CHECKPASSWORD_UNACCEPTABLE 1 +#define EXITCODE_CHECKPASSWORD_MISUSED 2 +#define EXITCODE_CHECKPASSWORD_TEMPFAIL 111 +/* sync with fixsmtpio.h */ +#define EXITCODE_FIXSMTPIO_TIMEOUT 16 +#define EXITCODE_FIXSMTPIO_PARSEFAIL 18 + +static int auth_tries_remaining = 0; + +static int timeout = 1200; +static int tls_level = UCSPITLS_UNAVAILABLE; +static int in_tls = 0; + +static void die() { unistd_exit( 1); } + +static int safewrite(int fd,char *buf,int len) { + int r; + r = timeoutwrite(timeout,fd,buf,len); + if (r <= 0) die(); + return r; +} + +static char ssoutbuf[SUBSTDIO_OUTSIZE]; +static substdio ssout = SUBSTDIO_FDBUF(safewrite,1,ssoutbuf,sizeof ssoutbuf); + +static void out(const char *s) { substdio_puts(&ssout,s); } +static void flush() { substdio_flush(&ssout); } + +static void pop3_err(const char *s) { out("-ERR "); out(s); out("\r\n"); flush(); } +static void smtp_out(const char *s) { out(s); out("\r\n"); flush(); } + +struct authup_error { + const char *name; + const char *message; + const char *smtpcode; + const char *smtperror; +}; + +static const struct authup_error fatals[] = { + { "control", "unable to read controls", "421", "4.3.0" } +, { "nomem", "out of memory", "451", "4.3.0" } +, { "alarm", "timeout", "451", "4.4.2" } +, { "pipe", "unable to open pipe", "454", "4.3.0" } +, { "read", "unable to read pipe", "454", "4.3.0" } +, { "write", "unable to write pipe", "454", "4.3.0" } +, { "fork", "unable to fork", "454", "4.3.0" } +, { "wait", "unable to wait for child", "454", "4.3.0" } +, { "crash", "aack, child crashed", "454", "4.3.0" } +, { "badauth", "authorization failed", "535", "5.7.0" } +, { "protocol","protocol exchange ended", "501", "5.0.0" } +, { 0, "unknown or unspecified error", "421", "4.3.0" } +}; + +static const struct authup_error errors[] = { + { "badauth", "authorization failed", "535", "5.7.0" } +, { "noauth", "auth type unimplemented", "504", "5.5.1" } +, { "input", "malformed auth input", "501", "5.5.4" } +, { "authabrt","auth exchange cancelled", "501", "5.0.0" } +, { "starttls","TLS temporarily not available","454", "5.7.3" } +, { "needtls", "Must start TLS first", "530", "5.7.0" } +, { 0, "unknown or unspecified error", "421", "4.3.0" } +}; + +static void pop3_auth_error(struct authup_error ae) { + out("-ERR " PROGNAME " "); + out(ae.message); +} + +static void smtp_auth_error(struct authup_error ae) { + out(ae.smtpcode); + out(" " PROGNAME " "); + out(ae.message); + out(" (#"); + out(ae.smtperror); + out(")"); +} + +static void (*protocol_error)(); + +static char sserrbuf[SUBSTDIO_OUTSIZE]; +static substdio sserr = SUBSTDIO_FDBUF(write,2,sserrbuf,sizeof sserrbuf); + +static void authup_die(const char *name) { + int i; + for (i = 0;fatals[i].name;++i) if (case_equals(fatals[i].name,name)) break; + protocol_error(fatals[i]); + out("\r\n"); + flush(); + die(); +} + +static void authup_err(const char *name) { + int i; + for (i = 0;errors[i].name;++i) if (case_equals(errors[i].name,name)) break; + protocol_error(errors[i]); + out("\r\n"); + flush(); +} + +static void die_nomem(const char *caller,const char *alloc_fn) { + substdio_puts(&sserr,PROGNAME ": die_nomem: "); + if (caller) { + substdio_puts(&sserr,caller); + substdio_puts(&sserr,": "); + } + if (alloc_fn) { + substdio_puts(&sserr,alloc_fn); + } + substdio_putsflush(&sserr,"\n"); + authup_die("nomem"); +} + +static void die_usage() { + substdio_puts(&sserr,PROGNAME ": "); + substdio_puts(&sserr,"usage: " PROGNAME " [ -t tries ] prog"); + substdio_putsflush(&sserr,"\n"); + die(); +} + +static void smtp_err_authoriz() { smtp_out("530 " PROGNAME " authentication required (#5.7.1)"); } +static void pop3_err_authoriz() { pop3_err(PROGNAME " authorization first"); } + +static void pop3_err_syntax() { pop3_err(PROGNAME " syntax error"); } +static void pop3_err_wantuser() { pop3_err(PROGNAME " USER first"); } + +static int saferead(int fd,char *buf,int len) { + int r; + r = timeoutread(timeout,fd,buf,len); + if (r == -1) if (errno == error_timeout) authup_die("alarm"); + if (r <= 0) authup_die("read"); + return r; +} + +static char ssinbuf[SUBSTDIO_INSIZE]; +static substdio ssin = SUBSTDIO_FDBUF(saferead,0,ssinbuf,sizeof ssinbuf); + +static stralloc greeting = {0}; +static stralloc capabilities = {0}; +static char **childargs; + +static void pop3_okay() { out("+OK \r\n"); flush(); } +static void pop3_quit() { pop3_okay(); unistd_exit(0); } +static void smtp_quit() { out("221 "); smtp_out(greeting.s); unistd_exit(0); } + +static stralloc username = {0}; +static stralloc logname = {0}; +static stralloc password = {0}; +static stralloc timestamp = {0}; + +static char *format_pid(unsigned int pid) { + char pidbuf[FMT_ULONG]; + stralloc sa = {0}; + if (!sa.len) { + int len = fmt_ulong(pidbuf,pid); + if (len) copyb(&sa,pidbuf,len); + append0(&sa); + } + return sa.s; +} + +static char *authup_pid; + +static void logpass(int checkpassword_pid) { + substdio_puts(&sserr,PROGNAME); + substdio_puts(&sserr," "); + substdio_puts(&sserr,authup_pid); + substdio_puts(&sserr," checkpassword "); + substdio_puts(&sserr,format_pid(checkpassword_pid)); + substdio_puts(&sserr," succeeded, child completed"); + substdio_putsflush(&sserr,"\n"); +} + +static void logfail(int checkpassword_pid) { + substdio_puts(&sserr,PROGNAME); + substdio_puts(&sserr," "); + substdio_puts(&sserr,authup_pid); + substdio_puts(&sserr," checkpassword "); + substdio_puts(&sserr,format_pid(checkpassword_pid)); + substdio_puts(&sserr," failed, "); + substdio_puts(&sserr,format_pid(auth_tries_remaining)); + substdio_puts(&sserr," remaining"); + substdio_putsflush(&sserr,"\n"); +} + +static void logstart(char *protocol) { + substdio_puts(&sserr,PROGNAME); + substdio_puts(&sserr," "); + substdio_puts(&sserr,authup_pid); + substdio_puts(&sserr," protocol "); + substdio_puts(&sserr,protocol); + substdio_putsflush(&sserr,"\n"); +} + +static void exit_according_to_child_exit(int exitcode,int child) { + switch (exitcode) { + case EXITCODE_CHECKPASSWORD_UNACCEPTABLE: + case EXITCODE_CHECKPASSWORD_MISUSED: + case EXITCODE_CHECKPASSWORD_TEMPFAIL: + logfail(child); + pfilter_notify(1, 0, PROGNAME, authup_pid); + if (auth_tries_remaining) return authup_err("badauth"); + authup_die("badauth"); + case EXITCODE_FIXSMTPIO_TIMEOUT: + authup_die("alarm"); + case EXITCODE_FIXSMTPIO_PARSEFAIL: + authup_die("control"); + } + + logpass(child); + pfilter_notify(0, 0, PROGNAME, authup_pid); + unistd_exit(exitcode); +} + +static void logtry(int checkpassword_pid) { + substdio_puts(&sserr,PROGNAME); + substdio_puts(&sserr," "); + substdio_puts(&sserr,authup_pid); + substdio_puts(&sserr," checkpassword "); + substdio_puts(&sserr,format_pid(checkpassword_pid)); + substdio_puts(&sserr," attempt for "); + substdio_puts(&sserr,logname.s); + substdio_putsflush(&sserr,"\n"); +} + +static void checkpassword(stralloc *username,stralloc *password,stralloc *timestamp) { + int child; + int wstat; + int pi[2]; + char upbuf[SUBSTDIO_OUTSIZE]; + substdio ssup; + + copy(&logname,username); append0(&logname); + + auth_tries_remaining--; + + unistd_close(3); + if (unistd_pipe(pi) == -1) authup_die("pipe"); + if (pi[0] != 3) authup_die("pipe"); + switch((child = unistd_fork())) { + case -1: + authup_die("fork"); + case 0: + unistd_close(pi[1]); + sig_pipedefault(); + append0(username); + logtry(unistd_getpid()); + if (!env_put2("AUTHUP_USER",username->s)) authup_die("nomem"); + unistd_execvp(*childargs,childargs); + authup_die("fork"); + } + unistd_close(pi[0]); + substdio_fdbuf(&ssup,write,pi[1],upbuf,sizeof upbuf); + + append0(username); + if (substdio_put(&ssup,username->s,username->len) == -1) authup_die("write"); + byte_zero(username->s,username->len); + + append0(password); + if (substdio_put(&ssup,password->s,password->len) == -1) authup_die("write"); + byte_zero(password->s,password->len); + + append0(timestamp); + if (substdio_put(&ssup,timestamp->s,timestamp->len) == -1) authup_die("write"); + byte_zero(timestamp->s,timestamp->len); + + if (substdio_flush(&ssup) == -1) authup_die("write"); + unistd_close(pi[1]); + byte_zero(upbuf,sizeof upbuf); + + if (wait_pid(&wstat,child) == -1) authup_die("wait"); + if (wait_crashed(wstat)) authup_die("crash"); + + exit_according_to_child_exit(wait_exitcode(wstat),child); +} + +static char unique[FMT_ULONG + FMT_ULONG + 3]; + +static void pop3_greet() { + char *s; + s = unique; + s += fmt_uint(s,unistd_getpid()); + *s++ = '.'; + s += fmt_ulong(s,(unsigned long) now()); + *s++ = '@'; + *s++ = 0; + out("+OK <"); + out(unique); + out(greeting.s); + out(">\r\n"); + flush(); +} + +static void pop3_format_capa(stralloc *multiline) { + cats(multiline,".\r\n"); +} + +static void pop3_capa(char *arg) { + out("+OK capability list follows\r\n"); + if (tls_level >= UCSPITLS_AVAILABLE && !in_tls) out("STLS\r\n"); + out("USER\r\n"); + out(capabilities.s); + flush(); +} + +static int seenuser = 0; + +static void pop3_stls(char *arg) { + if (tls_level < UCSPITLS_AVAILABLE || in_tls) return pop3_err("STLS not available"); + out("+OK starting TLS negotiation\r\n"); + flush(); + + if (!tls_init() || !tls_info(die_nomem)) return authup_err("starttls"); + /* reset state */ + seenuser = 0; + + in_tls = 1; +} + +static void pop3_user(char *arg) { + if (tls_level >= UCSPITLS_REQUIRED && !in_tls) return authup_err("needtls"); + if (!*arg) { pop3_err_syntax(); return; } + pop3_okay(); + seenuser = 1; + copys(&username,arg); +} + +static void pop3_pass(char *arg) { + if (!seenuser) { pop3_err_wantuser(); return; } + if (!*arg) { pop3_err_syntax(); return; } + + copys(&password,arg); + byte_zero(arg,str_len(arg)); + + copys(×tamp,"<"); + cats(×tamp,unique); + cats(×tamp,greeting.s); + cats(×tamp,">"); + + checkpassword(&username,&password,×tamp); +} + +static void smtp_greet() { + out("220 "); + out(greeting.s); + out(" ESMTP\r\n"); + flush(); +} + +static void smtp_helo(char *arg) { + out("250 "); + smtp_out(greeting.s); +} + +// copy from fixsmtpio_munge.c:change_last_line_fourth_char_to_space() +static void smtp_format_ehlo(stralloc *multiline) { + int pos = 0; + int i; + for (i = multiline->len - 2; i >= 0; i--) { + if (multiline->s[i] == '\n') { + pos = i + 1; + break; + } + } + capabilities.s[pos+3] = ' '; +} + +static void smtp_ehlo(char *arg) { + char *x; + out("250-"); out(greeting.s); out("\r\n"); + if (tls_level >= UCSPITLS_AVAILABLE && !in_tls) out("250-STARTTLS\r\n"); + out("250-AUTH LOGIN PLAIN\r\n"); + if ((x = env_get("AUTHUP_SASL_BROKEN_CLIENTS"))) + out("250-AUTH=LOGIN PLAIN\r\n"); + out(capabilities.s); + flush(); +} + +static void smtp_starttls() { + if (tls_level < UCSPITLS_AVAILABLE || in_tls) return smtp_out("502 unimplemented (#5.5.1)"); + smtp_out("220 Ready to start TLS (#5.7.0)"); + + if (!tls_init() || !tls_info(die_nomem)) return authup_err("starttls"); + /* reset state */ + ssin.p = 0; + + in_tls = 1; +} + +static stralloc authin = {0}; + +static void smtp_authgetl() { + int i; + + blank(&authin); + + for (;;) { + if (!stralloc_readyplus(&authin,1)) authup_die("nomem"); /* XXX */ + i = substdio_get(&ssin,authin.s + authin.len,1); + if (i != 1) authup_die("read"); + if (authin.s[authin.len] == '\n') break; + ++authin.len; + } + + if (authin.len > 0) if (authin.s[authin.len - 1] == '\r') --authin.len; + authin.s[authin.len] = 0; + + if (*authin.s == '*' && *(authin.s + 1) == 0) return authup_err("authabrt"); + if (authin.len == 0) return authup_err("input"); +} + +static int b64decode2(char *c,int i,stralloc *sa) { + return b64decode((const unsigned char *)c,i,sa); +} + +static void auth_login(char *arg) { + int r; + + if (*arg) { + if ((r = b64decode2(arg,str_len(arg),&username)) == 1) return authup_err("input"); + } + else { + smtp_out("334 VXNlcm5hbWU6"); /* Username: */ + smtp_authgetl(); + if ((r = b64decode2(authin.s,authin.len,&username)) == 1) return authup_err("input"); + } + if (r == -1) authup_die("nomem"); + + smtp_out("334 UGFzc3dvcmQ6"); /* Password: */ + + smtp_authgetl(); + if ((r = b64decode2(authin.s,authin.len,&password)) == 1) return authup_err("input"); + if (r == -1) authup_die("nomem"); + + if (!username.len || !password.len) return authup_err("input"); + checkpassword(&username,&password,×tamp); +} + +static stralloc resp = {0}; + +static void auth_plain(char *arg) { + int r, id = 0; + + if (*arg) { + if ((r = b64decode2(arg,str_len(arg),&resp)) == 1) return authup_err("input"); + } + else { + smtp_out("334 "); + smtp_authgetl(); + if ((r = b64decode2(authin.s,authin.len,&resp)) == 1) return authup_err("input"); + } + if (r == -1) authup_die("nomem"); + append0(&resp); + while (resp.s[id]) id++; /* ignore authorize-id */ + + if (resp.len > id + 1) + copys(&username,resp.s + id + 1); + if (resp.len > id + username.len + 2) + copys(&password,resp.s + id + username.len + 2); + + if (!username.len || !password.len) return authup_err("input"); + checkpassword(&username,&password,×tamp); +} + +static void smtp_auth(char *arg) { + int i; + char *cmd = arg; + + if (tls_level >= UCSPITLS_REQUIRED && !in_tls) return authup_err("needtls"); + + i = str_chr(cmd,' '); + arg = cmd + i; + while (*arg == ' ') ++arg; + cmd[i] = 0; + + if (case_equals("login",cmd)) return auth_login(arg); + if (case_equals("plain",cmd)) return auth_plain(arg); + return authup_err("noauth"); +} + +static void smtp_help() { + smtp_out("214 " PROGNAME " home page: " HOMEPAGE); +} + +static void smtp_noop() { + smtp_out("250 ok"); +} + +static struct commands pop3commands[] = { + { "stls", pop3_stls, 0 } +, { "user", pop3_user, 0 } +, { "pass", pop3_pass, 0 } +, { "quit", pop3_quit, 0 } +, { "capa", pop3_capa, 0 } +, { "noop", pop3_okay, 0 } +, { 0, pop3_err_authoriz, 0 } +}; + +static struct commands smtpcommands[] = { + { "starttls", smtp_starttls, 0 } +, { "auth", smtp_auth, flush } +, { "quit", smtp_quit, 0 } +, { "helo", smtp_helo, 0 } +, { "ehlo", smtp_ehlo, 0 } +, { "help", smtp_help, 0 } +, { "noop", smtp_noop, 0 } +, { 0, smtp_err_authoriz, 0 } +}; + +struct protocol { + char *name; + char *cap_prefix; + void (*cap_format_response)(); + void (*error)(); + void (*greet)(); + int auth_tries_remaining; + struct commands *c; +}; + +static struct protocol p[] = { + { "pop3", "", pop3_format_capa, pop3_auth_error, pop3_greet, 1, pop3commands } +, { "smtp", "250-", smtp_format_ehlo, smtp_auth_error, smtp_greet, 5, smtpcommands } +, { 0, "", 0, die_usage, die_usage, 0, 0 } +}; + +static int control_readgreeting(char *p) { + stralloc file = {0}; + int retval; + + copys(&file,"control/"); + cats(&file,p); + cats(&file,"greeting"); + append0(&file); + + retval = control_rldef(&greeting,file.s,1,(char *) 0); + if (retval != 1) retval = -1; + + append0(&greeting); + + return retval; +} + +static int control_readtimeout(char *p) { + stralloc file = {0}; + + copys(&file,"control/timeout"); + cats(&file,p); + cats(&file,"d"); + append0(&file); + + return control_readint(&timeout,file.s); +} + +static int control_readcapabilities(struct protocol p) { + stralloc file = {0}; + stralloc lines = {0}; + int linestart; + int pos; + + copys(&file,"control/"); + cats(&file,p.name); + cats(&file,"capabilities"); + append0(&file); + + if (control_readfile(&lines,file.s,0) != 1) return -1; + + blank(&capabilities); + for (linestart = 0, pos = 0; pos < lines.len; pos++) { + if (lines.s[pos] == '\0') { + cats(&capabilities,p.cap_prefix); + cats(&capabilities,lines.s+linestart); + cats(&capabilities,"\r\n"); + linestart = pos + 1; + } + } + p.cap_format_response(&capabilities); + append0(&capabilities); + + return 1; +} + +static void doprotocol(struct protocol p) { + protocol_error = p.error; + + logstart(p.name); + + if (auth_tries_remaining == 0) auth_tries_remaining = p.auth_tries_remaining; + if (unistd_chdir(auto_qmail) == -1) authup_die("control"); + if (control_init() == -1) authup_die("control"); + if (control_readgreeting(p.name) == -1) authup_die("control"); + if (control_readtimeout(p.name) == -1) authup_die("control"); + if (control_readcapabilities(p) == -1) authup_die("control"); + p.greet(); + commands(&ssin,p.c); + authup_die("protocol"); +} + +int main(int argc,char **argv) { + char *protocol; + int opt; + int i; + + sig_alarmcatch(die); + sig_pipeignore(); + + stralloc_set_die(die_nomem); + + auth_tries_remaining = 0; + while ((opt = getopt(argc,argv,"t:")) != opteof) { + switch (opt) { + case 't': + if (!scan_ulong(optarg,&auth_tries_remaining)) die_usage(); + break; + default: + die_usage(); + } + } + argc -= optind; + argv += optind; + + if (!*argv) die_usage(); + + protocol = argv[0]; + if (!protocol) die_usage(); + + childargs = argv + 1; + if (!*childargs) die_usage(); + + tls_level = ucspitls_level_configured(); + + authup_pid = format_pid(unistd_getpid()); + + for (i = 0; p[i].name; ++i) + if (case_equals(p[i].name,protocol)) + doprotocol(p[i]); + die_usage(); +} diff --git checknotroot.8 checknotroot.8 new file mode 100644 index 0000000..2c016a6 --- /dev/null +++ checknotroot.8 @@ -0,0 +1,32 @@ +.TH CHECKNOTROOT 8 2018-12-01 +.SH NAME +checknotroot \- refuse to run as UID 0 +.SH SYNOPSIS +.B checknotroot +.I prog +.SH DESCRIPTION +.B checknotroot +ensures that another command can never be +run as UID 0. + +.B checknotroot +is most commonly placed after +.B checkpassword +and before its remaining arguments to ensure that if someone guesses the +root password, they won't know they did. + +.SH "EXIT CODES" +If run by UID 0, +.B checknotroot +exits 1. +Otherwise it exits with the same code as +.IR prog . +.SH "EXAMPLES" +See +.IR https://schmonz.com/qmail/acceptutils . +.SH "AUTHOR" +.B Amitai Schleier +.SH "SEE ALSO" +checkpassword(8), +authup(8), +fixsmtpio(8). diff --git checknotroot.c checknotroot.c new file mode 100644 index 0000000..0f6d887 --- /dev/null +++ checknotroot.c @@ -0,0 +1,31 @@ +#include "exit.h" +#include "readwrite.h" +#include "substdio.h" + +#include "acceptutils_unistd.h" + +char sserrbuf[SUBSTDIO_OUTSIZE]; +substdio sserr = SUBSTDIO_FDBUF(write,2,sserrbuf,sizeof sserrbuf); + +void errflush(char *s) { + substdio_puts(&sserr,"checknotroot: "); + substdio_puts(&sserr,s); + substdio_puts(&sserr,"\n"); + substdio_flush(&sserr); +} + +void die() { _exit(1); } +void die_usage() { errflush("usage: checknotroot prog"); die(); } +void die_root() { errflush("WAS RUNNING AS ROOT, TERMINATING"); die(); } + +int main(int argc,char **argv) { + char **childargs; + + childargs = argv + 1; + if (!*childargs) die_usage(); + + if (unistd_getuid() == 0) die_root(); + + unistd_execvp(*childargs,childargs); + die(); +} diff --git conf-check conf-check new file mode 100644 index 0000000..566cfbc --- /dev/null +++ conf-check @@ -0,0 +1,3 @@ +/opt/pkg + +This is the prefix where libcheck is installed. diff --git fixsmtpio.8 fixsmtpio.8 new file mode 100644 index 0000000..df9a361 --- /dev/null +++ fixsmtpio.8 @@ -0,0 +1,258 @@ +.TH FIXSMTPIO 8 2020-12-11 +.SH NAME +fixsmtpio \- filter SMTP I/O and exit status +.SH SYNOPSIS +.B fixsmtpio +.I prog +.SH DESCRIPTION +.B fixsmtpio +is a proxy for changing how an SMTP service behaves without changing its code. +By default, with no configuration, it changes almost no observable behavior. +.P +In a typical configuration, +.B fixsmtpio +enables TLS for +.BR qmail-smtpd , +modernizes responses from +.BR ofmipd , +and makes both programs behave as though they were designed to run under +.BR authup(8) . +.B fixsmtpio +accomplishes this by adjusting certain +client requests, +server responses, +and exit codes. +The +.B "EXIT CODES" +section is important enough to be all the way up here: +.SH "EXIT CODES" +.B fixsmtpio +exits 18 when +.I control/fixsmtpio +exists and fails to parse. +.P +Part of +.BR fixsmtpio 's +job is to return distinct exit codes for distinct conditions. +For instance, +.B authup +needs to distinguish +.B checkpassword +exiting 1 (to reject a password) +from +.B ofmipd +or +.B qmail-smtpd +exiting 1 (on any error). +When running under +.BR authup , +.B fixsmtpio +can and should be configured to exit uniquely for each SMTP condition: +.TP 3 +14 +when the server greeting code starts with 4 (temporary failure), +.TP 3 +15 +when the server greeting code starts with 5 (permanent failure), +.TP 3 +16 +when the server times out, +and +.TP 3 +0 +when the server receives EOF from the client. +.SH "ENVIRONMENT VARIABLES" +When running under +.B "sslserver -n" +or +.BR "s6-ucspitlsd" , +.B fixsmtpio +can offer TLS. +Set +.B UCSPITLS +to the empty string to permit peers to negotiate TLS before transferring messages. +If the peer has negotiated TLS, +.B FIXSMTPIOTLS +will be set for use by filter rules. +.P +When running under +.BR authup , +.B AUTHUP_USER +will be set for use by filter rules +(such as those recommended in +.BR "EXIT CODES" ). +.P +If +.B DISABLETLS +is set, the presence and value of +.B UCSPITLS +will be ignored. +.P +If +.B NOFIXSMTPIO +is set, +.B fixsmtpio +will simply replace itself with +.IR prog . +.P +If +.B FIXSMTPIODEBUG +is set, +.B fixsmtpio +will show its work. +Lines are prefixed like so: +.TP 3 +1: Request received from client +.TP 3 +2: Request sent to server +.TP 3 +3: Response received from server +.TP 3 +4: Response sent to client +.TP 3 +D: DATA received from client and sent to server as is +.SH "FILTER RULES" +Filter rules follow the format: +.P +.I [env]:event:[request-prepend]:response-line-glob:[exitcode]:[response] +.P +(Values other than +.I response +may not contain ":", the field separator.) +.TP 5 +.I env +.br +(optional) +Environment variable which must be present for the rule to apply. +If empty, none is required. +.TP 5 +.I event +.br +(required) +SMTP verb (or "clienteof", "greeting", or "timeout") to which the rule applies. +This field is matched case-insensitively. +.TP 5 +.I request-prepend +.br +(optional) +String to prepend to the request before passing it to the server. +"NOOP " (with trailing space) causes the server to trivially accept a request, +performing no action. +If empty, the request is sent as is. +.TP 5 +.I response-line-glob +.br +(required) +.BR fnmatch(3) -style +glob pattern, with no special options set. +Each line of the response is compared against this glob. +.TP 5 +.I exitcode +.br +(optional) +Numeric code with which +.B fixsmtpio +is to exit immediately. +If empty, it will exit whenever it normally would. +If there are no rules specifying a value for this field, +.B fixsmtpio +will always exit with the same code as its child +.IR prog . +.TP 5 +.I response +.br +(optional) +String that replaces every matching line of the server response. + +If empty, matching lines are removed. + +Strings starting with +"&fixsmtpio" +are reserved for special handling. + +"&fixsmtpio_noop" +causes the response to be sent as is. + +"&fixsmtpio_fixup" +computes the replacement string via an event-specific internal routine. +(If no corresponding routine exists, +.B fixsmtpio + +Routines exist for +.TP 10 +.BR HELP : +.br +Prepend this program's home page, +in an attempt to direct support requests to +.BR AUTHOR . +Please enable it! +.TP 10 +.BR "greeting", +.BR EHLO , +.BR HELO , +and +.BR QUIT : +.br +Include +.I smtpgreeting +in the response. +This is harmless with +.BR qmail-smtpd , +which already does so, +and brings +.BR ofmipd 's +behavior up to par. +.TP 0 +Rules are applied in the order written. For instance, if two rules +match, the later rule re-modifies the response returned by the +earlier rule. +If multiple matching rules for an event set +.BR exitcode , +the last one wins. +.P +Not all rules (alone or in combination) make practical sense. +An earlier rule can cause a later one to start or stop matching. +Keep your configuration as simple as possible, and test it well. +.SH "CONTROL FILES" +.TP 5 +.I fixsmtpio +Filter rules as described in +.BR "FILTER RULES" . +Default: none. +.TP 5 +.I smtpgreeting +SMTP greeting message. +Default: +.IR me , +if that is supplied; +otherwise +.B fixsmtpio +will refuse to run. +The first word of +.I smtpgreeting +should be the current host's name. +.SH "COMPATIBILITY" +.B fixsmtpio +must terminate TLS in order to observe and modify requests and responses. +If you rely on +.B qmail-smtpd +being patched to support STARTTLS directly, +please share your use case with the author. +.SH "EXAMPLES" +See +.IR https://schmonz.com/qmail/acceptutils . +.SH "AUTHOR" +.B Amitai Schleier +.SH "SEE ALSO" +authup(8), +sslserver(1), +s6-ucspitlsd, +ucspi-tls(2), +checkpassword(8), +checknotroot(8), +qmail-smtpd(8), +ofmipd(8), +qmail-qfilter-queue(8), +fnmatch(3), +fixcrio(1), +spamdyke. diff --git fixsmtpio.c fixsmtpio.c new file mode 100644 index 0000000..f05f785 --- /dev/null +++ fixsmtpio.c @@ -0,0 +1,33 @@ +#include "fixsmtpio.h" +#include "fixsmtpio_die.h" +#include "fixsmtpio_filter.h" +#include "fixsmtpio_proxy.h" + +#include "acceptutils_stralloc.h" +#include "acceptutils_unistd.h" + +static void load_smtp_greeting(stralloc *greeting,char *configfile) { + if (control_init() == -1) die_control(); + if (control_rldef(greeting,configfile,1,(char *) 0) != 1) die_control(); +} + +static void cd_var_qmail() { + if (unistd_chdir(auto_qmail) == -1) die_control(); +} + +int main(int argc,char **argv) { + stralloc greeting = {0}; + filter_rule *rules; + + argv++; if (!*argv) die_usage(); + + if (env_get("NOFIXSMTPIO")) unistd_execvp(*argv,argv); + + stralloc_set_die(die_nomem); + + cd_var_qmail(); + load_smtp_greeting(&greeting,"control/smtpgreeting"); + rules = load_filter_rules(); + + be_proxy(&greeting,rules,argv); +} diff --git fixsmtpio.h fixsmtpio.h new file mode 100644 index 0000000..fd5be60 --- /dev/null +++ fixsmtpio.h @@ -0,0 +1,32 @@ +#include "alloc.h" +#include "auto_qmail.h" +#include "case.h" +#include "control.h" +#include "env.h" +#include "fd.h" +#include "scan.h" +#include "str.h" +#include "stralloc.h" +#include "substdio.h" +#include "wait.h" + +#define HOMEPAGE "https://schmonz.com/qmail/acceptutils" +#define PROGNAME "fixsmtpio" + +#define EVENT_GREETING "greeting" +#define EVENT_TIMEOUT "timeout" +#define EVENT_CLIENTEOF "clienteof" +#define MUNGE_INTERNALLY "&" PROGNAME "_fixup" +#define REQUEST_PASSTHRU "" +#define REQUEST_NOOP "NOOP " + +#define RESPONSELINE_NOCHANGE "&" PROGNAME "_noop" + +#define BEGIN_STARTTLS_NOW -2 +#define EXIT_LATER_NORMALLY -1 +#define EXIT_NOW_SUCCESS 0 +#define EXIT_NOW_TEMPFAIL 14 +#define EXIT_NOW_PERMFAIL 15 +/* sync with authup.c */ +#define EXIT_NOW_TIMEOUT 16 +#define EXIT_NOW_PARSEFAIL 18 diff --git fixsmtpio_control.c fixsmtpio_control.c new file mode 100644 index 0000000..3ec1b6c --- /dev/null +++ fixsmtpio_control.c @@ -0,0 +1,73 @@ +#include "fixsmtpio_control.h" +#include "fixsmtpio_munge.h" + +#include "acceptutils_stralloc.h" + +static void parse_field(int *fields_seen, stralloc *value, filter_rule *rule) { + char *s; + + (*fields_seen)++; + + if (!value->len) return; + + append0(value); + s = (char *)alloc(value->len); + str_copy(s, value->s); + blank(value); + + switch (*fields_seen) { + case 1: rule->env = s; break; + case 2: rule->event = s; break; + case 3: rule->request_prepend = s; break; + case 4: rule->response_line_glob = s; break; + case 5: + if (!scan_ulong(s,&rule->exitcode)) + rule->exitcode = 777; + break; + case 6: rule->response = s; break; + } +} + +filter_rule *parse_control_line(char *line) { + filter_rule *rule = (filter_rule *)alloc(sizeof(filter_rule)); + int line_length = str_len(line); + stralloc value = {0}; + int fields_seen = 0; + int i; + + rule->next = 0; + + rule->env = 0; + rule->event = 0; + rule->request_prepend = 0; + rule->response_line_glob = 0; + rule->exitcode = EXIT_LATER_NORMALLY; + rule->response = 0; + + for (i = 0; i < line_length; i++) { + char c = line[i]; + if (':' == c && fields_seen < 5) parse_field(&fields_seen, &value, rule); + else append(&value, &c); + } + parse_field(&fields_seen, &value, rule); + + if (fields_seen < 6) return 0; + if (!rule->event) return 0; + if (!rule->response_line_glob) return 0; + if ( rule->exitcode > 255) return 0; + if ( rule->response) { + if (!case_diffs(rule->event,"clienteof")) + return 0; + if (want_munge_internally(rule->response) + && !munge_line_fn(rule->event)) + return 0; + if (str_start(rule->response,"&fixsmtpio") + && !want_munge_internally(rule->response) + && !want_leave_line_as_is(rule->response)) + return 0; + } else { + rule->response = ""; + } + + return rule; +} diff --git fixsmtpio_control.h fixsmtpio_control.h new file mode 100644 index 0000000..ea821a7 --- /dev/null +++ fixsmtpio_control.h @@ -0,0 +1,3 @@ +#include "fixsmtpio_filter.h" + +filter_rule *parse_control_line(char *); diff --git fixsmtpio_die.c fixsmtpio_die.c new file mode 100644 index 0000000..c628c5e --- /dev/null +++ fixsmtpio_die.c @@ -0,0 +1,63 @@ +#include "fixsmtpio.h" +#include "fixsmtpio_die.h" +#include "readwrite.h" + +#include "acceptutils_stralloc.h" +#include "acceptutils_unistd.h" + +static void die() { unistd_exit(1); } + +static char sserrbuf[SUBSTDIO_OUTSIZE]; +substdio sserr = SUBSTDIO_FDBUF(write,2,sserrbuf,sizeof sserrbuf); + +static void errflush3(const char *caller,const char *alloc_fn,char *s) { + substdio_puts(&sserr,PROGNAME ":"); + if (caller) { + substdio_puts(&sserr,caller); + substdio_puts(&sserr,":"); + } + if (alloc_fn) { + substdio_puts(&sserr,alloc_fn); + substdio_puts(&sserr,":"); + } + substdio_puts(&sserr," "); + substdio_puts(&sserr,s); + substdio_putsflush(&sserr,"\n"); +} + +static void errflush(char *s) { + errflush3(0,0,s); +} + +static void dieerrflush(char *s) { + errflush(s); + die(); +} + +void die_usage() { dieerrflush("usage: " PROGNAME " prog [ arg ... ]"); } +void die_control(){dieerrflush("unable to read controls"); } +void die_pipe() { dieerrflush("unable to open pipe"); } +void die_fork() { dieerrflush("unable to fork"); } +void die_exec() { dieerrflush("unable to exec"); } +void die_wait() { dieerrflush("unable to wait for child"); } +void die_kill() { dieerrflush("unable to kill child"); } +void die_crash() { dieerrflush("aack, child crashed"); } +void die_read() { dieerrflush("unable to read"); } +void die_write() { dieerrflush("unable to write"); } +void die_nomem(const char *caller,const char *alloc_fn) { + errflush3(caller,alloc_fn,"out of memory"); + die(); +} +void die_tls() { dieerrflush("TLS temporarily not available"); } +void die_parse() { errflush("unable to parse control/fixsmtpio"); + unistd_exit(EXIT_NOW_PARSEFAIL); } + +void logit(stralloc *logstamp,char logprefix,stralloc *sa) { + if (!env_get("FIXSMTPIODEBUG")) return; + + substdio_put (&sserr,logstamp->s,logstamp->len); + substdio_put (&sserr,&logprefix,1); substdio_puts(&sserr,": "); + substdio_put (&sserr,sa->s,sa->len); + if (!ends_with_newline(sa)) substdio_puts(&sserr,"\n"); + substdio_flush(&sserr); +} diff --git fixsmtpio_die.h fixsmtpio_die.h new file mode 100644 index 0000000..6874ba7 --- /dev/null +++ fixsmtpio_die.h @@ -0,0 +1,16 @@ +#include "stralloc.h" + +void die_usage(void); +void die_control(void); +void die_pipe(void); +void die_fork(void); +void die_exec(void); +void die_wait(void); +void die_kill(void); +void die_crash(void); +void die_read(void); +void die_write(void); +void die_nomem(const char *,const char *); +void die_tls(void); +void die_parse(void); +void logit(stralloc *,char,stralloc *); diff --git fixsmtpio_eventq.c fixsmtpio_eventq.c new file mode 100644 index 0000000..2f3104d --- /dev/null +++ fixsmtpio_eventq.c @@ -0,0 +1,62 @@ +#include "alloc.h" +#include "str.h" +#include "fixsmtpio.h" +#include "fixsmtpio_die.h" + +#include "fixsmtpio_eventq.h" + +#define NULL 0 +#include + +typedef struct node { + const char *event; + TAILQ_ENTRY(node) nodes; +} node_t; + +typedef TAILQ_HEAD(head_s, node) head_t; + +static head_t head; +static int eventq_inited = 0; + +static void eventq_init() { + if (eventq_inited) return; + TAILQ_INIT(&head); + eventq_inited++; +} + +static node_t *eventq_alloc_node() { + node_t *e = (node_t *)alloc(sizeof(node_t)); + if (!e) die_nomem(__func__,"alloc"); + return e; +} + +static char *eventq_alloc_event(const char *event) { + char *s = (char *)alloc(sizeof(char) * (1 + str_len(event))); + if (!s) die_nomem(__func__,"alloc"); + return s; +} + +void eventq_put(const char *event) { + node_t *e; + eventq_init(); + e = eventq_alloc_node(); + e->event = eventq_alloc_event(event); + str_copy(e->event,event); + TAILQ_INSERT_TAIL(&head, e, nodes); +} + +const char *eventq_get() { + const char *event; + node_t *e; + if (TAILQ_EMPTY(&head)) { + event = eventq_alloc_event(EVENT_TIMEOUT); + str_copy(event,EVENT_TIMEOUT); + } else { + e = TAILQ_FIRST(&head); + event = e->event; + TAILQ_REMOVE(&head, e, nodes); + alloc_free(e); + } + + return event; +} diff --git fixsmtpio_eventq.h fixsmtpio_eventq.h new file mode 100644 index 0000000..8b6e221 --- /dev/null +++ fixsmtpio_eventq.h @@ -0,0 +1,2 @@ +void eventq_put(const char *); +const char *eventq_get(void); diff --git fixsmtpio_filter.c fixsmtpio_filter.c new file mode 100644 index 0000000..47591a2 --- /dev/null +++ fixsmtpio_filter.c @@ -0,0 +1,113 @@ +#include "fixsmtpio.h" +#include "fixsmtpio_control.h" +#include "fixsmtpio_filter.h" +#include "fixsmtpio_die.h" +#include "fixsmtpio_munge.h" +#include "fixsmtpio_glob.h" + +#include "acceptutils_stralloc.h" + +int want_munge_internally(char *response) { + return !str_diff(MUNGE_INTERNALLY,response); +} + +int want_leave_line_as_is(char *response) { + return !str_diff(RESPONSELINE_NOCHANGE,response); +} + +int envvar_exists_if_needed(char *envvar) { + if (envvar) { + if (!str_diff("",envvar)) return 1; + if (env_get(envvar)) return 1; + return 0; + } + return 1; +} + +int filter_rule_applies(filter_rule *rule,const char *event) { + return (event_matches(rule->event,event) && envvar_exists_if_needed(rule->env)); +} + +void munge_response_line(int lineno, + stralloc *line,int *exitcode, + stralloc *greeting,filter_rule *rules,const char *event, + int tls_level,int in_tls) { + filter_rule *rule; + stralloc line0 = {0}; + + copy(&line0,line); + append0(&line0); + + for (rule = rules; rule; rule = rule->next) { + if (!filter_rule_applies(rule,event)) continue; + if (!string_matches_glob(rule->response_line_glob,line0.s)) continue; + munge_exitcode(exitcode,rule); + if (want_munge_internally(rule->response)) + munge_line_internally(line,lineno,greeting,event,tls_level,in_tls); + else if (!want_leave_line_as_is(rule->response)) + copys(line,rule->response); + } + if (line->len) if (!ends_with_newline(line)) cats(line,"\r\n"); +} + +void munge_response(stralloc *response,int *exitcode, + stralloc *greeting,filter_rule *rules,const char *event, + int tls_level,int in_tls) { + stralloc munged = {0}; + stralloc line = {0}; + int lineno = 0; + int i; + + for (i = 0; i < response->len; i++) { + append(&line,i + response->s); + if (response->s[i] == '\n' || i == response->len - 1) { + munge_response_line(lineno++,&line,exitcode,greeting,rules,event,tls_level,in_tls); + cat(&munged,&line); + blank(&line); + } + } + + if (munged.len) reformat_multiline_response(&munged); + copy(response,&munged); +} + +filter_rule *prepend_rule(filter_rule *next, filter_rule *rule) { + rule->next = next; + next = rule; + return next; +} + +filter_rule *reverse_rules(filter_rule *rules) { + filter_rule *reversed_rules = 0; + filter_rule *temp; + + while (rules) { + temp = rules; + rules = rules->next; + temp->next = reversed_rules; + reversed_rules = temp; + } + + return reversed_rules; +} + +filter_rule *load_filter_rules(void) { + filter_rule *backwards_rules = 0; + + stralloc lines = {0}; + int linestart; + int pos; + + if (control_readfile(&lines,"control/fixsmtpio",0) == -1) die_control(); + + for (linestart = 0, pos = 0; pos < lines.len; pos++) { + if (lines.s[pos] == '\0') { + filter_rule *rule = parse_control_line(lines.s + linestart); + if (0 == rule) die_parse(); + backwards_rules = prepend_rule(backwards_rules, rule); + linestart = pos + 1; + } + } + + return reverse_rules(backwards_rules); +} diff --git fixsmtpio_filter.h fixsmtpio_filter.h new file mode 100644 index 0000000..7a0b20b --- /dev/null +++ fixsmtpio_filter.h @@ -0,0 +1,29 @@ +#ifndef _FIXSMTPIO_FILTER_H_ +#define _FIXSMTPIO_FILTER_H_ + +#include "fixsmtpio.h" + +typedef struct filter_rule { + struct filter_rule *next; + char *env; + char *event; + char *request_prepend; + char *response_line_glob; + int exitcode; + char *response; +} filter_rule; + +int want_munge_internally(char *); +int want_leave_line_as_is(char *); +int envvar_exists_if_needed(char *); + +filter_rule * load_filter_rules(void); +int filter_rule_applies(filter_rule *,const char *); + +int event_matches(char *,const char *); + +void munge_response(stralloc *,int *,stralloc *,filter_rule *,const char *,int,int); +void munge_response_line(int,stralloc *,int *,stralloc *,filter_rule *,const char *,int,int); +filter_rule *prepend_rule(filter_rule *,filter_rule *); + +#endif diff --git fixsmtpio_glob.c fixsmtpio_glob.c new file mode 100644 index 0000000..5b2c370 --- /dev/null +++ fixsmtpio_glob.c @@ -0,0 +1,8 @@ +#include "fixsmtpio_glob.h" + +/* + maybe this should be regex instead of glob? + */ +int string_matches_glob(char *glob,char *string) { + return 0 == fnmatch(glob,string,0); +} diff --git fixsmtpio_glob.h fixsmtpio_glob.h new file mode 100644 index 0000000..98ed86b --- /dev/null +++ fixsmtpio_glob.h @@ -0,0 +1,3 @@ +#include + +int string_matches_glob(char *,char *); diff --git fixsmtpio_munge.c fixsmtpio_munge.c new file mode 100644 index 0000000..94ad0ce --- /dev/null +++ fixsmtpio_munge.c @@ -0,0 +1,116 @@ +#include "fixsmtpio_munge.h" + +#include "acceptutils_stralloc.h" +#include "acceptutils_ucspitls.h" + +void munge_exitcode(int *exitcode,filter_rule *rule) { + if (rule->exitcode != EXIT_LATER_NORMALLY) *exitcode = rule->exitcode; +} + +void munge_greeting(stralloc *response,int lineno,stralloc *greeting, + int tls_level,int in_tls) { + copys(response,"220 "); cat(response,greeting); cats(response," ESMTP"); +} + +void munge_helo(stralloc *response,int lineno,stralloc *greeting, + int tls_level,int in_tls) { + copys(response,"250 "); cat(response,greeting); +} + +static int is_starttls_line(stralloc *response) { + return starts(response,"250-STARTTLS\r\n") + || starts(response,"250 STARTTLS\r\n"); +} + +void munge_ehlo(stralloc *response,int lineno,stralloc *greeting, + int tls_level,int in_tls) { + switch (lineno) { + case 0: + munge_helo(response,lineno,greeting,tls_level,in_tls); + break; + case 1: + if (is_starttls_line(response)) blank(response); + if (tls_level >= UCSPITLS_AVAILABLE && !in_tls && !env_get("AUTHUP_USER")) + prepends(response,"250-STARTTLS\r\n"); + break; + default: + if (is_starttls_line(response)) blank(response); + break; + } +} + +void munge_help(stralloc *response,int lineno,stralloc *greeting, + int tls_level,int in_tls) { + stralloc munged = {0}; + copys(&munged,"214 " PROGNAME " home page: " HOMEPAGE "\r\n"); + cat(&munged,response); + copy(response,&munged); +} + +void munge_quit(stralloc *response,int lineno,stralloc *greeting, + int tls_level,int in_tls) { + copys(response,"221 "); cat(response,greeting); +} + +void change_every_line_fourth_char_to_dash(stralloc *multiline) { + int pos = 0; + int i; + for (i = 0; i < multiline->len; i++) { + if (multiline->s[i] == '\n') pos = -1; + if (pos == 3) multiline->s[i] = '-'; + pos++; + } +} + +// copy to with authup.c:smtp_ehlo_format() +void change_last_line_fourth_char_to_space(stralloc *multiline) { + int pos = 0; + int i; + for (i = multiline->len - 2; i >= 0; i--) { + if (multiline->s[i] == '\n') { + pos = i + 1; + break; + } + } + multiline->s[pos+3] = ' '; +} + +void reformat_multiline_response(stralloc *response) { + change_every_line_fourth_char_to_dash(response); + change_last_line_fourth_char_to_space(response); +} + +int event_matches(char *s,const char *s2) { + if (!s || !s2) return 0; + if (!str_len(s) || !str_len(s2)) return 0; + return !case_diffs(s,s2); +} + +struct munge_command { + char *event; + void (*munger)(); +}; + +struct munge_command m[] = { + { EVENT_GREETING, munge_greeting } +, { "ehlo", munge_ehlo } +, { "helo", munge_helo } +, { "help", munge_help } +, { "quit", munge_quit } +, { 0, 0 } +}; + +void *munge_line_fn(const char *event) { + int i; + for (i = 0; m[i].event; ++i) + if (event_matches(m[i].event,event)) + return m[i].munger; + return 0; +} + +void munge_line_internally(stralloc *line,int lineno, + stralloc *greeting,const char *event, + int tls_level,int in_tls) { + void (*munger)() = munge_line_fn(event); + if (munger) munger(line,lineno,greeting,tls_level,in_tls); +} diff --git fixsmtpio_munge.h fixsmtpio_munge.h new file mode 100644 index 0000000..511e080 --- /dev/null +++ fixsmtpio_munge.h @@ -0,0 +1,14 @@ +#include "fixsmtpio_filter.h" + +void munge_exitcode(int *,filter_rule *); +void munge_greeting(stralloc *,int,stralloc *,int,int); +void munge_helo(stralloc *,int,stralloc *,int,int); +void munge_ehlo(stralloc *,int,stralloc *,int,int); +void munge_help(stralloc *,int,stralloc *,int,int); +void munge_quit(stralloc *,int,stralloc *,int,int); +void reformat_multiline_response(stralloc *); +int event_matches(char *,const char *); +void *munge_line_fn(const char *); +void munge_line_internally(stralloc *,int,stralloc *,const char *,int,int); +void change_every_line_fourth_char_to_dash(stralloc *); +void change_last_line_fourth_char_to_space(stralloc *); diff --git fixsmtpio_proxy.c fixsmtpio_proxy.c new file mode 100644 index 0000000..fd8b8cf --- /dev/null +++ fixsmtpio_proxy.c @@ -0,0 +1,413 @@ +#include +#include + +#include "fmt.h" + +#include "fixsmtpio_proxy.h" +#include "fixsmtpio_readwrite.h" +#include "fixsmtpio_die.h" +#include "fixsmtpio_eventq.h" +#include "fixsmtpio_filter.h" + +#include "acceptutils_stralloc.h" +#include "acceptutils_ucspitls.h" +#include "acceptutils_unistd.h" + +int ends_data(stralloc *r) { + int len = r->len; + + if (!len ) return 0; + if ( r->s[--len] != '\n') return 0; + if (len && r->s[--len] != '\r') ++len; + if (!len ) return 0; + if ( r->s[--len] != '.') return 0; + if (len && r->s[--len] != '\n') return 0; + + return 1; +} + +static int find_first_space(stralloc *request) { + int i; + for (i = 0; i < request->len; i++) if (request->s[i] == ' ') return i; + return -1; +} + +void strip_last_eol(stralloc *sa) { + if (sa->len > 0 && sa->s[sa->len-1] == '\n') sa->len--; + if (sa->len > 0 && sa->s[sa->len-1] == '\r') sa->len--; +} + +static void all_verb_no_arg(stralloc *verb,stralloc *arg,stralloc *request) { + copy(verb,request); + strip_last_eol(verb); + blank(arg); +} + +static void verb_and_arg(stralloc *verb,stralloc *arg,int pos,stralloc *request) { + copyb(verb,request->s,pos-1); + copyb(arg,request->s+pos,request->len-pos); + strip_last_eol(arg); +} + +void parse_client_request(stralloc *verb,stralloc *arg,stralloc *request) { + int pos; + pos = find_first_space(request); + if (pos == -1) + all_verb_no_arg(verb,arg,request); + else + verb_and_arg(verb,arg,++pos,request); +} + +static int need_starttls_first(int tls_level,int in_tls,const char *event) { + return tls_level >= UCSPITLS_REQUIRED + && !in_tls + && !event_matches(EVENT_GREETING,event) + && !event_matches(EVENT_TIMEOUT,event) + && !event_matches(EVENT_CLIENTEOF,event) + && !event_matches("noop",event) + && !event_matches("ehlo",event) + && !event_matches("starttls",event) + && !event_matches("quit",event); +} + +void construct_proxy_request(stralloc *proxy_request, + filter_rule *rules, + const char *event,stralloc *arg, + stralloc *client_request, + int tls_level, + int *want_tls,int in_tls, + int *want_data) { + filter_rule *rule; + + for (rule = rules; rule; rule = rule->next) + if (rule->request_prepend && filter_rule_applies(rule,event)) + prepends(proxy_request,rule->request_prepend); + if (need_starttls_first(tls_level,in_tls,event)) + prepends(proxy_request,REQUEST_NOOP PROGNAME " STARTTLS FIRST "); + else if (event_matches("starttls",event)) { + *want_tls = 1; + if (tls_level >= UCSPITLS_AVAILABLE) { + if (in_tls) + prepends(proxy_request,REQUEST_NOOP PROGNAME " STARTTLS AGAIN "); + else + prepends(proxy_request,REQUEST_NOOP PROGNAME " STARTTLS BEGIN "); + } else { + prepends(proxy_request,REQUEST_NOOP PROGNAME " STARTTLS BLOCK "); + } + } + else if (event_matches("data",event)) + *want_data = 1; + cat(proxy_request,client_request); +} + +static int accepted_data(stralloc *response) { return starts(response,"354 "); } + +void construct_proxy_response(stralloc *proxy_response, + stralloc *greeting, + filter_rule *rules, + const char *event, + stralloc *server_response, + int *proxy_exitcode, + int tls_level, + int want_tls,int in_tls, + int *want_data,int *in_data) { + if (event_matches("data",event) && *want_data) { + *want_data = 0; + if (accepted_data(server_response)) { + eventq_put("in_data"); + *in_data = 1; + } + } + + if (event_matches("starttls",event) && want_tls) { + if (tls_level < UCSPITLS_AVAILABLE || in_tls) + copys(proxy_response,"502 unimplemented (#5.5.1)\r\n"); + else + copys(proxy_response,"220 Ready to start TLS (#5.7.0)\r\n"); + return; + } + if (need_starttls_first(tls_level,in_tls,event)) + copys(proxy_response,"530 Must start TLS first (#5.7.0)\r\n"); + else + copy(proxy_response,server_response); + munge_response(proxy_response,proxy_exitcode,greeting,rules,event,tls_level,in_tls); +} + +int get_one(const char *caller,stralloc *one,stralloc *pile,int (*fn)(stralloc *)) { + stralloc caller_sa = {0}; + int got_one = 0; + stralloc next_pile = {0}; + int pos = 0; + int i; + + for (i = pos; i < pile->len; i++) { + if (pile->s[i] == '\n') { + stralloc line = {0}; + + catb(&line,pile->s+pos,i+1-pos); + pos = i+1; + cat(one,&line); + + if (!fn || fn(&line)) { + got_one = 1; + break; + } + } + } + + if (got_one) { + copys(&caller_sa,(char *)caller); + cats(&caller_sa,":"); + cats(&caller_sa,(char *)__func__); + append0(&caller_sa); + + contextlogging_copyb(caller_sa.s,&next_pile,pile->s+pos,pile->len-pos); + contextlogging_copy(caller_sa.s,pile,&next_pile); + blank(&next_pile); + + blank(&caller_sa); + } else { + blank(one); + } + + return got_one; +} + +int get_one_request(stralloc *one,stralloc *pile) { + return get_one(__func__,one,pile,0); +} + +int is_last_line_of_response(stralloc *line) { + return line->len >= 4 && line->s[3] == ' '; +} + +int get_one_response(stralloc *one,stralloc *pile) { + return get_one(__func__,one,pile,&is_last_line_of_response); +} + +static void handle_data_specially(stralloc *data,int *in_data,stralloc *logstamp) { + logit(logstamp,'D',data); + if (ends_data(data)) + *in_data = 0; +} + +static void handle_request(stralloc *proxy_request,stralloc *request, + int tls_level,int *want_tls,int in_tls, + int *want_data, + filter_rule *rules,stralloc *logstamp) { + stralloc event = {0}, verb = {0}, arg = {0}; + + logit(logstamp,'1',request); + parse_client_request(&verb,&arg,request); + copy(&event,&verb); + append0(&event); + eventq_put(event.s); + construct_proxy_request(proxy_request,rules, + event.s,&arg, + request, + tls_level, + want_tls,in_tls, + want_data); + blank(request); + logit(logstamp,'2',proxy_request); +} + +static void handle_response(int *exitcode, + stralloc *proxy_response,stralloc *response, + int tls_level,int want_tls,int in_tls, + int *want_data,int *in_data, + filter_rule *rules,stralloc *greeting, + stralloc *logstamp) { + const char *event; + logit(logstamp,'3',response); + event = eventq_get(); + construct_proxy_response(proxy_response, + greeting,rules,event, + response, + exitcode, + tls_level, + want_tls,in_tls, + want_data,in_data); + logit(logstamp,'4',proxy_response); + alloc_free(event); + blank(response); +} + +static void use_as_stdin(int fd) { if (fd_move(0,fd) == -1) die_pipe(); } +static void use_as_stdout(int fd) { if (fd_move(1,fd) == -1) die_pipe(); } + +static void make_pipe(int *from,int *to) { + int pi[2]; + if (unistd_pipe(pi) == -1) die_pipe(); + *from = pi[0]; + *to = pi[1]; +} + +static void be_proxied(int from_proxy,int to_proxy, + int from_server,int to_server, + char **argv) { + unistd_close(from_server); + unistd_close(to_server); + use_as_stdin(from_proxy); + use_as_stdout(to_proxy); + unistd_execvp(*argv,argv); + die_exec(); +} + +static char *format_pid(unsigned int pid) { + char pidbuf[FMT_ULONG]; + stralloc sa = {0}; + if (!sa.len) { + int len = fmt_ulong(pidbuf,pid); + if (len) copyb(&sa,pidbuf,len); + append0(&sa); + } + return sa.s; +} + +static void prepare_logstamp(stralloc *sa,int kid_pid,char *kid_name) { + copys(sa,PROGNAME " "); + cats(sa,format_pid(unistd_getpid())); cats(sa," "); + cats(sa,kid_name); cats(sa," "); + cats(sa,format_pid(kid_pid)); cats(sa," "); +} + +static void stop_kid_and_maybe_myself(int exitcode,int kid_pid, + int from_server,int to_server) { + int wstat; + int startingtls = (exitcode == BEGIN_STARTTLS_NOW); + + unistd_close(from_server); + unistd_close(to_server); + + if (startingtls && -1 == kill(kid_pid,SIGTERM)) die_kill(); + + if (wait_pid(&wstat,kid_pid) == -1) die_wait(); + + if (startingtls) return; + + if (wait_crashed(wstat)) die_crash(); + + if (exitcode == EXIT_LATER_NORMALLY) + unistd_exit(wait_exitcode(wstat)); + else + unistd_exit(exitcode); +} + +static void run_new_kid_in_read_loop(int from_client,int to_proxy, + int from_proxy,int to_server, + int from_server,int to_client, + stralloc *logstamp,stralloc *greeting, + filter_rule *rules,char **argv, + int in_tls) { + int kid_pid = unistd_fork(); + + if (kid_pid) { + unistd_close(from_proxy); + unistd_close(to_proxy); + prepare_logstamp(logstamp,kid_pid,basename(argv[0])); + eventq_put(EVENT_GREETING); + stop_kid_and_maybe_myself( + read_and_process_until_either_end_closes(from_client,to_server, + from_server,to_client, + greeting,rules, + logstamp,in_tls), + kid_pid,from_server,to_server); + } else if (0 == kid_pid) { + be_proxied(from_proxy,to_proxy, + from_server,to_server, + argv); + } else { + die_fork(); + } +} + +void be_proxy(stralloc *greeting,filter_rule *rules,char **argv) { + int from_client = 0, to_proxy; + int from_proxy, to_server; + int from_server, to_client = 1; + stralloc logstamp = {0}; + + for (int in_tls = 0; in_tls <= 1; in_tls++) { + make_pipe(&from_proxy,&to_server); + make_pipe(&from_server,&to_proxy); + run_new_kid_in_read_loop(from_client,to_proxy, + from_proxy,to_server, + from_server,to_client, + &logstamp,greeting, + rules,argv, + in_tls); + } +} + +int read_and_process_until_either_end_closes(int from_client,int to_server, + int from_server,int to_client, + stralloc *greeting, + filter_rule *rules, + stralloc *logstamp, + int in_tls) { + char buf [SUBSTDIO_INSIZE]; + int exitcode = EXIT_LATER_NORMALLY; + int tls_level = ucspitls_level_configured(), + want_tls = 0, + want_data = 0, in_data = 0; + stralloc client_requests = {0}, one_request = {0}, proxy_request = {0}, + server_responses = {0}, one_response = {0}, proxy_response = {0}; + + for (;;) { + if (!block_efficiently_until_can_read_either(from_client,from_server)) break; + + if (can_read(from_client)) { + if (!safeappend(&client_requests,from_client,buf,sizeof buf)) { + // XXX maybe telnet and Ctrl-C gets here?? + munge_response_line(0, + &client_requests,&exitcode, + greeting,rules,EVENT_CLIENTEOF, + tls_level,in_tls); + break; + } + while (client_requests.len) { + if (in_data) { + handle_data_specially(&client_requests,&in_data,logstamp); + safewrite(to_server,&client_requests); + } else if (get_one_request(&one_request,&client_requests)) { + handle_request(&proxy_request,&one_request, + tls_level,&want_tls,in_tls, + &want_data, + rules,logstamp); + safewrite(to_server,&proxy_request); + //} else { + // /* not in_data and no full request received yet */ + // break; + } + } + } + + if (can_read(from_server)) { + if (!safeappend(&server_responses,from_server,buf,sizeof buf)) break; + while (server_responses.len + && exitcode == EXIT_LATER_NORMALLY + && get_one_response(&one_response,&server_responses)) { + handle_response(&exitcode, + &proxy_response,&one_response, + tls_level,want_tls,in_tls, + &want_data,&in_data, + rules,greeting, + logstamp); + safewrite(to_client,&proxy_response); + if (want_tls) { + want_tls = 0; + if (tls_level >= UCSPITLS_AVAILABLE && !in_tls) { + if (!tls_init() || !tls_info(die_nomem)) die_tls(); + if (!env_put("FIXSMTPIOTLS=1")) die_nomem(__func__,"env_put"); + exitcode = BEGIN_STARTTLS_NOW; + } + } + } + } + + if (exitcode != EXIT_LATER_NORMALLY) break; + } + + return exitcode; +} diff --git fixsmtpio_proxy.h fixsmtpio_proxy.h new file mode 100644 index 0000000..112a1b3 --- /dev/null +++ fixsmtpio_proxy.h @@ -0,0 +1,11 @@ +#include "fixsmtpio.h" +#include "fixsmtpio_filter.h" + +void strip_last_eol(stralloc *); +int ends_data(stralloc *); +int is_last_line_of_response(stralloc *); +void parse_client_request(stralloc *,stralloc *,stralloc *); +int get_one_response(stralloc *,stralloc *); +int read_and_process_until_either_end_closes(int,int,int,int,stralloc *,filter_rule *,stralloc *,int); +void construct_proxy_request(stralloc *,filter_rule *,const char *,stralloc *,stralloc *,int,int *,int,int *); +void be_proxy(stralloc *,filter_rule *,char **); diff --git fixsmtpio_readwrite.c fixsmtpio_readwrite.c new file mode 100644 index 0000000..42ffa9f --- /dev/null +++ fixsmtpio_readwrite.c @@ -0,0 +1,46 @@ +#include "fixsmtpio_readwrite.h" +#include "fixsmtpio_die.h" +#include "error.h" +#include "readwrite.h" +#include "select.h" + +#include "acceptutils_stralloc.h" + +fd_set fds; + +static void want_to_read(int fd1,int fd2) { + FD_ZERO(&fds); + FD_SET(fd1,&fds); + FD_SET(fd2,&fds); +} + +int can_read(int fd) { return FD_ISSET(fd,&fds); } + +static int max(int a,int b) { return a > b ? a : b; } + +int block_efficiently_until_can_read_either(int fd1,int fd2) { + int ready; + want_to_read(fd1,fd2); + ready = select(1+max(fd1,fd2),&fds,(fd_set *)0,(fd_set *)0,(struct timeval *) 0); + if (ready == -1 && errno != error_intr) die_read(); + return ready; +} + +static int saferead(int fd,char *buf,int len) { + int r; + r = read(fd,buf,len); + if (r == -1) if (errno != error_intr) die_read(); + return r; +} + +int safeappend(stralloc *sa,int fd,char *buf,int len) { + int r; + r = saferead(fd,buf,len); + catb(sa,buf,r); + return r; +} + +void safewrite(int fd,stralloc *sa) { + if (write(fd,sa->s,sa->len) == -1) die_write(); + blank(sa); +} diff --git fixsmtpio_readwrite.h fixsmtpio_readwrite.h new file mode 100644 index 0000000..b1d77b9 --- /dev/null +++ fixsmtpio_readwrite.h @@ -0,0 +1,6 @@ +#include "stralloc.h" + +int can_read(int); +int block_efficiently_until_can_read_either(int,int); +int safeappend(stralloc *,int,char *,int); +void safewrite(int,stralloc *); diff --git make-compile99.sh make-compile99.sh new file mode 100644 index 0000000..5699fcf --- /dev/null +++ make-compile99.sh @@ -0,0 +1 @@ +echo exec "$CC" -std=c99 -c '${1+"$@"}' diff --git qmail-qfilter-addtlsheader.8 qmail-qfilter-addtlsheader.8 new file mode 100644 index 0000000..d4714de --- /dev/null +++ qmail-qfilter-addtlsheader.8 @@ -0,0 +1,53 @@ +.TH QMAIL-QFILTER-ADDTLSHEADER 8 2018-12-01 +.SH NAME +qmail-qfilter-addtlsheader \- Add Received: header with TLS parameters +.SH SYNOPSIS +.B qmail-qfilter-addtlsheader +.SH DESCRIPTION +.B qmail-qfilter-addtlsheader +takes a message (headers and body) on standard input +and prints it to standard output. +If +.B UCSPITLS +connection details are available, it prepends a +.B Received: +header. +.PP +.B qmail-qfilter-addtlsheader +is intended to be invoked by +.BR qmail-qfilter . +It is typically used under +.B authup +or +.B fixsmtpio +(via +.BR qmail-qfilter-queue ). +.SH "ENVIRONMENT VARIABLES" +If both +.B SSL_CIPHER +and +.B SSL_PROTOCOL +are set, +.B qmail-qfilter-addtlsheader +will add its header. +.PP +If either +.B FIXSMTPIOTLS +or +.B AUTHUP_USER +is set, the header will mention +.BR fixsmtpio (8) +or +.BR authup (8), +respectively. +.SH "EXAMPLES" +See +.IR https://schmonz.com/qmail/acceptutils . +.SH "AUTHOR" +.B Amitai Schleier +.SH "SEE ALSO" +.BR ucspi-tls(2), +.BR qmail-qfilter(1), +.BR authup(8), +.BR fixsmtpio(8), +.BR qmail-qfilter-queue(8). diff --git qmail-qfilter-addtlsheader.c qmail-qfilter-addtlsheader.c new file mode 100755 index 0000000..12e8b0d --- /dev/null +++ qmail-qfilter-addtlsheader.c @@ -0,0 +1,54 @@ +#include "datetime.h" +#include "date822fmt.h" +#include "env.h" +#include "now.h" +#include "readwrite.h" +#include "substdio.h" + +static char inbuf[SUBSTDIO_INSIZE]; +static substdio ssin = SUBSTDIO_FDBUF(read,0,inbuf,sizeof(inbuf)); +static char outbuf[SUBSTDIO_OUTSIZE]; +static substdio ssout = SUBSTDIO_FDBUF(write,1,outbuf,sizeof(outbuf)); + +static void out(char *s) { substdio_puts(&ssout,s); } + +static char datebuf[DATE822FMT]; + +static void set_now(char *datebuf) { + struct datetime dt; + datetime_tai(&dt,now()); + date822fmt(datebuf,&dt); +} + +static void perhaps_write_tls_header() { + char *ssl_cipher, *ssl_protocol, *authup_user; + + if ((ssl_cipher = env_get("SSL_CIPHER")) + && (ssl_protocol = env_get("SSL_PROTOCOL"))) { + out("Received: (ucspitls"); + if (env_get("FIXSMTPIOTLS")) { + out(" acceptutils fixsmtpio"); + } else if ((authup_user = env_get("AUTHUP_USER"))) { + out(" acceptutils authup "); + out(authup_user); + } + out( " "); out(ssl_protocol); + out( " "); out(ssl_cipher); + set_now(datebuf); + out("); "); out(datebuf); + substdio_flush(&ssout); + } +} + +static void copy_stdin_to_stdout() { + int i; + + while ((i = substdio_get(&ssin,inbuf,sizeof(inbuf))) > 0) + substdio_putflush(&ssout,inbuf,i); +} + +int main(void) { + perhaps_write_tls_header(); + copy_stdin_to_stdout(); + return 0; +} diff --git test_acceptutils_stralloc.c test_acceptutils_stralloc.c new file mode 100644 index 0000000..6f1f8d9 --- /dev/null +++ test_acceptutils_stralloc.c @@ -0,0 +1,31 @@ +#include "check.h" + +#include "acceptutils_stralloc.h" + +void assert_prepends(const char *input, char *prepend, const char *expected_output) { + stralloc sa = {0}; stralloc_copys(&sa, input); + + prepends(&sa, prepend); + + stralloc_0(&sa); + ck_assert_str_eq(sa.s, expected_output); +} + +START_TEST (test_prepends) +{ + assert_prepends("", "", ""); + assert_prepends("", "foo", "foo"); + assert_prepends("bar", "", "bar"); + assert_prepends("baz", "foo bar", "foo barbaz"); + assert_prepends("baz quux", "foo bar ", "foo bar baz quux"); + assert_prepends(" baz quux", "foo bar", "foo bar baz quux"); +} +END_TEST + +TCase *tc_stralloc(void) { + TCase *tc = tcase_create(""); + + tcase_add_test(tc, test_prepends); + + return tc; +} diff --git test_fixsmtpio.c test_fixsmtpio.c new file mode 100644 index 0000000..68bea62 --- /dev/null +++ test_fixsmtpio.c @@ -0,0 +1,41 @@ +#include "check.h" +#include + +extern TCase *tc_stralloc(void); +extern TCase *tc_control(void); +extern TCase *tc_eventq(void); +extern TCase *tc_filter(void); +extern TCase *tc_glob(void); +extern TCase *tc_munge(void); +extern TCase *tc_proxy(void); + +Suite * fixsmtpio_suite(void) +{ + Suite *s = suite_create("fixsmtpio"); + + suite_add_tcase(s, tc_stralloc()); + suite_add_tcase(s, tc_control()); + suite_add_tcase(s, tc_eventq()); + suite_add_tcase(s, tc_filter()); + suite_add_tcase(s, tc_glob()); + suite_add_tcase(s, tc_munge()); + suite_add_tcase(s, tc_proxy()); + + return s; +} + +int main(void) +{ + int number_failed; + Suite *s; + SRunner *sr; + + s = fixsmtpio_suite(); + sr = srunner_create(s); + + srunner_set_tap(sr, "-"); + srunner_run_all(sr, CK_SILENT); + number_failed = srunner_ntests_failed(sr); + srunner_free(sr); + return (number_failed == 0) ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git test_fixsmtpio_control.c test_fixsmtpio_control.c new file mode 100644 index 0000000..2e4f8e1 --- /dev/null +++ test_fixsmtpio_control.c @@ -0,0 +1,200 @@ +#include "check.h" + +#include "fixsmtpio_control.h" + +#define assert_str_null_or_eq(s1,s2) \ + if (s1 == NULL) \ + ck_assert_ptr_null(s2); \ + else \ + ck_assert_str_eq(s1, s2); + +void assert_parsed_line(char *input, + char *env,char *event,char *request_prepend, + char *response_line_glob,int exitcode,char *response) { + filter_rule *rule = parse_control_line(input); + + ck_assert_ptr_nonnull(rule); + ck_assert_ptr_null(rule->next); + assert_str_null_or_eq(env, rule->env); + assert_str_null_or_eq(event, rule->event); + assert_str_null_or_eq(request_prepend, rule->request_prepend); + assert_str_null_or_eq(response_line_glob, rule->response_line_glob); + ck_assert_int_eq(exitcode, rule->exitcode); + assert_str_null_or_eq(response, rule->response); +} + +void assert_non_parsed_line(char *input) { + ck_assert_ptr_null(parse_control_line(input)); +} + +START_TEST (test_reject_blank_line) { + assert_non_parsed_line( + "" + ); +} END_TEST + +START_TEST (test_reject_just_a_comma) { + assert_non_parsed_line( + "," + ); +} END_TEST + +START_TEST (test_reject_just_a_colon) { + assert_non_parsed_line( + ":" + ); +} END_TEST + +START_TEST (test_accept_empty_env) { + assert_parsed_line( + ":event:prepend:glob:55:response", + NULL,"event","prepend","glob",55,"response" + ); +} END_TEST + +START_TEST (test_reject_empty_event) { + assert_non_parsed_line( + "env::prepend:glob:55:response" + ); +} END_TEST + +START_TEST (test_accept_invented_event) { + assert_parsed_line( + "env:flibbertigibbet:prepend:glob:55:response", + "env","flibbertigibbet","prepend","glob",55,"response" + ); +} END_TEST + +START_TEST (test_accept_empty_request_prepend) { + assert_parsed_line( + "env:event::glob:55:response", + "env","event",NULL,"glob",55,"response" + ); +} END_TEST + +START_TEST (test_reject_empty_response_line_glob) { + assert_non_parsed_line( + "env:event:prepend::55:response" + ); +} END_TEST + +START_TEST (test_accept_empty_exitcode) { + assert_parsed_line( + "env:event:prepend:glob::response", + "env","event","prepend","glob",EXIT_LATER_NORMALLY,"response" + ); +} END_TEST + +START_TEST (test_reject_exitcode_non_numeric) { + assert_non_parsed_line( + "env:event:prepend:glob:exitcode:response" + ); +} END_TEST + +START_TEST (test_reject_exitcode_too_large) { + assert_non_parsed_line( + "env:event:prepend:glob:500:response" + ); +} END_TEST + +START_TEST (test_accept_valid_exitcode) { + assert_parsed_line( + "env:event:prepend:glob:5:response", + "env","event","prepend","glob",5,"response" + ); +} END_TEST + +START_TEST (test_accept_empty_response) { + assert_parsed_line( + "env:event:prepend:glob:55:", + "env","event","prepend","glob",55,"" + ); +} END_TEST + +START_TEST (test_reject_response_not_specified) { + assert_non_parsed_line( + "env:event:prepend:glob:55" + ); +} END_TEST + +START_TEST (test_accept_response_containing_space) { + assert_parsed_line( + "env:event:prepend:glob:55:response 250 ok", + "env","event","prepend","glob",55,"response 250 ok" + ); +} END_TEST + +START_TEST (test_accept_response_containing_colon) { + assert_parsed_line( + "env:event:prepend:glob:55:response: 250 ok", + "env","event","prepend","glob",55,"response: 250 ok" + ); +} END_TEST + +START_TEST (test_accept_realistic_line) { + assert_parsed_line( + ":word:NOOP :*::250 indeed", + NULL,"word","NOOP ","*",EXIT_LATER_NORMALLY,"250 indeed" + ); +} END_TEST + +START_TEST (test_reject_clienteof_with_custom_response) { + assert_non_parsed_line( + "env:clienteof:prepend:glob:55:custom response" + ); +} END_TEST + +START_TEST (test_accept_fixup_for_applicable_event) { + assert_parsed_line( + "env:HELO:prepend:glob:55:&fixsmtpio_fixup", + "env","HELO","prepend","glob",55,"&fixsmtpio_fixup" + ); +} END_TEST + +START_TEST (test_reject_fixup_for_inapplicable_event) { + assert_non_parsed_line( + "env:event:prepend:glob:55:&fixsmtpio_fixup" + ); +} END_TEST + +START_TEST (test_reject_unknown_response_thingy) { + assert_non_parsed_line( + "env:event:prepend:glob:55:&fixsmtpio_unknown_thingy" + ); +} END_TEST + +START_TEST (test_accept_other_ampersand_response) { + assert_parsed_line( + "env:event:prepend:glob:55:&other_ampersand_response", + "env","event","prepend","glob",55,"&other_ampersand_response" + ); +} END_TEST + +TCase *tc_control(void) { + TCase *tc = tcase_create(""); + + tcase_add_test(tc, test_reject_blank_line); + tcase_add_test(tc, test_reject_just_a_comma); + tcase_add_test(tc, test_reject_just_a_colon); + tcase_add_test(tc, test_accept_empty_env); + tcase_add_test(tc, test_reject_empty_event); + tcase_add_test(tc, test_accept_invented_event); + tcase_add_test(tc, test_accept_empty_request_prepend); + tcase_add_test(tc, test_reject_empty_response_line_glob); + tcase_add_test(tc, test_accept_empty_exitcode); + tcase_add_test(tc, test_reject_exitcode_non_numeric); + tcase_add_test(tc, test_reject_exitcode_too_large); + tcase_add_test(tc, test_accept_valid_exitcode); + tcase_add_test(tc, test_accept_empty_response); + tcase_add_test(tc, test_reject_response_not_specified); + tcase_add_test(tc, test_accept_response_containing_space); + tcase_add_test(tc, test_accept_response_containing_colon); + tcase_add_test(tc, test_accept_realistic_line); + tcase_add_test(tc, test_reject_clienteof_with_custom_response); + tcase_add_test(tc, test_accept_fixup_for_applicable_event); + tcase_add_test(tc, test_reject_fixup_for_inapplicable_event); + tcase_add_test(tc, test_reject_unknown_response_thingy); + tcase_add_test(tc, test_accept_other_ampersand_response); + + return tc; +} diff --git test_fixsmtpio_eventq.c test_fixsmtpio_eventq.c new file mode 100644 index 0000000..517c5b0 --- /dev/null +++ test_fixsmtpio_eventq.c @@ -0,0 +1,28 @@ +#include "check.h" + +#include "fixsmtpio_eventq.h" + +START_TEST (test_eventq_put_and_get) +{ + ck_assert_str_eq(eventq_get(), "timeout"); + + eventq_put("foo"); + ck_assert_str_eq(eventq_get(), "foo"); + + eventq_put("bar"); + eventq_put("baz"); + eventq_put("quux"); + ck_assert_str_eq(eventq_get(), "bar"); + ck_assert_str_eq(eventq_get(), "baz"); + ck_assert_str_eq(eventq_get(), "quux"); + ck_assert_str_eq(eventq_get(), "timeout"); +} +END_TEST + +TCase *tc_eventq(void) { + TCase *tc = tcase_create(""); + + tcase_add_test(tc, test_eventq_put_and_get); + + return tc; +} diff --git test_fixsmtpio_filter.c test_fixsmtpio_filter.c new file mode 100644 index 0000000..6cf4ba4 --- /dev/null +++ test_fixsmtpio_filter.c @@ -0,0 +1,120 @@ +#include "check.h" + +#include "fixsmtpio_filter.h" + +void assert_filter_rule(filter_rule *filter_rule, const char *event, int expected) { + ck_assert_int_eq(filter_rule_applies(filter_rule, event), expected); +} + +START_TEST (test_filter_rule_applies) +{ + filter_rule rule = { + 0, + 0, "caliente", + REQUEST_PASSTHRU, "*", + EXIT_LATER_NORMALLY, "", + }; + assert_filter_rule(&rule, "clienteof", 0); + + rule.event = "clienteof"; + assert_filter_rule(&rule, "clienteof", 1); +} +END_TEST + +START_TEST (test_want_munge_internally) { + ck_assert_int_eq(1, want_munge_internally("&fixsmtpio_fixup")); + ck_assert_int_eq(0, want_munge_internally("&fixsmtpio_noop")); + ck_assert_int_eq(0, want_munge_internally("")); + //ck_assert_int_eq(0, want_munge_internally(NULL)); + ck_assert_int_eq(0, want_munge_internally("random other text\r\n")); +} +END_TEST + +START_TEST (test_want_leave_line_as_is) { + ck_assert_int_eq(1, want_leave_line_as_is("&fixsmtpio_noop")); + ck_assert_int_eq(0, want_leave_line_as_is("&fixsmtpio_fixup")); + ck_assert_int_eq(0, want_leave_line_as_is("")); + //ck_assert_int_eq(0, want_leave_line_as_is(NULL)); + ck_assert_int_eq(0, want_leave_line_as_is("random other text\r\n")); +} +END_TEST + +START_TEST (test_envvar_exists_if_needed) { + ck_assert_int_eq(0, envvar_exists_if_needed("VERY_UNLIKELY_TO_BE_SET")); + ck_assert_int_eq(1, envvar_exists_if_needed("")); + ck_assert_int_eq(1, envvar_exists_if_needed(NULL)); +} +END_TEST + +void assert_munge_response_line(char *expected_output, int lineno, char *line, int exitcode, char *greeting, filter_rule *rules, char *event) { + stralloc line_sa = {0}; stralloc_copys(&line_sa, line); + stralloc greeting_sa = {0}; stralloc_copys(&greeting_sa, greeting); + + munge_response_line(lineno, &line_sa, &exitcode, &greeting_sa, rules, event, 0, 0); + + stralloc_0(&line_sa); + + ck_assert_str_eq(line_sa.s, expected_output); +} + +START_TEST (test_munge_response_line) { + filter_rule *rules = 0; + filter_rule helo = { + 0, + 0, "helo", + REQUEST_PASSTHRU, "2*", + EXIT_LATER_NORMALLY, MUNGE_INTERNALLY, + }; + filter_rule ehlo = { + 0, + 0, "ehlo", + REQUEST_PASSTHRU, "2*", + EXIT_LATER_NORMALLY, MUNGE_INTERNALLY, + }; + + assert_munge_response_line("222 sup duuuude\r\n", 0, "222 sup duuuude", 0, "yo.sup.local", rules, "ehlo"); + assert_munge_response_line("222 OUTSTANDING\r\n", 1, "222 OUTSTANDING", 0, "yo.sup.local", rules, "ehlo"); + + rules = prepend_rule(rules, &helo); + assert_munge_response_line("222 sup duuuude\r\n", 0, "222 sup duuuude", 0, "yo.sup.local", rules, "ehlo"); + assert_munge_response_line("222 OUTSTANDING\r\n", 1, "222 OUTSTANDING", 0, "yo.sup.local", rules, "ehlo"); + + rules = prepend_rule(rules, &ehlo); + assert_munge_response_line("250 yo.sup.local\r\n", 0, "222 sup duuuude", 0, "yo.sup.local", rules, "ehlo"); + assert_munge_response_line("222 OUTSTANDING\r\n", 1, "222 OUTSTANDING", 0, "yo.sup.local", rules, "ehlo"); +} +END_TEST + +void assert_munge_response(char *expected_output, char *response, int exitcode, char *greeting, filter_rule *rules, char *event) { + stralloc response_sa = {0}; stralloc_copys(&response_sa, response); + stralloc greeting_sa = {0}; stralloc_copys(&greeting_sa, greeting); + + munge_response(&response_sa, &exitcode, &greeting_sa, rules, event, 0, 0); + + stralloc_0(&response_sa); + + ck_assert_str_eq(response_sa.s, expected_output); +} + +START_TEST (test_munge_response) { + filter_rule *rules = 0; + + // annoying to test NULL, unlikely to be bug + assert_munge_response("", "", EXIT_LATER_NORMALLY, "yo.sup.local", rules, "ehlo"); + assert_munge_response("512 grump\r\n", "512 grump", EXIT_LATER_NORMALLY, "yo.sup.local", rules, "ehlo"); + assert_munge_response("512-grump\r\n256 mump\r\n", "512 grump\r\n256 mump", EXIT_LATER_NORMALLY, "yo.sup.local", rules, "ehlo"); +} +END_TEST + +TCase *tc_filter(void) { + TCase *tc = tcase_create(""); + + tcase_add_test(tc, test_filter_rule_applies); + tcase_add_test(tc, test_want_munge_internally); + tcase_add_test(tc, test_want_leave_line_as_is); + tcase_add_test(tc, test_envvar_exists_if_needed); + tcase_add_test(tc, test_munge_response_line); + tcase_add_test(tc, test_munge_response); + + return tc; +} diff --git test_fixsmtpio_glob.c test_fixsmtpio_glob.c new file mode 100644 index 0000000..8e8287c --- /dev/null +++ test_fixsmtpio_glob.c @@ -0,0 +1,35 @@ +#include "check.h" + +#include "fixsmtpio_glob.h" + +START_TEST (test_string_matches_glob) { + ck_assert(string_matches_glob("*", "")); + ck_assert(string_matches_glob("*", "foob;;ar")); + ck_assert(string_matches_glob("4*", "450 tempfail")); + ck_assert(string_matches_glob("4*", "4")); + ck_assert(string_matches_glob("250?STARTTLS", "250?STARTTLS")); + ck_assert(string_matches_glob("250?AUTH*", "250 AUTH ")); + ck_assert(string_matches_glob("250?AUTH*", "250 AUTH")); + ck_assert(string_matches_glob("250?auth*", "250 auth")); + ck_assert(string_matches_glob("2*", "250-auth login")); + ck_assert(string_matches_glob("250?auth*", "250-auth login")); + ck_assert(string_matches_glob("", "")); + + + ck_assert(!string_matches_glob("250 auth", "the anthology contains works by 250 authors")); + ck_assert(!string_matches_glob("250?AUTH*", "250 AUTH")); + ck_assert(!string_matches_glob("250?AUTH*", " 250 AUTH")); + ck_assert(!string_matches_glob("250?AUTH*", " 250 AUTH")); + ck_assert(!string_matches_glob("4*", "foob;;ar")); + ck_assert(!string_matches_glob("", " 250 AUTH")); + ck_assert(!string_matches_glob("4*", "I have eaten 450 french fries")); +} +END_TEST + +TCase *tc_glob(void) { + TCase *tc = tcase_create(""); + + tcase_add_test(tc, test_string_matches_glob); + + return tc; +} diff --git test_fixsmtpio_munge.c test_fixsmtpio_munge.c new file mode 100644 index 0000000..8f57a5b --- /dev/null +++ test_fixsmtpio_munge.c @@ -0,0 +1,152 @@ +#include "check.h" + +#include "fixsmtpio_munge.h" + +void assert_change_every_line_fourth_char_to_dash( char *input, char *expected_response) { + stralloc response_sa = {0}; stralloc_copys(&response_sa, input); + change_every_line_fourth_char_to_dash(&response_sa); + stralloc_0(&response_sa); + ck_assert_str_eq(response_sa.s, expected_response); + +} + +START_TEST (test_change_every_line_fourth_char_to_dash) { + // annoying to test, currently don't believe I have this bug: + // assert_change_every_line_fourth_char_to_dash(NULL, ""); + + assert_change_every_line_fourth_char_to_dash("", ""); + + assert_change_every_line_fourth_char_to_dash("ab", "ab"); + + assert_change_every_line_fourth_char_to_dash("abc", "abc"); + + assert_change_every_line_fourth_char_to_dash("abcd", "abc-"); + + assert_change_every_line_fourth_char_to_dash("abcd efgh\n", "abc- efgh\n"); + + assert_change_every_line_fourth_char_to_dash( + "abcd efgh\nijk\n", "abc- efgh\nijk\n"); + + assert_change_every_line_fourth_char_to_dash( + "ijk\n" + "abcd efgh\n", + + "ijk\n" + "abc- efgh\n"); + + assert_change_every_line_fourth_char_to_dash( + "abcd efgh\n" + "ijk\n" + "bcde fghi\n", + + "abc- efgh\n" + "ijk\n" + "bcd- fghi\n"); +} +END_TEST + +void assert_change_last_line_fourth_char_to_space(char *input, char *expected_response) { + stralloc response_sa = {0}; stralloc_copys(&response_sa, input); + change_last_line_fourth_char_to_space(&response_sa); + stralloc_0(&response_sa); + ck_assert_str_eq(response_sa.s, expected_response); +} + +START_TEST (test_change_last_line_fourth_char_to_space) { + // annoying to test, currently don't believe I have this bug: + // assert_change_last_line_fourth_char_to_space(NULL, ""); + + assert_change_last_line_fourth_char_to_space("", ""); + + assert_change_last_line_fourth_char_to_space("ab", "ab"); + + assert_change_last_line_fourth_char_to_space("abc", "abc"); + + assert_change_last_line_fourth_char_to_space("abcd", "abc "); + + assert_change_last_line_fourth_char_to_space("abcd efgh\n", "abc efgh\n"); + + assert_change_last_line_fourth_char_to_space( + "abcd efgh\nij\n", "abcd efgh\nij\n"); + + assert_change_last_line_fourth_char_to_space( + "abcd efgh\nijk\n", "abcd efgh\nijk "); + + assert_change_last_line_fourth_char_to_space( + "ijk\n" + "abcd efgh\n", + + "ijk\n" + "abc efgh\n"); + + assert_change_last_line_fourth_char_to_space( + "abcd efgh\n" + "ijk\n" + "bcde fghi\n", + + "abcd efgh\n" + "ijk\n" + "bcd fghi\n"); +} +END_TEST + +START_TEST (test_event_matches) { + /* + char empty_unterminated[] = { }; + char unterminated[] = {'f', 'o', 'o' }; + char terminated[] = {'f', 'o', 'o', '\0'}; + + ck_assert(!event_matches(empty_unterminated, "")); + ck_assert(!event_matches("", empty_unterminated)); + ck_assert( event_matches(empty_unterminated, empty_unterminated)); + + ck_assert(!event_matches(unterminated, "")); + ck_assert(!event_matches("", unterminated)); + ck_assert( event_matches(unterminated, unterminated)); + + ck_assert(!event_matches(unterminated, NULL)); + ck_assert(!event_matches(NULL, unterminated)); + + ck_assert( event_matches(unterminated, terminated)); + */ + + ck_assert(!event_matches(NULL, "")); + ck_assert(!event_matches("", NULL)); + ck_assert(!event_matches(NULL, NULL)); + ck_assert(!event_matches("", "")); + + ck_assert(!event_matches("foo", "bar")); + ck_assert( event_matches("baz", "baz")); + ck_assert( event_matches("Quux", "quuX")); +} +END_TEST + +void assert_munge_line_internally(char *input, int lineno, char *greeting, char *event, char *expected_output) { + stralloc input_sa = {0}; stralloc_copys(&input_sa, input); + stralloc greeting_sa = {0}; stralloc_copys(&greeting_sa, greeting); + munge_line_internally(&input_sa,lineno,&greeting_sa,event,0,0); + stralloc_0(&input_sa); + ck_assert_str_eq(input_sa.s, expected_output); +} + +START_TEST (test_munge_line_internally) { + assert_munge_line_internally("250 word up, kids", 0, "yo.sup.local", "word", "250 word up, kids"); + assert_munge_line_internally("250-applesauce", 0, "yo.sup.local", "ehlo", "250 yo.sup.local"); + assert_munge_line_internally("250-STARTSOMETHING", 1, "yo.sup.local", "ehlo", "250-STARTSOMETHING"); + assert_munge_line_internally("250 ENDSOMETHING", 2, "yo.sup.local", "ehlo", "250 ENDSOMETHING"); + assert_munge_line_internally("250 applesauce", 0, "yo.sup.local", "helo", "250 yo.sup.local"); + assert_munge_line_internally("214 ask your grandmother\r\n", 0, "yo.sup.local", "help", "214 fixsmtpio home page: https://schmonz.com/qmail/acceptutils\r\n214 ask your grandmother\r\n"); + assert_munge_line_internally("221 get outta here", 0, "yo.sup.local", "quit", "221 yo.sup.local"); +} +END_TEST + +TCase *tc_munge(void) { + TCase *tc = tcase_create(""); + + tcase_add_test(tc, test_change_every_line_fourth_char_to_dash); + tcase_add_test(tc, test_change_last_line_fourth_char_to_space); + tcase_add_test(tc, test_event_matches); + tcase_add_test(tc, test_munge_line_internally); + + return tc; +} diff --git test_fixsmtpio_proxy.c test_fixsmtpio_proxy.c new file mode 100644 index 0000000..76c766c --- /dev/null +++ test_fixsmtpio_proxy.c @@ -0,0 +1,216 @@ +#include "check.h" +#include "acceptutils_stralloc.h" + +#include "fixsmtpio_proxy.h" + +void assert_strip_last_eol(const char *input, const char *expected_output) { + stralloc sa = {0}; stralloc_copys(&sa, input); + + strip_last_eol(&sa); + + ck_assert_int_eq(sa.len, strlen(expected_output)); + stralloc_0(&sa); + ck_assert_str_eq(sa.s, expected_output); +} + +START_TEST (test_strip_last_eol) +{ + assert_strip_last_eol("", ""); + assert_strip_last_eol("\n", ""); + assert_strip_last_eol("\r", ""); + assert_strip_last_eol("\r\n", ""); + assert_strip_last_eol("\n\r", "\n"); + assert_strip_last_eol("\r\r", "\r"); + assert_strip_last_eol("\n\n", "\n"); + assert_strip_last_eol("yo geeps", "yo geeps"); + assert_strip_last_eol("yo geeps\r\n", "yo geeps"); + assert_strip_last_eol("yo geeps\r\nhow you doin?\r\n", "yo geeps\r\nhow you doin?"); + assert_strip_last_eol("yo geeps\r\nhow you doin?", "yo geeps\r\nhow you doin?"); +} +END_TEST + +void assert_ends_with_newline(char *input, int expected) { + stralloc sa = {0}; stralloc_copys(&sa, input); + + int actual = ends_with_newline(&sa); + + ck_assert_int_eq(actual, expected); +} + +START_TEST (test_ends_with_newline) +{ + // annoying to test, currently don't believe I have this bug: + // assert_ends_with_newline(NULL, 0); + assert_ends_with_newline("", 0); + assert_ends_with_newline("123", 0); + assert_ends_with_newline("123\n", 1); + assert_ends_with_newline("1\n23\n", 1); +} +END_TEST + +void assert_is_last_line_of_response(const char *input, int expected) +{ + stralloc sa = {0}; stralloc_copys(&sa, input); + int actual = is_last_line_of_response(&sa); + ck_assert_int_eq(actual, expected); +} + +START_TEST (test_is_last_line_of_response) +{ + //assert_is_last_line_of_response(NULL, 0); + assert_is_last_line_of_response("", 0); + assert_is_last_line_of_response("123", 0); + assert_is_last_line_of_response("1234", 0); + assert_is_last_line_of_response("123 this is a final line", 1); + assert_is_last_line_of_response("123-this is NOT a final line", 0); + assert_is_last_line_of_response("777-is not\r\n", 0); + assert_is_last_line_of_response("777 is\r\n", 1); + + + // two surprises, but maybe fine for this function's job: + // - "\r\n" can be un-present and it's fine + // - it can have nothing after the space and it's fine + assert_is_last_line_of_response("123 ", 1); + assert_is_last_line_of_response("123\n", 0); +} +END_TEST + +void assert_parse_client_request(const char *request, const char *verb, const char *arg) +{ + stralloc sa_request = {0}; stralloc_copys(&sa_request, request); + stralloc sa_request_copy = {0}; stralloc_copy(&sa_request_copy, &sa_request); + stralloc sa_verb = {0}; + stralloc sa_arg = {0}; + + parse_client_request(&sa_verb, &sa_arg, &sa_request); + + ck_assert_int_eq(sa_request_copy.len, sa_request.len); + stralloc_0(&sa_verb); + ck_assert_str_eq(sa_verb.s, verb); + stralloc_0(&sa_arg); + ck_assert_str_eq(sa_arg.s, arg); +} + +START_TEST (test_parse_client_request) +{ + //assert_parse_client_request(NULL, "", ""); + assert_parse_client_request("", "", ""); + assert_parse_client_request("MAIL FROM:\r\n", "MAIL", "FROM:"); + assert_parse_client_request("RCPT TO:\r\n", "RCPT", "TO:"); + assert_parse_client_request("GENIUSPROGRAMMER\r\n", "GENIUSPROGRAMMER", ""); + assert_parse_client_request(" NEATO\r\n", "", "NEATO"); + assert_parse_client_request("SWELL \r\n", "SWELL", ""); + assert_parse_client_request(" \r\n", "", ""); + assert_parse_client_request(" \r\n", "", " "); + assert_parse_client_request("SUPER WEIRD STUFF\r\n", "SUPER", "WEIRD STUFF"); + assert_parse_client_request("R WEIRD STUFF\r\n", "R", "WEIRD STUFF"); + assert_parse_client_request("MAIL FROM:\r\nRCPT TO:\r\n", "MAIL", "FROM:\r\nRCPT TO:"); +} +END_TEST + +static void assert_get_one_response(char *input, const char *expected_result, const char *expected_remaining, int expected_return) { + stralloc actual_one = {0}, actual_many = {0}; + int return_value; + copys(&actual_many,input); + + return_value = get_one_response(&actual_one,&actual_many); + + ck_assert_int_eq(return_value, expected_return); + + stralloc_0(&actual_one); + ck_assert_str_eq(actual_one.s, expected_result); + + stralloc_0(&actual_many); + ck_assert_str_eq(actual_many.s, expected_remaining); +} + +START_TEST (test_get_one_response) +{ + assert_get_one_response("777 oneline\r\n", "777 oneline\r\n", "", 1); + assert_get_one_response("777 separate\r\n888 responses\r\n", "777 separate\r\n", "888 responses\r\n", 1); + assert_get_one_response("777-two\r\n777 lines\r\n888 three\r\n", "777-two\r\n777 lines\r\n", "888 three\r\n", 1); + assert_get_one_response("777-two\r\n777 lines\r\n888 three\r\n999 four\r\n", "777-two\r\n777 lines\r\n", "888 three\r\n999 four\r\n", 1); + assert_get_one_response("777-two\r\n", "", "777-two\r\n", 0); +} +END_TEST + +static void assert_ends_data(const char *input, const int expected) { + stralloc input_sa = {0}; stralloc_copys(&input_sa, input); + + int actual = ends_data(&input_sa); + + ck_assert_int_eq(actual, expected); +} + +START_TEST (test_ends_data) +{ + // annoying to test, currently don't believe I have this bug: + // assert_is_last_line_of_data(NULL, 0); + assert_ends_data("", 0); + assert_ends_data("\r\n", 0); + assert_ends_data(" \r\n", 0); + assert_ends_data(".\r\n", 1); + assert_ends_data(" .\r\n", 0); + assert_ends_data("\n.\r\n", 1); + assert_ends_data("\r\n.\r\n", 1); + assert_ends_data("snorf.\r\n", 0); + assert_ends_data("snorf.\r\n.\r\n", 1); +} +END_TEST + +START_TEST (test_construct_proxy_request) +{ + stralloc proxy_request = {0}, + arg = {0}, + client_request = {0}; + int want_data = 0; + + filter_rule test_rule = { + 0, + "SPECIFIC_ENV_VAR", "specific_verb", + "prepend me: ", "*", + EXIT_LATER_NORMALLY, "337 hello friend", + }; + filter_rule *test_rules = prepend_rule(0, &test_rule); + + blank(&proxy_request); copys(&client_request, "SPECIFIC_VERB somearg\r\n"); + construct_proxy_request(&proxy_request,test_rules,"SPECIFIC_VERB",&arg,&client_request,0,(void *)0,0,&want_data); + stralloc_0(&proxy_request); stralloc_0(&client_request); + ck_assert_str_eq(proxy_request.s, client_request.s); + + env_put2("SPECIFIC_ENV_VAR",""); + + blank(&proxy_request); copys(&client_request, "SPECIFIC_VERB somearg\r\n"); + construct_proxy_request(&proxy_request,test_rules,"SPECIFIC_VERB",&arg,&client_request,0,(void *)0,0,&want_data); + stralloc_0(&proxy_request); stralloc_0(&client_request); + ck_assert_str_ne(proxy_request.s, client_request.s); + + env_unset("SPECIFIC_ENV_VAR"); + + // XXX not in_data, a rule says to prepend: proxy request is equal to prepended + client_request + // XXX not in_data, two rules prepend: proxy request is equal to p1 + p2 + client_request (WILL FAIL) +} +END_TEST + +START_TEST (test_construct_proxy_response) +{ + ck_assert(1); + // not sure whether to test: + // not want_data, not in_data, no request_received, no verb: it's a timeout +} +END_TEST + +TCase *tc_proxy(void) { + TCase *tc = tcase_create(""); + + tcase_add_test(tc, test_strip_last_eol); + tcase_add_test(tc, test_ends_with_newline); + tcase_add_test(tc, test_is_last_line_of_response); + tcase_add_test(tc, test_parse_client_request); + tcase_add_test(tc, test_get_one_response); + tcase_add_test(tc, test_ends_data); + tcase_add_test(tc, test_construct_proxy_request); + tcase_add_test(tc, test_construct_proxy_response); + + return tc; +} diff --git tryblist.c tryblist.c new file mode 100644 index 0000000..b48fd34 --- /dev/null +++ tryblist.c @@ -0,0 +1,6 @@ +#include + +void main() +{ + struct blacklist *blstate = blacklist_open(); +}