From 36a45c827713a2a5fa7c3ad1513cd011ebfa49a4 Mon Sep 17 00:00:00 2001 From: Gilles Chehade Date: Mon, 11 Mar 2019 18:19:41 +0100 Subject: move smtpscript/ to OpenSMTPD --- smtpscript/LICENSE | 15 + smtpscript/Makefile | 3 + smtpscript/Makefile.inc | 3 + smtpscript/README.md | 40 ++ smtpscript/iobuf.c | 466 +++++++++++++++++++ smtpscript/iobuf.h | 71 +++ smtpscript/parse.y | 925 ++++++++++++++++++++++++++++++++++++ smtpscript/smtpscript.c | 1009 ++++++++++++++++++++++++++++++++++++++++ smtpscript/smtpscript.h | 79 ++++ smtpscript/smtpscript/Makefile | 12 + smtpscript/ssl.c | 167 +++++++ 11 files changed, 2790 insertions(+) create mode 100644 smtpscript/LICENSE create mode 100644 smtpscript/Makefile create mode 100644 smtpscript/Makefile.inc create mode 100644 smtpscript/README.md create mode 100644 smtpscript/iobuf.c create mode 100644 smtpscript/iobuf.h create mode 100644 smtpscript/parse.y create mode 100644 smtpscript/smtpscript.c create mode 100644 smtpscript/smtpscript.h create mode 100644 smtpscript/smtpscript/Makefile create mode 100644 smtpscript/ssl.c diff --git a/smtpscript/LICENSE b/smtpscript/LICENSE new file mode 100644 index 00000000..92859014 --- /dev/null +++ b/smtpscript/LICENSE @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2013-2014 Eric Faurot + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ diff --git a/smtpscript/Makefile b/smtpscript/Makefile new file mode 100644 index 00000000..6a628f2a --- /dev/null +++ b/smtpscript/Makefile @@ -0,0 +1,3 @@ +SUBDIR+= smtpscript + +.include diff --git a/smtpscript/Makefile.inc b/smtpscript/Makefile.inc new file mode 100644 index 00000000..be93057b --- /dev/null +++ b/smtpscript/Makefile.inc @@ -0,0 +1,3 @@ +CFLAGS= -Wall -W + +BINDIR?= /usr/bin diff --git a/smtpscript/README.md b/smtpscript/README.md new file mode 100644 index 00000000..18f5fe9e --- /dev/null +++ b/smtpscript/README.md @@ -0,0 +1,40 @@ +smtpscript +========== + +smtpscript is a tool to write SMTP scenarios and easily implement regression tests for SMTP server-side implementations. + +A smtpscript will look like: + + + # this is a function init-helo that we want to call in all our regress tests + proc init-helo { + expect smtp ok + writeln "HELO regress" + expect smtp helo + } + + # each of the test-case will be called sequentially + test-case name "mailfrom.empty" { + call init-helo + writeln "MAIL FROM:<>" + expect smtp ok + } + + test-case name "mailfrom.broken" { + call init-helo + writeln "MAIL FROM:< @bleh>" + expect smtp permfail + } + + +which once executed, produces the output: + + $ smtpscript foo + ===> running test-case "mailfrom.empty" ok + ===> running test-case "mailfrom.broken" ok + ===> all run + passed: 2/2 (skipped: 0, failed: 0, error: 0) + $ + + +The scripting language also supports TLS, randomization and loops, so fairly complex scenarios can be achieved. diff --git a/smtpscript/iobuf.c b/smtpscript/iobuf.c new file mode 100644 index 00000000..7dcd45b9 --- /dev/null +++ b/smtpscript/iobuf.c @@ -0,0 +1,466 @@ +/* $OpenBSD$ */ +/* + * Copyright (c) 2012 Eric Faurot + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#ifdef IO_SSL +#include +#include +#endif + +#include "iobuf.h" + +#define IOBUF_MAX 65536 +#define IOBUFQ_MIN 4096 + +struct ioqbuf *ioqbuf_alloc(struct iobuf *, size_t); +void iobuf_drain(struct iobuf *, size_t); + +int +iobuf_init(struct iobuf *io, size_t size, size_t max) +{ + memset(io, 0, sizeof *io); + + if (max == 0) + max = IOBUF_MAX; + + if (size == 0) + size = max; + + if (size > max) + return (-1); + + if ((io->buf = malloc(size)) == NULL) + return (-1); + + io->size = size; + io->max = max; + + return (0); +} + +void +iobuf_clear(struct iobuf *io) +{ + struct ioqbuf *q; + + if (io->buf) + free(io->buf); + + while ((q = io->outq)) { + io->outq = q->next; + free(q); + } + + memset(io, 0, sizeof (*io)); +} + +void +iobuf_drain(struct iobuf *io, size_t n) +{ + struct ioqbuf *q; + size_t left = n; + + while ((q = io->outq) && left) { + if ((q->wpos - q->rpos) > left) { + q->rpos += left; + left = 0; + } else { + left -= q->wpos - q->rpos; + io->outq = q->next; + free(q); + } + } + + io->queued -= (n - left); + if (io->outq == NULL) + io->outqlast = NULL; +} + +int +iobuf_extend(struct iobuf *io, size_t n) +{ + char *t; + + if (n > io->max) + return (-1); + + if (io->max - io->size < n) + return (-1); + + t = realloc(io->buf, io->size + n); + if (t == NULL) + return (-1); + + io->size += n; + io->buf = t; + + return (0); +} + +size_t +iobuf_left(struct iobuf *io) +{ + return io->size - io->wpos; +} + +size_t +iobuf_space(struct iobuf *io) +{ + return io->size - (io->wpos - io->rpos); +} + +size_t +iobuf_len(struct iobuf *io) +{ + return io->wpos - io->rpos; +} + +char * +iobuf_data(struct iobuf *io) +{ + return io->buf + io->rpos; +} + +void +iobuf_drop(struct iobuf *io, size_t n) +{ + if (n >= iobuf_len(io)) { + io->rpos = io->wpos = 0; + return; + } + + io->rpos += n; +} + +char * +iobuf_getline(struct iobuf *iobuf, size_t *rlen) +{ + char *buf; + size_t len, i; + + buf = iobuf_data(iobuf); + len = iobuf_len(iobuf); + + for (i = 0; i + 1 <= len; i++) + if (buf[i] == '\n') { + /* Note: the returned address points into the iobuf + * buffer. We NUL-end it for convenience, and discard + * the data from the iobuf, so that the caller doesn't + * have to do it. The data remains "valid" as long + * as the iobuf does not overwrite it, that is until + * the next call to iobuf_normalize() or iobuf_extend(). + */ + iobuf_drop(iobuf, i + 1); + len = (i && buf[i - 1] == '\r') ? i - 1 : i; + buf[len] = '\0'; + if (rlen) + *rlen = len; + return (buf); + } + + return (NULL); +} + +void +iobuf_normalize(struct iobuf *io) +{ + if (io->rpos == 0) + return; + + if (io->rpos == io->wpos) { + io->rpos = io->wpos = 0; + return; + } + + memmove(io->buf, io->buf + io->rpos, io->wpos - io->rpos); + io->wpos -= io->rpos; + io->rpos = 0; +} + +ssize_t +iobuf_read(struct iobuf *io, int fd) +{ + ssize_t n; + + n = read(fd, io->buf + io->wpos, iobuf_left(io)); + if (n == -1) { + /* XXX is this really what we want? */ + if (errno == EAGAIN || errno == EINTR) + return (IOBUF_WANT_READ); + return (IOBUF_ERROR); + } + if (n == 0) + return (IOBUF_CLOSED); + + io->wpos += n; + + return (n); +} + +struct ioqbuf * +ioqbuf_alloc(struct iobuf *io, size_t len) +{ + struct ioqbuf *q; + + if (len < IOBUFQ_MIN) + len = IOBUFQ_MIN; + + if ((q = malloc(sizeof(*q) + len)) == NULL) + return (NULL); + + q->rpos = 0; + q->wpos = 0; + q->size = len; + q->next = NULL; + q->buf = (char *)(q) + sizeof(*q); + + if (io->outqlast == NULL) + io->outq = q; + else + io->outqlast->next = q; + io->outqlast = q; + + return (q); +} + +size_t +iobuf_queued(struct iobuf *io) +{ + return io->queued; +} + +void * +iobuf_reserve(struct iobuf *io, size_t len) +{ + struct ioqbuf *q; + void *r; + + if (len == 0) + return (NULL); + + if (((q = io->outqlast) == NULL) || q->size - q->wpos <= len) { + if ((q = ioqbuf_alloc(io, len)) == NULL) + return (NULL); + } + + r = q->buf + q->wpos; + q->wpos += len; + io->queued += len; + + return (r); +} + +int +iobuf_queue(struct iobuf *io, const void *data, size_t len) +{ + void *buf; + + if (len == 0) + return (0); + + if ((buf = iobuf_reserve(io, len)) == NULL) + return (-1); + + memmove(buf, data, len); + + return (0); +} + +int +iobuf_queuev(struct iobuf *io, const struct iovec *iov, int iovcnt) +{ + int i; + size_t len = 0; + char *buf; + + for (i = 0; i < iovcnt; i++) + len += iov[i].iov_len; + + if ((buf = iobuf_reserve(io, len)) == NULL) + return (-1); + + for (i = 0; i < iovcnt; i++) { + if (iov[i].iov_len == 0) + continue; + memmove(buf, iov[i].iov_base, iov[i].iov_len); + buf += iov[i].iov_len; + } + + return (0); + +} + +int +iobuf_fqueue(struct iobuf *io, const char *fmt, ...) +{ + va_list ap; + int len; + + va_start(ap, fmt); + len = iobuf_vfqueue(io, fmt, ap); + va_end(ap); + + return (len); +} + +int +iobuf_vfqueue(struct iobuf *io, const char *fmt, va_list ap) +{ + char *buf; + int len; + + len = vasprintf(&buf, fmt, ap); + + if (len == -1) + return (-1); + + len = iobuf_queue(io, buf, len); + free(buf); + + return (len); +} + +ssize_t +iobuf_write(struct iobuf *io, int fd) +{ + struct iovec iov[IOV_MAX]; + struct ioqbuf *q; + int i; + ssize_t n; + + i = 0; + for (q = io->outq; q ; q = q->next) { + if (i >= IOV_MAX) + break; + iov[i].iov_base = q->buf + q->rpos; + iov[i].iov_len = q->wpos - q->rpos; + i++; + } + + n = writev(fd, iov, i); + if (n == -1) { + if (errno == EAGAIN || errno == EINTR) + return (IOBUF_WANT_WRITE); + if (errno == EPIPE) + return (IOBUF_CLOSED); + return (IOBUF_ERROR); + } + + iobuf_drain(io, n); + + return (n); +} + +int +iobuf_flush(struct iobuf *io, int fd) +{ + ssize_t s; + + while (io->queued) + if ((s = iobuf_write(io, fd)) < 0) + return (s); + + return (0); +} + +#ifdef IO_SSL + +int +iobuf_flush_ssl(struct iobuf *io, void *ssl) +{ + ssize_t s; + + while (io->queued) + if ((s = iobuf_write_ssl(io, ssl) < 0)) + return (s); + + return (0); +} + +ssize_t +iobuf_write_ssl(struct iobuf *io, void *ssl) +{ + struct ioqbuf *q; + int r; + ssize_t n; + + q = io->outq; + n = SSL_write(ssl, q->buf + q->rpos, q->wpos - q->rpos); + if (n <= 0) { + switch ((r = SSL_get_error(ssl, n))) { + case SSL_ERROR_WANT_READ: + return (IOBUF_WANT_READ); + case SSL_ERROR_WANT_WRITE: + return (IOBUF_WANT_WRITE); + case SSL_ERROR_ZERO_RETURN: /* connection closed */ + return (IOBUF_CLOSED); + case SSL_ERROR_SYSCALL: + if (ERR_peek_last_error()) + return (IOBUF_SSLERROR); + if (r == 0) + errno = EPIPE; + return (IOBUF_ERROR); + default: + return (IOBUF_SSLERROR); + } + } + iobuf_drain(io, n); + + return (n); +} + +ssize_t +iobuf_read_ssl(struct iobuf *io, void *ssl) +{ + ssize_t n; + int r; + + n = SSL_read(ssl, io->buf + io->wpos, iobuf_left(io)); + if (n < 0) { + switch ((r = SSL_get_error(ssl, n))) { + case SSL_ERROR_WANT_READ: + return (IOBUF_WANT_READ); + case SSL_ERROR_WANT_WRITE: + return (IOBUF_WANT_WRITE); + case SSL_ERROR_SYSCALL: + if (ERR_peek_last_error()) + return (IOBUF_SSLERROR); + if (r == 0) + errno = EPIPE; + return (IOBUF_ERROR); + default: + return (IOBUF_SSLERROR); + } + } else if (n == 0) + return (IOBUF_CLOSED); + + io->wpos += n; + + return (n); +} + +#endif /* IO_SSL */ diff --git a/smtpscript/iobuf.h b/smtpscript/iobuf.h new file mode 100644 index 00000000..ee4690c8 --- /dev/null +++ b/smtpscript/iobuf.h @@ -0,0 +1,71 @@ +/* $OpenBSD$ */ +/* + * Copyright (c) 2012 Eric Faurot + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include + +#include + +struct ioqbuf { + struct ioqbuf *next; + char *buf; + size_t size; + size_t wpos; + size_t rpos; +}; + +struct iobuf { + char *buf; + size_t max; + size_t size; + size_t wpos; + size_t rpos; + + size_t queued; + struct ioqbuf *outq; + struct ioqbuf *outqlast; +}; + +#define IOBUF_WANT_READ -1 +#define IOBUF_WANT_WRITE -2 +#define IOBUF_CLOSED -3 +#define IOBUF_ERROR -4 +#define IOBUF_SSLERROR -5 + +int iobuf_init(struct iobuf *, size_t, size_t); +void iobuf_clear(struct iobuf *); + +int iobuf_extend(struct iobuf *, size_t); +void iobuf_normalize(struct iobuf *); +void iobuf_drop(struct iobuf *, size_t); +size_t iobuf_space(struct iobuf *); +size_t iobuf_len(struct iobuf *); +size_t iobuf_left(struct iobuf *); +char *iobuf_data(struct iobuf *); +char *iobuf_getline(struct iobuf *, size_t *); +ssize_t iobuf_read(struct iobuf *, int); +ssize_t iobuf_read_ssl(struct iobuf *, void *); + +size_t iobuf_queued(struct iobuf *); +void* iobuf_reserve(struct iobuf *, size_t); +int iobuf_queue(struct iobuf *, const void*, size_t); +int iobuf_queuev(struct iobuf *, const struct iovec *, int); +int iobuf_fqueue(struct iobuf *, const char *, ...); +int iobuf_vfqueue(struct iobuf *, const char *, va_list); +int iobuf_flush(struct iobuf *, int); +int iobuf_flush_ssl(struct iobuf *, void *); +ssize_t iobuf_write(struct iobuf *, int); +ssize_t iobuf_write_ssl(struct iobuf *, void *); diff --git a/smtpscript/parse.y b/smtpscript/parse.y new file mode 100644 index 00000000..10ddb90e --- /dev/null +++ b/smtpscript/parse.y @@ -0,0 +1,925 @@ +/* $OpenBSD: parse.y,v 1.109 2012/10/14 11:58:23 gilles Exp $ */ + +/* + * Copyright (c) 2012 Eric Faurot + * Copyright (c) 2008 Gilles Chehade + * Copyright (c) 2008 Pierre-Yves Ritschard + * Copyright (c) 2002, 2003, 2004 Henning Brauer + * Copyright (c) 2001 Markus Friedl. All rights reserved. + * Copyright (c) 2001 Daniel Hartmeier. All rights reserved. + * Copyright (c) 2001 Theo de Raadt. All rights reserved. + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +%{ +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "smtpscript.h" + +TAILQ_HEAD(files, file) files = TAILQ_HEAD_INITIALIZER(files); +static struct file { + TAILQ_ENTRY(file) entry; + FILE *stream; + char *name; + int lineno; + int errors; +} *file, *topfile; +struct file *pushfile(const char *, int); +int popfile(void); +int check_file_secrecy(int, const char *); +int yyparse(void); +int yylex(void); +int kw_cmp(const void *, const void *); +int lookup(char *); +int lgetc(int); +int lungetc(int); +int findeol(void); +int yyerror(const char *, ...) + __attribute__ ((format (printf, 1, 2))); + +TAILQ_HEAD(symhead, sym) symhead = TAILQ_HEAD_INITIALIZER(symhead); +struct sym { + TAILQ_ENTRY(sym) entry; + int used; + int persist; + char *nam; + char *val; +}; +int symset(const char *, const char *, int); +char *symget(const char *); + +void push_op(struct op *); +struct op *peek_op(void); +struct op *pop_op(void); + +#define MAXDEPTH 50 + +static struct op * opstack[MAXDEPTH]; +static int opstackidx; + +static int errors = 0; + +static struct script *currscript; +static struct procedure *currproc; + +int delaytonum(char *); + +typedef struct { + union { + int64_t number; + char *string; + struct op *op; + } v; + int lineno; +} YYSTYPE; + +%} + +%token INCLUDE PORT REPEAT RANDOM NOOP +%token PROC TESTCASE NAME NO_AUTOCONNECT EXPECT FAIL SKIP +%token CALL CONNECT DISCONNECT STARTTLS SLEEP WRITE WRITELN +%token SMTP OK TEMPFAIL PERMFAIL HELO +%token ERROR ARROW +%token STRING +%token NUMBER +%type quantifier port duration size +%type statement block +%% + +grammar : /* empty */ + | grammar '\n' + | grammar include '\n' + | grammar varset '\n' + | grammar proc '\n' + | grammar testcase '\n' + | grammar error '\n' { file->errors++; } + ; + +include : INCLUDE STRING { + struct file *nfile; + + if ((nfile = pushfile($2, 0)) == NULL) { + yyerror("failed to include file %s", $2); + free($2); + YYERROR; + } + free($2); + + file = nfile; + lungetc('\n'); + } + ; + +varset : STRING '=' STRING { + if (symset($1, $3, 0) == -1) + errx(1, "cannot store variable"); + free($1); + free($3); + } + ; + +optnl : '\n' optnl + | + ; + +nl : '\n' optnl + ; + +quantifier : /* empty */ { $$ = 1; } + | 's' { $$ = 1000; } + | 'm' { $$ = 60 * 1000; } + | 'h' { $$ = 3600 * 1000; } + ; + +duration : NUMBER quantifier { + if ($1 < 0) { + yyerror("invalid duration: %" PRId64, $1); + YYERROR; + } + $$ = $1 * $2; + } + ; + +size : NUMBER { + if ($1 < 0) { + yyerror("invalid size: %" PRId64, $1); + YYERROR; + } + $$ = $1; + } + | STRING { + long long result; + + if (scan_scaled($1, &result) == -1 || result < 0) { + yyerror("invalid size: %s", $1); + YYERROR; + } + free($1); + + $$ = result; + } + ; + +port : PORT STRING { + struct servent *servent; + + servent = getservbyname($2, "tcp"); + if (servent == NULL) { + yyerror("port %s is invalid", $2); + free($2); + YYERROR; + } + $$ = ntohs(servent->s_port); + free($2); + } + | PORT NUMBER { + if ($2 <= 0 || $2 >= (int)USHRT_MAX) { + yyerror("invalid port: %" PRId64, $2); + YYERROR; + } + $$ = $2; + } + | /* empty */ { + $$ = 25; + } + ; + +statement : block + | REPEAT NUMBER { push_op(NULL); } statement { + pop_op(); + $$ = op_repeat(peek_op(), $2, $4); + } + | RANDOM { push_op(NULL); } block { + pop_op(); + $$ = op_random(peek_op(), $3); + } + | CALL STRING { + struct procedure *p; + p = procedure_get_by_name(currscript, $2); + if (p == NULL) { + yyerror("call to undefined proc \"%s\"", $2); + file->errors++; + } else if (p == currproc) { + yyerror("recursive call to proc \"%s\"", $2); + file->errors++; + } else { + $$ = op_call(peek_op(), p); + } + free($2); + } + | NOOP { + $$ = op_noop(peek_op()); + } + | SLEEP duration { + $$ = op_sleep(peek_op(), $2); + } + | FAIL STRING { + $$ = op_fail(peek_op(), $2); + } + | CONNECT STRING port { + $$ = op_connect(peek_op(), $2, $3); + } + | DISCONNECT { + $$ = op_disconnect(peek_op()); + } + | STARTTLS { + $$ = op_starttls(peek_op()); + } + | WRITE STRING { + $$ = op_write(peek_op(), $2, strlen($2)); + } + | WRITELN STRING { + $$ = op_printf(peek_op(), "%s\r\n", $2); + free($2); + } + | EXPECT DISCONNECT { + $$ = op_expect_disconnect(peek_op()); + } + | EXPECT SMTP { + $$ = op_expect_smtp_response(peek_op(), + RESP_SMTP_ANY | RESP_SMTP_MULTILINE); + } + | EXPECT SMTP OK { + $$ = op_expect_smtp_response(peek_op(), + RESP_SMTP_OK); + } + | EXPECT SMTP HELO { + $$ = op_expect_smtp_response(peek_op(), + RESP_SMTP_OK | RESP_SMTP_MULTILINE); + } + | EXPECT SMTP TEMPFAIL { + $$ = op_expect_smtp_response(peek_op(), + RESP_SMTP_TEMPFAIL); + } + | EXPECT SMTP PERMFAIL { + $$ = op_expect_smtp_response(peek_op(), + RESP_SMTP_PERMFAIL); + } + ; + +statement_list : statement nl statement_list + | statement + | /* EMPTY */ + ; + +block : '{' { + push_op(op_block(peek_op())); + } optnl statement_list '}' { + $$ = pop_op(); + } + ; + +procparam : '%' STRING { + if (proc_addvar(currproc, $2) == -1) { + yyerror("cannot add parameter %s", $2); + file->errors++; + } + } + ; + +procparams : procparam procparams + | /* EMPTY */ + ; + +proc : PROC STRING { + printf("# proc %s\n", $2); + currproc = procedure_create(currscript, $2); + if (currproc == NULL) + file->errors++; + } procparams block { + if (currproc) + currproc->root = $5; + } + ; + +testopt_name : NAME STRING { + if (procedure_get_by_name(currscript, $2)) { + file->errors++; + } else { + free(currproc->name); + currproc->name = ($2); + } + } + | /* EMPTY */ + ; + +testopt_cnx : NO_AUTOCONNECT { + currproc->flags |= PROC_NOCONNECT; + } + | /* EMPTY */ + ; +testopt_fail : EXPECT FAIL { + currproc->flags |= PROC_EXPECTFAIL; + } + | /* EMPTY */ + ; + +testopt_skip : SKIP { + currproc->flags |= PROC_SKIP; + } + | /* EMPTY */ + ; + +testcaseopts : testopt_name testopt_cnx testopt_fail testopt_skip; + +testcase : TESTCASE { + char buf[1024]; + snprintf(buf, sizeof buf, "<%s:%i>", + file->name, file->lineno); + currproc = procedure_create(currscript, strdup(buf)); + if (currproc) { + currproc->flags |= PROC_TESTCASE; + } else { + file->errors++; + } + } testcaseopts block { + currproc->root = $4; + } + ; +%% + +struct keywords { + const char *k_name; + int k_val; +}; + +int +yyerror(const char *fmt, ...) +{ + va_list ap; + + file->errors++; + va_start(ap, fmt); + fprintf(stderr, "%s:%d: ", file->name, yylval.lineno); + vfprintf(stderr, fmt, ap); + fprintf(stderr, "\n"); + va_end(ap); + return (0); +} + +int +kw_cmp(const void *k, const void *e) +{ + return (strcmp(k, ((const struct keywords *)e)->k_name)); +} + +int +lookup(char *s) +{ + /* this has to be sorted always */ + static const struct keywords keywords[] = { + { "call", CALL }, + { "connect", CONNECT }, + { "disconnect", DISCONNECT }, + { "expect", EXPECT }, + { "fail", FAIL }, + { "helo", HELO }, + { "name", NAME }, + { "no-autoconnect", NO_AUTOCONNECT }, + { "noop", NOOP }, + { "ok", OK }, + { "permfail", PERMFAIL }, + { "port", PORT }, + { "proc", PROC }, + { "random", RANDOM }, + { "repeat", REPEAT }, + { "skip", SKIP }, + { "sleep", SLEEP }, + { "smtp", SMTP }, + { "starttls", STARTTLS }, + { "tempfail", TEMPFAIL }, + { "test-case", TESTCASE }, + { "write", WRITE }, + { "writeln", WRITELN }, + }; + const struct keywords *p; + + p = bsearch(s, keywords, sizeof(keywords)/sizeof(keywords[0]), + sizeof(keywords[0]), kw_cmp); + + if (p) + return (p->k_val); + else + return (STRING); +} + +#define MAXPUSHBACK 128 + +char *parsebuf; +int parseindex; +char pushback_buffer[MAXPUSHBACK]; +int pushback_index = 0; + +int +lgetc(int quotec) +{ + int c, next; + + if (parsebuf) { + /* Read character from the parsebuffer instead of input. */ + if (parseindex >= 0) { + c = parsebuf[parseindex++]; + if (c != '\0') + return (c); + parsebuf = NULL; + } else + parseindex++; + } + + if (pushback_index) + return (pushback_buffer[--pushback_index]); + + if (quotec) { + if ((c = getc(file->stream)) == EOF) { + yyerror("reached end of file while parsing " + "quoted string"); + if (file == topfile || popfile() == EOF) + return (EOF); + return (quotec); + } + return (c); + } + + while ((c = getc(file->stream)) == '\\') { + next = getc(file->stream); + if (next != '\n') { + c = next; + break; + } + yylval.lineno = file->lineno; + file->lineno++; + } + + while (c == EOF) { + if (file == topfile || popfile() == EOF) + return (EOF); + c = getc(file->stream); + } + return (c); +} + +int +lungetc(int c) +{ + if (c == EOF) + return (EOF); + if (parsebuf) { + parseindex--; + if (parseindex >= 0) + return (c); + } + if (pushback_index < MAXPUSHBACK-1) + return (pushback_buffer[pushback_index++] = c); + else + return (EOF); +} + +int +findeol(void) +{ + int c; + + parsebuf = NULL; + pushback_index = 0; + + /* skip to either EOF or the first real EOL */ + while (1) { + c = lgetc(0); + if (c == '\n') { + file->lineno++; + break; + } + if (c == EOF) + break; + } + return (ERROR); +} + +int +yylex(void) +{ + char buf[8096]; + char *p, *val; + int quotec, next, c; + int token; + +top: + p = buf; + while ((c = lgetc(0)) == ' ' || c == '\t') + ; /* nothing */ + + yylval.lineno = file->lineno; + if (c == '#') + while ((c = lgetc(0)) != '\n' && c != EOF) + ; /* nothing */ + if (c == '$' && parsebuf == NULL) { + while (1) { + if ((c = lgetc(0)) == EOF) + return (0); + + if (p + 1 >= buf + sizeof(buf) - 1) { + yyerror("string too long"); + return (findeol()); + } + if (isalnum(c) || c == '_') { + *p++ = (char)c; + continue; + } + *p = '\0'; + lungetc(c); + break; + } + val = symget(buf); + if (val == NULL) { + yyerror("macro '%s' not defined", buf); + return (findeol()); + } + parsebuf = val; + parseindex = 0; + goto top; + } + + switch (c) { + case '\'': + case '"': + quotec = c; + while (1) { + if ((c = lgetc(quotec)) == EOF) + return (0); + if (c == '\n') { + file->lineno++; + continue; + } else if (c == '\\') { + if ((next = lgetc(quotec)) == EOF) + return (0); + if (next == quotec || c == ' ' || c == '\t') + c = next; + else if (next == '\n') { + file->lineno++; + continue; + } else + lungetc(next); + } else if (c == quotec) { + *p = '\0'; + break; + } + if (p + 1 >= buf + sizeof(buf) - 1) { + yyerror("string too long"); + return (findeol()); + } + *p++ = (char)c; + } + yylval.v.string = strdup(buf); + if (yylval.v.string == NULL) + err(1, "yylex: strdup"); + return (STRING); + } + +#define allowed_to_end_number(x) \ + (isspace(x) || x == ')' || x ==',' || x == '/' || x == '}' || x == '=') + + if (c == '-' || isdigit(c)) { + do { + *p++ = c; + if ((unsigned)(p-buf) >= sizeof(buf)) { + yyerror("string too long"); + return (findeol()); + } + } while ((c = lgetc(0)) != EOF && isdigit(c)); + lungetc(c); + if (p == buf + 1 && buf[0] == '-') + goto nodigits; + if (c == EOF || allowed_to_end_number(c)) { + const char *errstr = NULL; + + *p = '\0'; + yylval.v.number = strtonum(buf, LLONG_MIN, + LLONG_MAX, &errstr); + if (errstr) { + yyerror("\"%s\" invalid number: %s", + buf, errstr); + return (findeol()); + } + return (NUMBER); + } else { +nodigits: + while (p > buf + 1) + lungetc(*--p); + c = *--p; + if (c == '-') + return (c); + } + } + + if (c == '=') { + if ((c = lgetc(0)) != EOF && c == '>') + return (ARROW); + lungetc(c); + c = '='; + } + +#define allowed_in_string(x) \ + (isalnum(x) || (ispunct(x) && x != '(' && x != ')' && \ + x != '{' && x != '}' && x != '<' && x != '>' && \ + x != '!' && x != '=' && x != '#' && \ + x != ',')) + + if (isalnum(c) || c == ':' || c == '_') { + do { + *p++ = c; + if ((unsigned)(p-buf) >= sizeof(buf)) { + yyerror("string too long"); + return (findeol()); + } + } while ((c = lgetc(0)) != EOF && (allowed_in_string(c))); + lungetc(c); + *p = '\0'; + if ((token = lookup(buf)) == STRING) + if ((yylval.v.string = strdup(buf)) == NULL) + err(1, "yylex: strdup"); + return (token); + } + if (c == '\n') { + yylval.lineno = file->lineno; + file->lineno++; + } + if (c == EOF) + return (0); + return (c); +} + +int +check_file_secrecy(int fd, const char *fname) +{ + struct stat st; + + if (fstat(fd, &st)) { + warn("cannot stat %s", fname); + return (-1); + } + if (st.st_uid != 0 && st.st_uid != getuid()) { + warnx("%s: owner not root or current user", fname); + return (-1); + } + if (st.st_mode & (S_IRWXG | S_IRWXO)) { + warnx("%s: group/world readable/writeable", fname); + return (-1); + } + return (0); +} + +struct file * +pushfile(const char *name, int secret) +{ + struct file *nfile; + + if ((nfile = calloc(1, sizeof(struct file))) == NULL) { + warn("malloc"); + return (NULL); + } + if ((nfile->name = strdup(name)) == NULL) { + warn("malloc"); + free(nfile); + return (NULL); + } + if ((nfile->stream = fopen(nfile->name, "r")) == NULL) { + warn("%s", nfile->name); + free(nfile->name); + free(nfile); + return (NULL); + } else if (secret && + check_file_secrecy(fileno(nfile->stream), nfile->name)) { + fclose(nfile->stream); + free(nfile->name); + free(nfile); + return (NULL); + } + nfile->lineno = 1; + TAILQ_INSERT_TAIL(&files, nfile, entry); + return (nfile); +} + +int +popfile(void) +{ + struct file *prev; + + if ((prev = TAILQ_PREV(file, files, entry)) != NULL) + prev->errors += file->errors; + + TAILQ_REMOVE(&files, file, entry); + fclose(file->stream); + free(file->name); + free(file); + file = prev; + return (file ? 0 : EOF); +} + +struct script * +parse_script(const char *filename) +{ + errors = 0; + + currscript = calloc(1, sizeof *currscript); + TAILQ_INIT(&currscript->procs); + currproc = NULL; + + opstackidx = 0; + + if ((file = pushfile(filename, 0)) == NULL) + return (NULL); + + topfile = file; + + /* + * parse configuration + */ + setservent(1); + yyparse(); + errors = file->errors; + popfile(); + endservent(); + + if (errors) + return (NULL); + + return (currscript); +} + +int +symset(const char *nam, const char *val, int persist) +{ + struct sym *sym; + + for (sym = TAILQ_FIRST(&symhead); sym && strcmp(nam, sym->nam); + sym = TAILQ_NEXT(sym, entry)) + ; /* nothing */ + + if (sym != NULL) { + if (sym->persist == 1) + return (0); + else { + free(sym->nam); + free(sym->val); + TAILQ_REMOVE(&symhead, sym, entry); + free(sym); + } + } + if ((sym = calloc(1, sizeof(*sym))) == NULL) + return (-1); + + sym->nam = strdup(nam); + if (sym->nam == NULL) { + free(sym); + return (-1); + } + sym->val = strdup(val); + if (sym->val == NULL) { + free(sym->nam); + free(sym); + return (-1); + } + sym->used = 0; + sym->persist = persist; + TAILQ_INSERT_TAIL(&symhead, sym, entry); + return (0); +} + +int +cmdline_symset(char *s) +{ + char *sym, *val; + int ret; + size_t len; + + if ((val = strrchr(s, '=')) == NULL) + return (-1); + + len = strlen(s) - strlen(val) + 1; + if ((sym = malloc(len)) == NULL) + errx(1, "cmdline_symset: malloc"); + + (void)strlcpy(sym, s, len); + + ret = symset(sym, val + 1, 1); + free(sym); + + return (ret); +} + +char * +symget(const char *nam) +{ + struct sym *sym; + + TAILQ_FOREACH(sym, &symhead, entry) + if (strcmp(nam, sym->nam) == 0) { + sym->used = 1; + return (sym->val); + } + return (NULL); +} + +int +delaytonum(char *str) +{ + unsigned int factor; + size_t len; + const char *errstr = NULL; + int delay; + + /* we need at least 1 digit and 1 unit */ + len = strlen(str); + if (len < 2) + goto bad; + + switch(str[len - 1]) { + + case 's': + factor = 1; + break; + + case 'm': + factor = 60; + break; + + case 'h': + factor = 60 * 60; + break; + + case 'd': + factor = 24 * 60 * 60; + break; + + default: + goto bad; + } + + str[len - 1] = '\0'; + delay = strtonum(str, 1, INT_MAX / factor, &errstr); + if (errstr) + goto bad; + + return (delay * factor); + +bad: + return (-1); +} + + +void +push_op(struct op *op) +{ + if (opstackidx == MAXDEPTH) { + yyerror("too deep"); + return; + } + opstack[opstackidx++] = op; +} + +struct op * +pop_op(void) +{ + if (opstackidx == 0) + return (NULL); + return (opstack[--opstackidx]); +} + +struct op * +peek_op(void) +{ + if (opstackidx == 0) + return (NULL); + return (opstack[opstackidx - 1]); +} diff --git a/smtpscript/smtpscript.c b/smtpscript/smtpscript.c new file mode 100644 index 00000000..a75c4249 --- /dev/null +++ b/smtpscript/smtpscript.c @@ -0,0 +1,1009 @@ +/* $OpenBSD: iobuf.h,v 1.1 2012/01/29 00:32:51 eric Exp $ */ +/* + * Copyright (c) 2012 Eric Faurot + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "iobuf.h" + +#include "smtpscript.h" + +void *ssl_connect(int); +void ssl_close(void *); + +/* XXX */ +#define SMTP_LINE_MAX 4096 + +enum { + OP_BLOCK, + OP_REPEAT, + OP_RANDOM, + + OP_NOOP, + + OP_FAIL, + OP_CALL, + OP_CONNECT, + OP_DISCONNECT, + OP_STARTTLS, + OP_SLEEP, + OP_WRITE, + + OP_EXPECT_DISCONNECT, + OP_EXPECT_SMTP_RESPONSE, +}; + +struct op { + struct op *next; + int type; + union { + struct { + int count; + struct op *start; + struct op *last; + } block; + struct { + struct op *op; + int count; + } repeat; + struct { + struct op *block; + } random; + struct { + char *reason; + } fail; + struct { + struct procedure *proc; + } call; + struct { + char *hostname; + int portno; + } connect; + struct { + unsigned int ms; + } sleep; + struct { + const void *buf; + size_t len; + } write; + struct { + int flags; + } expect_smtp; + } u; +}; + +#define RES_OK 0 +#define RES_SKIP 1 +#define RES_FAIL 2 +#define RES_ERROR 3 + +struct ctx { + int sock; + void *ssl; + struct iobuf iobuf; + int lvl; + + int result; + char *reason; +}; + +static struct op * _op_connect; + +int verbose; +int randomdelay; /* between each testcase */ +int tapout; +size_t rundelay; /* between each testcase */ + +static size_t test_pass; +static size_t test_skip; +static size_t test_fail; +static size_t test_error; +static size_t test_total = 0; + +static struct op *op_add_child(struct op *, const struct op *); +static void run_testcase(struct procedure *); +static void print_testcase(char *status, char *name, char *reason, char *directive, size_t number); +static void process_op(struct ctx *, struct op *); +static const char * parse_smtp_response(char *, size_t, char **, int *); + +struct procedure * +procedure_create(struct script *scr, char *name) +{ + struct procedure *p; + + if (procedure_get_by_name(scr, name)) { + warnx("procedure \"%s\" already exists", name); + return (NULL); + } + + p = calloc(1, sizeof *p); + TAILQ_INIT(&p->vars); + p->name = strdup(name); + + TAILQ_INSERT_TAIL(&scr->procs, p, entry); + + return (p); +} + +struct procedure * +procedure_get_by_name(struct script *scr, const char *name) +{ + struct procedure *p; + + TAILQ_FOREACH(p, &scr->procs, entry) + if (!strcmp(name, p->name)) + return (p); + + return (NULL); +} + +int +proc_getvaridx(struct procedure *proc, char *name) +{ + struct variable *v; + int n; + + n = 0; + TAILQ_FOREACH(v, &proc->vars, entry) { + if (!strcmp(name, v->name)) + return (n); + n++; + } + + return (-1); +} + +int +proc_addvar(struct procedure *proc, char *name) +{ + struct variable *v; + + printf("adding variable \"%s\"\n", name); + + if (proc_getvaridx(proc, name) != -1) + return (-1); + v = calloc(1, sizeof *v); + v->name = name; + TAILQ_INSERT_TAIL(&proc->vars, v, entry); + + return (proc->varcount++); +} + +struct op * +op_block(struct op *parent) +{ + struct op o; + + bzero(&o, sizeof o); + o.type = OP_BLOCK; + + return (op_add_child(parent, &o)); +} + +struct op * +op_repeat(struct op *parent, int count, struct op *op) +{ + struct op o; + + bzero(&o, sizeof o); + o.type = OP_REPEAT; + o.u.repeat.count = count; + o.u.repeat.op = op; + + return (op_add_child(parent, &o)); +} + +struct op * +op_random(struct op *parent, struct op *op) +{ + struct op o; + + bzero(&o, sizeof o); + o.type = OP_RANDOM; + o.u.random.block = op; + + return (op_add_child(parent, &o)); +} + +struct op * +op_noop(struct op *parent) +{ + struct op o; + + bzero(&o, sizeof o); + o.type = OP_NOOP; + + return (op_add_child(parent, &o)); +} + +struct op * +op_call(struct op *parent, struct procedure *proc) +{ + struct op o; + + bzero(&o, sizeof o); + o.type = OP_CALL; + o.u.call.proc = proc; + + return (op_add_child(parent, &o)); +} + +struct op * +op_fail(struct op *parent, char *reason) +{ + struct op o; + + bzero(&o, sizeof o); + o.type = OP_FAIL; + o.u.fail.reason = reason; + + return (op_add_child(parent, &o)); +} + +struct op * +op_connect(struct op *parent, const char *hostname, int portno) +{ + struct op o; + + bzero(&o, sizeof o); + o.type = OP_CONNECT; + o.u.connect.hostname = strdup(hostname); + o.u.connect.portno = portno; + return (op_add_child(parent, &o)); +} + +struct op * +op_disconnect(struct op *parent) +{ + struct op o; + + bzero(&o, sizeof o); + o.type = OP_DISCONNECT; + return (op_add_child(parent, &o)); +} + +struct op * +op_starttls(struct op *parent) +{ + struct op o; + + bzero(&o, sizeof o); + o.type = OP_STARTTLS; + return (op_add_child(parent, &o)); +} + +struct op * +op_sleep(struct op *parent, unsigned int ms) +{ + struct op o; + + bzero(&o, sizeof o); + o.type = OP_SLEEP; + o.u.sleep.ms = ms; + return (op_add_child(parent, &o)); +} + +struct op * +op_write(struct op *parent, const void *buf, size_t len) +{ + struct op o; + + bzero(&o, sizeof o); + o.type = OP_WRITE; + o.u.write.buf = buf; + o.u.write.len = len; + return (op_add_child(parent, &o)); +} + +struct op * +op_printf(struct op *parent, const char *fmt, ...) +{ + va_list ap; + char *buf; + int len; + + va_start(ap, fmt); + if ((len = vasprintf(&buf, fmt, ap)) == -1) + err(1, "vasprintf"); + va_end(ap); + + return op_write(parent, buf, len); +} + +struct op * +op_expect_disconnect(struct op *parent) +{ + struct op o; + + bzero(&o, sizeof o); + o.type = OP_EXPECT_DISCONNECT; + return (op_add_child(parent, &o)); +} + +struct op * +op_expect_smtp_response(struct op *parent, int flags) +{ + struct op o; + + bzero(&o, sizeof o); + o.type = OP_EXPECT_SMTP_RESPONSE; + o.u.expect_smtp.flags = flags; + return (op_add_child(parent, &o)); +} + +static void +usage(void) +{ + extern const char *__progname; + errx(1, "Usage: %s [-rvt] [-d delay] script", __progname); +} + +int +main(int argc, char **argv) +{ + struct script *s; + struct procedure *p; + int ch; + + while ((ch = getopt(argc, argv, "d:rvt")) != -1) { + switch(ch) { + case 'v': + verbose += 1; + break; + case 'd': + rundelay = atoi(optarg) * 1000; + break; + case 'r': + randomdelay = 1; + break; + case 't': + tapout = 1; + break; + default: + usage(); + /* NOTREACHED */ + } + } + argc -= optind; + argv += optind; + + if (argc != 1) + usage(); + + s = parse_script(argv[0]); + if (s == NULL) + errx(1, "error reading script file"); + + _op_connect = op_connect(NULL, "127.0.0.1", 25); + + if (tapout) { + printf("# smtpscript is an SMTP testing framework\n\n"); + printf("TAP version 13\n"); + } + + TAILQ_FOREACH(p, &s->procs, entry) + if (p->flags & PROC_TESTCASE) + run_testcase(p); + + if (tapout) + printf("1..%zu\n", test_total); + else { + printf("passed: %zu/%zu (skipped: %zu, failed: %zu, error: %zu)\n", + test_pass, + test_total, + test_skip, + test_fail, + test_error); + } + return (0); +} + +static struct op * +op_add_child(struct op *parent, const struct op *op) +{ + struct op *n; + + n = malloc(sizeof *n); + if (n == NULL) + err(1, "malloc"); + + memmove(n, op, sizeof *n); + n->next = NULL; + + /* printf("op:%p type:%i parent: %p\n", n, n->type, parent); */ + + if (parent) { + if (parent->u.block.start == NULL) + parent->u.block.start = n; + if (parent->u.block.last) + parent->u.block.last->next = n; + parent->u.block.last = n; + parent->u.block.count += 1; + } + + return (n); +} + +static void +run_testcase(struct procedure *proc) +{ + struct ctx c; + uint32_t rdelay; + + bzero(&c, sizeof c); + c.sock = -1; + c.lvl = 1; + + if (rundelay) { + if (randomdelay) + rdelay = arc4random_uniform(rundelay); + else + rdelay = rundelay; + usleep(rdelay); + } + + fflush(stdout); + + if (verbose > 1) + printf("\n"); + + if (!(proc->flags & PROC_NOCONNECT)) + process_op(&c, _op_connect); + process_op(&c, proc->root); + + if (c.sock != -1) + close(c.sock); + if (c.ssl) + ssl_close(c.ssl); + iobuf_clear(&c.iobuf); + + if (verbose > 1) { + printf("# Done with test-case \"%s\": ", proc->name); + } + + switch (c.result) { + case RES_OK: + test_total += 1; + if (proc->flags & PROC_EXPECTFAIL) { + print_testcase("not ok", proc->name, c.reason, "TODO", test_total); // XPass + test_fail += 1; + } else if (proc->flags & PROC_SKIP) { + test_skip += 1; + print_testcase("ok", proc->name, c.reason, "SKIP", test_total); + } + else { + print_testcase("ok", proc->name, c.reason, NULL, test_total); + test_pass += 1; + } + + break; + + case RES_SKIP: + test_skip += 1; + test_total += 1; + print_testcase("not ok", proc->name, c.reason, "SKIP", test_total); + break; + + case RES_FAIL: + test_total += 1; + if (proc->flags & PROC_EXPECTFAIL) { + print_testcase("not ok", proc->name, c.reason, "TODO", test_total); // XFail + test_pass += 1; + } else if (proc->flags & PROC_SKIP) { + test_skip += 1; + print_testcase("ok", proc->name, c.reason, "SKIP", test_total); + } + else { + print_testcase("not ok", proc->name, c.reason, NULL, test_total); + test_fail += 1; + } + + break; + + case RES_ERROR: + test_error += 1; + test_total += 1; + print_testcase("not ok", proc->name, c.reason, NULL, test_total); + break; + } + + if (verbose > 1) { + printf("\n"); + } + +} + +void print_testcase(char *status, char *name, char *reason, char *directive, size_t number) +{ + printf("%s %zu", status, number); + if (directive) + printf(" - %s # %s\n", name, directive); + else + if (reason) + printf(" - %s # %s\n", name, reason); + else + printf(" - %s\n", name); +} + +static size_t +strvisx2(char *dst, const char *src, size_t srclen, int flag) +{ + size_t n, r, i; + + n = strvisx(dst, src, srclen, flag); + if (n == 0) + return (0); + + r = n; + for (i = n - 1; i; i--) { + if (dst[i] == '\r') { + memmove(dst + i + 2, dst + i + 1, n + 1 - i); + dst[i+1] = 'r'; + dst[i] = '\\'; + r++; + } else if (dst[i] == '"') { + memmove(dst + i + 2, dst + i + 1, n + 1 - i); + dst[i+1] = '"'; + dst[i] = '\\'; + r++; + } + } + + return (r); +} + +static const char * +show_data(const char *src, size_t len, size_t max) +{ + static char buf[8192 + 3]; + char tmp[256]; + size_t l, n; + + l = len; + if (len > 2048) + l = 2048; + + buf[0] = '"'; + n = strvisx2(&buf[1], src, l, VIS_SAFE | VIS_NL | VIS_TAB | VIS_CSTYLE); + if (n >= max) { + snprintf(tmp, sizeof tmp, "...\" [%zu]", l); + buf[max - strlen(tmp)] = '\0'; + strlcat(buf, tmp, sizeof(buf)); + } else { + strlcat(buf, "\"", sizeof(buf)); + } + + return (buf); +} + +static void +print_op(struct op *op, int lvl) +{ + + + if (op->type == OP_BLOCK) + return; + + while (lvl--) + printf(" "); + + switch(op->type) { + + case OP_REPEAT: + printf("=> repeat: %i\n", op->u.repeat.count); + break; + + case OP_RANDOM: + printf("=> random: %i\n", op->u.random.block->u.block.count); + break; + + case OP_NOOP: + printf("=> noop\n"); + break; + + case OP_FAIL: + printf("=> fail: %s\n", op->u.fail.reason); + break; + + case OP_CALL: + printf("=> call: %s\n", op->u.call.proc->name); + break; + + case OP_CONNECT: + printf("=> connect %s:%i\n", + op->u.connect.hostname, + op->u.connect.portno); + break; + + case OP_DISCONNECT: + printf("=> disconnect\n"); + break; + + case OP_STARTTLS: + printf("=> starttls\n"); + break; + + case OP_SLEEP: + printf("=> sleep %ims\n", op->u.sleep.ms); + break; + + case OP_WRITE: + printf("=> write %s\n", + show_data(op->u.write.buf, op->u.write.len, 70)); + break; + + case OP_EXPECT_DISCONNECT: + printf("<= disconnect\n"); + break; + + case OP_EXPECT_SMTP_RESPONSE: + printf("<= smtp-response 0x%04x\n", op->u.expect_smtp.flags); + break; + + default: + printf("<> ??? %i;\n", op->type); + break; + } +} + + +static void +set_failure(struct ctx *ctx, int res, const char *fmt, ...) +{ + va_list ap; + int len; + + ctx->result = res; + va_start(ap, fmt); + if ((len = vasprintf(&ctx->reason, fmt, ap)) == -1) + err(1, "vasprintf"); + va_end(ap); +} + +static void +process_op(struct ctx *ctx, struct op *op) +{ + struct addrinfo hints, *a, *ai; + struct op *o; + struct iobuf *iobuf; + int i, r, s, save_errno, cont; + const char *cause; + char buf[16], *servname, *line; + ssize_t n; + size_t len; + const char *e; + + if (verbose > 1) + print_op(op, ctx->lvl); + + iobuf = &ctx->iobuf; + + switch(op->type) { + + case OP_BLOCK: + ctx->lvl += 1; + for (o = op->u.block.start; o; o = o->next) { + process_op(ctx, o); + if (ctx->result) + break; + } + ctx->lvl -= 1; + break; + + case OP_REPEAT: + ctx->lvl += 1; + for (i = 0; i < op->u.repeat.count; i++) { + process_op(ctx, op->u.repeat.op); + if (ctx->result) + break; + } + ctx->lvl -= 1; + break; + + case OP_RANDOM: + + if (op->u.random.block->u.block.count == 0) + return; + + ctx->lvl += 1; + + i = arc4random_uniform(op->u.random.block->u.block.count); + for (o = op->u.random.block->u.block.start; i; i--, o = o->next) + ; + process_op(ctx, o); + if (ctx->result) + break; + ctx->lvl -= 1; + break; + + case OP_NOOP: + break; + + case OP_FAIL: + set_failure(ctx, RES_FAIL, op->u.fail.reason); + break; + + case OP_CALL: + process_op(ctx, op->u.call.proc->root); + break; + + case OP_CONNECT: + if (ctx->sock != -1) + close(ctx->sock); + ctx->sock = -1; + iobuf_clear(iobuf); + + servname = NULL; + if (op->u.connect.portno) { + snprintf(buf, sizeof buf, "%i", op->u.connect.portno); + servname = buf; + } + bzero(&hints, sizeof hints); + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + r = getaddrinfo(op->u.connect.hostname, servname, &hints, &ai); + if (r) { + set_failure(ctx, RES_ERROR, + "failed to connect to %s:%s: %s", + op->u.connect.hostname, servname, gai_strerror(r)); + return; + } + + s = -1; + for(a = ai; a; a = a->ai_next) { + s = socket(a->ai_family, a->ai_socktype, a->ai_protocol); + if (s == -1) { + cause = "socket"; + continue; + } + if (connect(s, a->ai_addr, a->ai_addrlen) == -1) { + cause = "connect"; + save_errno = errno; + close(s); + errno = save_errno; + s = -1; + continue; + } + break; /* okay we got one */ + } + freeaddrinfo(ai); + if (s == -1) { + set_failure(ctx, RES_ERROR, + "failed to connect to %s:%s: %s", + op->u.connect.hostname, servname, cause); + } else { + ctx->sock = s; + iobuf_init(iobuf, 0, 0); + } + break; + + case OP_DISCONNECT: + if (ctx->sock != -1) + close(ctx->sock); + ctx->sock = -1; + iobuf_clear(iobuf); + break; + + case OP_STARTTLS: + if (ctx->ssl) + set_failure(ctx, RES_ERROR, "SSL context already here"); + else if ((ctx->ssl = ssl_connect(ctx->sock)) == NULL) + set_failure(ctx, RES_ERROR, "SSL connection failed"); + break; + + case OP_SLEEP: + usleep(op->u.sleep.ms * 1000); + break; + + case OP_WRITE: + iobuf_queue(iobuf, op->u.write.buf, op->u.write.len); + if (ctx->ssl) + r = iobuf_flush_ssl(iobuf, ctx->ssl); + else + r = iobuf_flush(iobuf, ctx->sock); + switch (r) { + case 0: + break; + case IOBUF_CLOSED: + set_failure(ctx, RES_FAIL, "connection closed"); + break; + case IOBUF_WANT_WRITE: + set_failure(ctx, RES_ERROR, "iobuf_write(): WANT_WRITE"); + break; + case IOBUF_ERROR: + set_failure(ctx, RES_ERROR, "IO error"); + break; + case IOBUF_SSLERROR: + set_failure(ctx, RES_ERROR, "SSL error"); + break; + default: + set_failure(ctx, RES_ERROR, "iobuf_write(): bad value"); + break; + } + break; + + case OP_EXPECT_DISCONNECT: + if (iobuf_len(iobuf)) { + set_failure(ctx, RES_ERROR, "%zu bytes of input left", + iobuf_len(iobuf)); + break; + } + if (ctx->ssl) + n = iobuf_read_ssl(iobuf, ctx->ssl); + else + n = iobuf_read(iobuf, ctx->sock); + switch (n) { + case IOBUF_CLOSED: + close(ctx->sock); + ctx->sock = -1; + if (ctx->ssl) + ssl_close(ctx->ssl); + break; + case IOBUF_WANT_READ: + set_failure(ctx, RES_ERROR, "iobuf_read(): WANT_READ"); + break; + case IOBUF_ERROR: + set_failure(ctx, RES_ERROR, "IO error"); + break; + case IOBUF_SSLERROR: + set_failure(ctx, RES_ERROR, "SSL error"); + break; + default: + set_failure(ctx, RES_FAIL, "data read: %s", + show_data(iobuf_data(iobuf), iobuf_len(iobuf), 70)); + break; + } + break; + + case OP_EXPECT_SMTP_RESPONSE: + line = NULL; + while (1) { + line = iobuf_getline(iobuf, &len); + if (line) { + e = parse_smtp_response(line, len, NULL, &cont); + if (e) { + set_failure(ctx, RES_FAIL, e); + return; + } + if (!cont) { + iobuf_normalize(iobuf); + break; + } + if (!(op->u.expect_smtp.flags + & RESP_SMTP_MULTILINE)) { + set_failure(ctx, RES_FAIL, + "single line response expected"); + return; + } + continue; + } + + if (iobuf_len(iobuf) >= SMTP_LINE_MAX) { + set_failure(ctx, RES_FAIL, "line too long"); + return; + } + + iobuf_normalize(iobuf); + + again: + if (ctx->ssl) + n = iobuf_read_ssl(iobuf, ctx->ssl); + else + n = iobuf_read(iobuf, ctx->sock); + switch (n) { + case IOBUF_CLOSED: + set_failure(ctx, RES_FAIL, "connection closed"); + return; + case IOBUF_WANT_READ: + goto again; + case IOBUF_ERROR: + set_failure(ctx, RES_ERROR, "io error"); + return; + case IOBUF_SSLERROR: + set_failure(ctx, RES_ERROR, "SSL error"); + return; + default: + break; + } + } + + /* got our response */ + + if (verbose > 1) { + len = ctx->lvl; + while (len--) + printf(" "); + printf(" >>> %s\n", show_data(line, strlen(line), 70)); + } + + switch (line[0]) { + case '2': + case '3': + if (!(op->u.expect_smtp.flags & RESP_SMTP_OK)) + set_failure(ctx, RES_FAIL, + "unexpected response code0: %s", line); + break; + case '4': + if (!(op->u.expect_smtp.flags & RESP_SMTP_TEMPFAIL)) + set_failure(ctx, RES_FAIL, + "unexpected response code1: %s", line); + break; + case '5': + if (!(op->u.expect_smtp.flags & RESP_SMTP_PERMFAIL)) + set_failure(ctx, RES_FAIL, + "unexpected response code2: %s", line); + break; + default: + set_failure(ctx, RES_FAIL, + "unexpected response code???: %s", line); + break; + } + break; + + default: + ctx->result = RES_ERROR; + ctx->reason = "invalid operator"; + break; + } +} + +static const char * +parse_smtp_response(char *line, size_t len, char **msg, int *cont) +{ + size_t i; + + if (len >= SMTP_LINE_MAX) + return "line too long"; + + if (len > 3) { + if (msg) + *msg = line + 4; + if (cont) + *cont = (line[3] == '-'); + } else if (len == 3) { + if (msg) + *msg = line + 3; + if (cont) + *cont = 0; + } else + return "line too short"; + + /* validate reply code */ + if (line[0] < '2' || line[0] > '5' || !isdigit(line[1]) || + !isdigit(line[2])) + return "reply code out of range"; + + /* validate reply message */ + for (i = 0; i < len; i++) + if (!isprint(line[i])) + return "non-printable character in reply"; + + return NULL; +} diff --git a/smtpscript/smtpscript.h b/smtpscript/smtpscript.h new file mode 100644 index 00000000..ba90b240 --- /dev/null +++ b/smtpscript/smtpscript.h @@ -0,0 +1,79 @@ +/* $OpenBSD: iobuf.h,v 1.1 2012/01/29 00:32:51 eric Exp $ */ +/* + * Copyright (c) 2012 Eric Faurot + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +struct op; + +#define PROC_TESTCASE 0x0001 +#define PROC_SKIP 0x0002 +#define PROC_EXPECTFAIL 0x0004 +#define PROC_NOCONNECT 0x0008 + + +struct variable { + TAILQ_ENTRY(variable) entry; + char *name; +}; + +struct procedure { + int flags; + + TAILQ_ENTRY(procedure) entry; + char *name; + + TAILQ_HEAD(, variable) vars; + int varcount; + + struct op *root; + + int skip; + int expect_fail; +}; + +#define RESP_SMTP_OK 0x0001 +#define RESP_SMTP_TEMPFAIL 0x0002 +#define RESP_SMTP_PERMFAIL 0x0004 +#define RESP_SMTP_ANY 0x0007 + +#define RESP_SMTP_MULTILINE 0x0100 + +struct script { + TAILQ_HEAD(, procedure) procs; +}; + +int proc_addvar(struct procedure *, char *name); +int proc_getvaridx(struct procedure *, char *name); + +struct op *op_block(struct op *); +struct op *op_repeat(struct op *, int, struct op *); +struct op *op_random(struct op *, struct op *); +struct op *op_noop(struct op *); +struct op *op_fail(struct op *, char *); +struct op *op_call(struct op *, struct procedure *); +struct op *op_connect(struct op *, const char *, int); +struct op *op_disconnect(struct op *); +struct op *op_starttls(struct op *); +struct op *op_sleep(struct op *, unsigned int); +struct op *op_write(struct op *, const void *, size_t); +struct op *op_printf(struct op *, const char *, ...); + +struct op *op_expect_disconnect(struct op *); +struct op *op_expect_smtp_response(struct op *, int); + +struct procedure *procedure_create(struct script *, char *); +struct procedure *procedure_get_by_name(struct script *, const char *); + +struct script * parse_script(const char *); diff --git a/smtpscript/smtpscript/Makefile b/smtpscript/smtpscript/Makefile new file mode 100644 index 00000000..3679481c --- /dev/null +++ b/smtpscript/smtpscript/Makefile @@ -0,0 +1,12 @@ +.PATH: ${.CURDIR}/.. + +PROG= smtpscript +SRCS= smtpscript.c iobuf.c parse.y ssl.c +NOMAN= noman + +LDADD+= -lutil -lssl -lcrypto +DPADD+= ${LIBEVENT} ${LIBUTIL} ${LIBSSL} ${LIBCRYPTO} +CPPFLAGS+= -I${.CURDIR}/.. +CPPFLAGS+= -DIO_SSL + +.include diff --git a/smtpscript/ssl.c b/smtpscript/ssl.c new file mode 100644 index 00000000..54f0993d --- /dev/null +++ b/smtpscript/ssl.c @@ -0,0 +1,167 @@ +/* $OpenBSD: ssl.c,v 1.50 2012/11/12 14:58:53 eric Exp $ */ + +/* + * Copyright (c) 2008 Pierre-Yves Ritschard + * Copyright (c) 2008 Reyk Floeter + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#define SSL_CIPHERS "HIGH" + +void ssl_error(const char *); + +static void ssl_init(void); +static SSL_CTX *ssl_ctx_create(void); +static void *ssl_client_ctx(void); + +static void +ssl_init(void) +{ + static int init = 0; + + if (init) + return; + + init = 1; + + SSL_library_init(); + SSL_load_error_strings(); + + OpenSSL_add_all_algorithms(); + + /* Init hardware crypto engines. */ + ENGINE_load_builtin_engines(); + ENGINE_register_all_complete(); +} + +void +ssl_error(const char *where) +{ + unsigned long code; + char errbuf[128]; + + for (; (code = ERR_get_error()) != 0 ;) { + ERR_error_string_n(code, errbuf, sizeof(errbuf)); + fprintf(stderr, "debug: SSL library error: %s: %s", + where, errbuf); + } +} + +void * +ssl_connect(int sock) +{ + SSL *ssl; + + ssl = ssl_client_ctx(); + + if (SSL_set_fd(ssl, sock) == 0) { + ssl_error("ssl_connect:SSL_set_fd"); + SSL_free(ssl); + return (NULL); + } + + if (SSL_connect(ssl) != 1) { + ssl_error("ssl_connect:SSL_connect"); + SSL_free(ssl); + return (NULL); + } + + return ((void*)ssl); +} + +void +ssl_close(void *a) +{ + SSL *ssl = a; + + SSL_free(ssl); +} + +static SSL_CTX * +ssl_ctx_create(void) +{ + SSL_CTX *ctx; + + ssl_init(); + + ctx = SSL_CTX_new(SSLv23_method()); + if (ctx == NULL) { + ssl_error("ssl_ctx_create"); + errx(1, "ssl_ctx_create: could not create SSL context"); + } + + SSL_CTX_set_session_cache_mode(ctx, SSL_SESS_CACHE_OFF); + SSL_CTX_set_timeout(ctx, 30); + SSL_CTX_set_options(ctx, + SSL_OP_ALL | SSL_OP_NO_SSLv2 | SSL_OP_NO_TICKET); + SSL_CTX_set_options(ctx, + SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION); + + if (!SSL_CTX_set_cipher_list(ctx, SSL_CIPHERS)) { + ssl_error("ssl_ctx_create"); + errx(1, "ssl_ctx_create: could not set cipher list"); + } + + return (ctx); +} + +static void * +ssl_client_ctx(void) +{ + SSL_CTX *ctx; + SSL *ssl = NULL; + int rv = -1; + + ctx = ssl_ctx_create(); + + if ((ssl = SSL_new(ctx)) == NULL) + goto done; + SSL_CTX_free(ctx); + + if (!SSL_set_ssl_method(ssl, SSLv23_client_method())) + goto done; + + rv = 0; +done: + if (rv) { + if (ssl) + SSL_free(ssl); + else if (ctx) + SSL_CTX_free(ctx); + ssl = NULL; + } + return (void*)(ssl); +} -- cgit v1.2.3-59-g8ed1b