diff options
Diffstat (limited to 'src/global/smtp_stream.c')
-rw-r--r-- | src/global/smtp_stream.c | 562 |
1 files changed, 562 insertions, 0 deletions
diff --git a/src/global/smtp_stream.c b/src/global/smtp_stream.c new file mode 100644 index 0000000..6629508 --- /dev/null +++ b/src/global/smtp_stream.c @@ -0,0 +1,562 @@ +/*++ +/* NAME +/* smtp_stream 3 +/* SUMMARY +/* smtp stream I/O support +/* SYNOPSIS +/* #include <smtp_stream.h> +/* +/* void smtp_stream_setup(stream, timeout, enable_deadline, +/* min_data_rate) +/* VSTREAM *stream; +/* int timeout; +/* int enable_deadline; +/* int min_data_rate; +/* +/* void smtp_printf(stream, format, ...) +/* VSTREAM *stream; +/* const char *format; +/* +/* void smtp_flush(stream) +/* VSTREAM *stream; +/* +/* int smtp_fgetc(stream) +/* VSTREAM *stream; +/* +/* int smtp_get(vp, stream, maxlen, flags) +/* VSTRING *vp; +/* VSTREAM *stream; +/* ssize_t maxlen; +/* int flags; +/* +/* void smtp_fputs(str, len, stream) +/* const char *str; +/* ssize_t len; +/* VSTREAM *stream; +/* +/* void smtp_fwrite(str, len, stream) +/* const char *str; +/* ssize_t len; +/* VSTREAM *stream; +/* +/* void smtp_fread_buf(vp, len, stream) +/* VSTRING *vp; +/* ssize_t len; +/* VSTREAM *stream; +/* +/* void smtp_fputc(ch, stream) +/* int ch; +/* VSTREAM *stream; +/* +/* void smtp_vprintf(stream, format, ap) +/* VSTREAM *stream; +/* char *format; +/* va_list ap; +/* +/* int smtp_detect_bare_lf; +/* int smtp_got_bare_lf; +/* AUXILIARY API +/* int smtp_get_noexcept(vp, stream, maxlen, flags) +/* VSTRING *vp; +/* VSTREAM *stream; +/* ssize_t maxlen; +/* int flags; +/* LEGACY API +/* void smtp_timeout_setup(stream, timeout) +/* VSTREAM *stream; +/* int timeout; +/* DESCRIPTION +/* This module reads and writes text records delimited by CR LF, +/* with error detection: timeouts or unexpected end-of-file. +/* A trailing CR LF is added upon writing and removed upon reading. +/* +/* smtp_stream_setup() prepares the specified stream for SMTP read +/* and write operations described below. +/* This routine alters the behavior of streams as follows: +/* .IP \(bu +/* When enable_deadline is non-zero, then the timeout argument +/* specifies a deadline for the total amount time that may be +/* spent in all subsequent read/write operations. +/* Otherwise, the stream is configured to enforce +/* a time limit for each individual read/write system call. +/* .IP \f(bu +/* Additionally, when min_data_rate is > 0, the deadline is +/* incremented by 1/min_data_rate seconds for every min_data_rate +/* bytes transferred. However, the deadline will never exceed +/* the value specified with the timeout argument. +/* .IP \f(bu +/* The stream is configured to use double buffering. +/* .IP \f(bu +/* The stream is configured to enable exception handling. +/* .PP +/* smtp_printf() formats its arguments and writes the result to +/* the named stream, followed by a CR LF pair. The stream is NOT flushed. +/* Long lines of text are not broken. +/* +/* smtp_flush() flushes the named stream. +/* +/* smtp_fgetc() reads one character from the named stream. +/* +/* smtp_get() reads the named stream up to and including +/* the next LF character and strips the trailing CR LF. The +/* \fImaxlen\fR argument limits the length of a line of text, +/* and protects the program against running out of memory. +/* Specify a zero bound to turn off bounds checking. +/* The result is the last character read, or VSTREAM_EOF. +/* The \fIflags\fR argument is zero or more of: +/* .RS +/* .IP SMTP_GET_FLAG_SKIP +/* Skip over input in excess of \fImaxlen\fR). Either way, a result +/* value of '\n' means that the input did not exceed \fImaxlen\fR. +/* .IP SMTP_GET_FLAG_APPEND +/* Append content to the buffer instead of overwriting it. +/* .RE +/* Specify SMTP_GET_FLAG_NONE for no special processing. +/* +/* smtp_fputs() writes its string argument to the named stream. +/* Long strings are not broken. Each string is followed by a +/* CR LF pair. The stream is not flushed. +/* +/* smtp_fwrite() writes its string argument to the named stream. +/* Long strings are not broken. No CR LF is appended. The stream +/* is not flushed. +/* +/* smtp_fread_buf() invokes vstream_fread_buf() to read the +/* specified number of unformatted bytes from the stream. The +/* result is not null-terminated. NOTE: do not skip calling +/* smtp_fread_buf() when len == 0. This function has side +/* effects including resetting the buffer write position, and +/* skipping the call would invalidate the buffer state. +/* +/* smtp_fputc() writes one character to the named stream. +/* The stream is not flushed. +/* +/* smtp_vprintf() is the machine underneath smtp_printf(). +/* +/* smtp_get_noexcept() implements the subset of smtp_get() +/* without timeouts and without making long jumps. Instead, +/* query the stream status with vstream_feof() etc. +/* +/* This function assigns smtp_got_bare_lf = smtp_detect_bare_lf, +/* if smtp_detect_bare_lf is non-zero and the last read line +/* was terminated with a bare newline. Otherwise, this function +/* sets smtp_got_bare_lf to zero. +/* +/* smtp_timeout_setup() is a backwards-compatibility interface +/* for programs that don't require deadline or data-rate support. +/* DIAGNOSTICS +/* .fi +/* .ad +/* In case of error, a vstream_longjmp() call is performed to the +/* context specified with vstream_setjmp(). +/* After write error, further writes to the socket are disabled. +/* This eliminates the need for clumsy code to avoid unwanted +/* I/O while shutting down a TLS engine or closing a VSTREAM. +/* Error codes passed along with vstream_longjmp() are: +/* .IP SMTP_ERR_EOF +/* An I/O error happened, or the peer has disconnected unexpectedly. +/* .IP SMTP_ERR_TIME +/* The time limit specified to smtp_stream_setup() was exceeded. +/* .PP +/* Additional error codes that may be used by applications: +/* .IP SMTP_ERR_QUIET +/* Perform silent cleanup; the error was already reported by +/* the application. +/* This error is never generated by the smtp_stream(3) module, but +/* is defined for application-specific use. +/* .IP SMTP_ERR_DATA +/* Application data error - the program cannot proceed with this +/* SMTP session. +/* .IP SMTP_ERR_NONE +/* A non-error code that makes setjmp()/longjmp() convenient +/* to use. +/* BUGS +/* The timeout deadline affects all I/O on the named stream, not +/* just the I/O done on behalf of this module. +/* +/* The timeout deadline overwrites any previously set up state on +/* the named stream. +/* LICENSE +/* .ad +/* .fi +/* The Secure Mailer license must be distributed with this software. +/* AUTHOR(S) +/* Wietse Venema +/* IBM T.J. Watson Research +/* P.O. Box 704 +/* Yorktown Heights, NY 10598, USA +/* +/* Wietse Venema +/* Google, Inc. +/* 111 8th Avenue +/* New York, NY 10011, USA +/*--*/ + +/* System library. */ + +#include <sys_defs.h> +#include <sys/socket.h> +#include <sys/time.h> +#include <setjmp.h> +#include <stdlib.h> +#include <stdarg.h> +#include <unistd.h> +#include <string.h> /* FD_ZERO() needs bzero() prototype */ +#include <errno.h> + +/* Utility library. */ + +#include <vstring.h> +#include <vstream.h> +#include <vstring_vstream.h> +#include <msg.h> +#include <iostuff.h> + +/* Application-specific. */ + +#include "smtp_stream.h" + + /* + * Important: the time limit feature must not introduce any system calls + * when the input is already in the buffer, or when the output still fits in + * the buffer. Such system calls would really hurt when receiving or sending + * body content one line at a time. + */ +int smtp_detect_bare_lf; +int smtp_got_bare_lf; + +/* smtp_timeout_reset - reset per-stream error flags */ + +static void smtp_timeout_reset(VSTREAM *stream) +{ + + /* + * Individual smtp_stream(3) I/O functions must not recharge the deadline + * timer, because multiline responses involve multiple smtp_stream(3) + * calls, and we really want to limit the time to send or receive a + * response. + */ + vstream_clearerr(stream); +} + +/* smtp_longjmp - raise an exception */ + +static NORETURN smtp_longjmp(VSTREAM *stream, int err, const char *context) +{ + + /* + * If we failed to write, don't bang our head against the wall another + * time when closing the stream. In the case of SMTP over TLS, poisoning + * the socket with shutdown() is more robust than purging the VSTREAM + * buffer or replacing the write function pointer with dummy_write(). + */ + if (msg_verbose) + msg_info("%s: %s", context, err == SMTP_ERR_TIME ? "timeout" : "EOF"); + if (vstream_wr_error(stream)) + /* Don't report ECONNRESET (hangup), EINVAL (already shut down), etc. */ + (void) shutdown(vstream_fileno(stream), SHUT_WR); + vstream_longjmp(stream, err); +} + +/* smtp_stream_setup - configure timeout trap */ + +void smtp_stream_setup(VSTREAM *stream, int maxtime, int enable_deadline, + int min_data_rate) +{ + const char *myname = "smtp_stream_setup"; + + if (msg_verbose) + msg_info("%s: maxtime=%d enable_deadline=%d min_data_rate=%d", + myname, maxtime, enable_deadline, min_data_rate); + + vstream_control(stream, + CA_VSTREAM_CTL_DOUBLE, + CA_VSTREAM_CTL_TIMEOUT(maxtime), + enable_deadline ? CA_VSTREAM_CTL_START_DEADLINE + : CA_VSTREAM_CTL_STOP_DEADLINE, + CA_VSTREAM_CTL_MIN_DATA_RATE(min_data_rate), + CA_VSTREAM_CTL_EXCEPT, + CA_VSTREAM_CTL_END); +} + +/* smtp_flush - flush stream */ + +void smtp_flush(VSTREAM *stream) +{ + int err; + + /* + * Do the I/O, protected against timeout. + */ + smtp_timeout_reset(stream); + err = vstream_fflush(stream); + + /* + * See if there was a problem. + */ + if (vstream_ftimeout(stream)) + smtp_longjmp(stream, SMTP_ERR_TIME, "smtp_flush"); + if (err != 0) + smtp_longjmp(stream, SMTP_ERR_EOF, "smtp_flush"); +} + +/* smtp_vprintf - write one line to SMTP peer */ + +void smtp_vprintf(VSTREAM *stream, const char *fmt, va_list ap) +{ + int err; + + /* + * Do the I/O, protected against timeout. + */ + smtp_timeout_reset(stream); + vstream_vfprintf(stream, fmt, ap); + vstream_fputs("\r\n", stream); + err = vstream_ferror(stream); + + /* + * See if there was a problem. + */ + if (vstream_ftimeout(stream)) + smtp_longjmp(stream, SMTP_ERR_TIME, "smtp_vprintf"); + if (err != 0) + smtp_longjmp(stream, SMTP_ERR_EOF, "smtp_vprintf"); +} + +/* smtp_printf - write one line to SMTP peer */ + +void smtp_printf(VSTREAM *stream, const char *fmt,...) +{ + va_list ap; + + va_start(ap, fmt); + smtp_vprintf(stream, fmt, ap); + va_end(ap); +} + +/* smtp_fgetc - read one character from SMTP peer */ + +int smtp_fgetc(VSTREAM *stream) +{ + int ch; + + /* + * Do the I/O, protected against timeout. + */ + smtp_timeout_reset(stream); + ch = VSTREAM_GETC(stream); + + /* + * See if there was a problem. + */ + if (vstream_ftimeout(stream)) + smtp_longjmp(stream, SMTP_ERR_TIME, "smtp_fgetc"); + if (vstream_feof(stream) || vstream_ferror(stream)) + smtp_longjmp(stream, SMTP_ERR_EOF, "smtp_fgetc"); + return (ch); +} + +/* smtp_get - read one line from SMTP peer */ + +int smtp_get(VSTRING *vp, VSTREAM *stream, ssize_t bound, int flags) +{ + int last_char; + + /* + * Do the I/O, protected against timeout. + */ + smtp_timeout_reset(stream); + last_char = smtp_get_noexcept(vp, stream, bound, flags); + + /* + * EOF is bad, whether or not it happens in the middle of a record. Don't + * allow data that was truncated because of EOF. + */ + if (vstream_ftimeout(stream)) + smtp_longjmp(stream, SMTP_ERR_TIME, "smtp_get"); + if (vstream_feof(stream) || vstream_ferror(stream)) + smtp_longjmp(stream, SMTP_ERR_EOF, "smtp_get"); + return (last_char); +} + +/* smtp_get_noexcept - read one line from SMTP peer, without exceptions */ + +int smtp_get_noexcept(VSTRING *vp, VSTREAM *stream, ssize_t bound, int flags) +{ + int last_char; + int next_char; + + smtp_got_bare_lf = 0; + + /* + * It's painful to do I/O with records that may span multiple buffers. + * Allow for partial long lines (we will read the remainder later) and + * allow for lines ending in bare LF. The idea is to be liberal in what + * we accept, strict in what we send. + * + * XXX 2821: Section 4.1.1.4 says that an SMTP server must not recognize + * bare LF as record terminator. + */ + last_char = (bound == 0 ? + vstring_get_flags(vp, stream, + (flags & SMTP_GET_FLAG_APPEND) ? + VSTRING_GET_FLAG_APPEND : 0) : + vstring_get_flags_bound(vp, stream, + (flags & SMTP_GET_FLAG_APPEND) ? + VSTRING_GET_FLAG_APPEND : 0, bound)); + + switch (last_char) { + + /* + * Do some repair in the rare case that we stopped reading in the + * middle of the CRLF record terminator. + */ + case '\r': + if ((next_char = VSTREAM_GETC(stream)) == '\n') { + VSTRING_ADDCH(vp, '\n'); + last_char = '\n'; + /* FALLTRHOUGH */ + } else { + if (next_char != VSTREAM_EOF) + vstream_ungetc(stream, next_char); + break; + } + + /* + * Strip off the record terminator: either CRLF or just bare LF. + * + * XXX RFC 2821 disallows sending bare CR everywhere. We remove bare CR + * if received before CRLF, and leave it alone otherwise. + */ + case '\n': + vstring_truncate(vp, VSTRING_LEN(vp) - 1); + if (smtp_detect_bare_lf) { + if (VSTRING_LEN(vp) == 0 || vstring_end(vp)[-1] != '\r') + smtp_got_bare_lf = smtp_detect_bare_lf; + else + vstring_truncate(vp, VSTRING_LEN(vp) - 1); + } else { + while (VSTRING_LEN(vp) > 0 && vstring_end(vp)[-1] == '\r') + vstring_truncate(vp, VSTRING_LEN(vp) - 1); + } + VSTRING_TERMINATE(vp); + /* FALLTRHOUGH */ + + /* + * Partial line: just read the remainder later. If we ran into EOF, + * the next test will deal with it. + */ + default: + break; + } + + /* + * Optionally, skip over excess input, protected by the same time limit. + */ + if (last_char != '\n' && (flags & SMTP_GET_FLAG_SKIP) + && vstream_feof(stream) == 0 && vstream_ferror(stream) == 0) + while ((next_char = VSTREAM_GETC(stream)) != VSTREAM_EOF + && next_char != '\n') + /* void */ ; + + return (last_char); +} + +/* smtp_fputs - write one line to SMTP peer */ + +void smtp_fputs(const char *cp, ssize_t todo, VSTREAM *stream) +{ + int err; + + if (todo < 0) + msg_panic("smtp_fputs: negative todo %ld", (long) todo); + + /* + * Do the I/O, protected against timeout. + */ + smtp_timeout_reset(stream); + err = (vstream_fwrite(stream, cp, todo) != todo + || vstream_fputs("\r\n", stream) == VSTREAM_EOF); + + /* + * See if there was a problem. + */ + if (vstream_ftimeout(stream)) + smtp_longjmp(stream, SMTP_ERR_TIME, "smtp_fputs"); + if (err != 0) + smtp_longjmp(stream, SMTP_ERR_EOF, "smtp_fputs"); +} + +/* smtp_fwrite - write one string to SMTP peer */ + +void smtp_fwrite(const char *cp, ssize_t todo, VSTREAM *stream) +{ + int err; + + if (todo < 0) + msg_panic("smtp_fwrite: negative todo %ld", (long) todo); + + /* + * Do the I/O, protected against timeout. + */ + smtp_timeout_reset(stream); + err = (vstream_fwrite(stream, cp, todo) != todo); + + /* + * See if there was a problem. + */ + if (vstream_ftimeout(stream)) + smtp_longjmp(stream, SMTP_ERR_TIME, "smtp_fwrite"); + if (err != 0) + smtp_longjmp(stream, SMTP_ERR_EOF, "smtp_fwrite"); +} + +/* smtp_fread_buf - read one buffer from SMTP peer */ + +void smtp_fread_buf(VSTRING *vp, ssize_t todo, VSTREAM *stream) +{ + int err; + + /* + * Do not return early if todo == 0. We still need the side effects from + * calling vstream_fread_buf() including resetting the buffer write + * position. Skipping the call would invalidate the buffer state. + */ + if (todo < 0) + msg_panic("smtp_fread_buf: negative todo %ld", (long) todo); + + /* + * Do the I/O, protected against timeout. + */ + smtp_timeout_reset(stream); + err = (vstream_fread_buf(stream, vp, todo) != todo); + + /* + * See if there was a problem. + */ + if (vstream_ftimeout(stream)) + smtp_longjmp(stream, SMTP_ERR_TIME, "smtp_fread"); + if (err != 0) + smtp_longjmp(stream, SMTP_ERR_EOF, "smtp_fread"); +} + +/* smtp_fputc - write to SMTP peer */ + +void smtp_fputc(int ch, VSTREAM *stream) +{ + int stat; + + /* + * Do the I/O, protected against timeout. + */ + smtp_timeout_reset(stream); + stat = VSTREAM_PUTC(ch, stream); + + /* + * See if there was a problem. + */ + if (vstream_ftimeout(stream)) + smtp_longjmp(stream, SMTP_ERR_TIME, "smtp_fputc"); + if (stat == VSTREAM_EOF) + smtp_longjmp(stream, SMTP_ERR_EOF, "smtp_fputc"); +} |