/* $NetBSD: smtp_stream.c,v 1.2 2017/02/14 01:16:45 christos Exp $ */
/*++
/* NAME
/* smtp_stream 3
/* SUMMARY
/* smtp stream I/O support
/* SYNOPSIS
/* #include <smtp_stream.h>
/*
/* void smtp_stream_setup(stream, timeout, enable_deadline)
/* VSTREAM *stream;
/* int timeout;
/* int enable_deadline;
/*
/* 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_fputc(ch, stream)
/* int ch;
/* VSTREAM *stream;
/*
/* void smtp_vprintf(stream, format, ap)
/* VSTREAM *stream;
/* char *format;
/* va_list ap;
/* LEGACY API
/* void smtp_timeout_setup(stream, timeout)
/* VSTREAM *stream;
/* int timeout;
/* int enable_deadline;
/* 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, the stream is configured
/* to enforce a total time limit for each smtp_stream read/write
/* operation. Otherwise, the stream is configured to enforce
/* a time limit for each individual read/write system call.
/* .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 either SMTP_GET_FLAG_NONE (no
/* special processing) or 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.
/*
/* 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_fputc() writes one character to the named stream.
/* The stream is not flushed.
/*
/* smtp_vprintf() is the machine underneath smtp_printf().
/*
/* smtp_timeout_setup() is a backwards-compatibility interface
/* for programs that don't require per-record deadline 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
/*--*/
/* 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"
/* smtp_timeout_reset - reset per-stream error flags, restart deadline timer */
static void smtp_timeout_reset(VSTREAM *stream)
{
vstream_clearerr(stream);
/*
* 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.
*/
if (vstream_fstat(stream, VSTREAM_FLAG_DEADLINE))
vstream_control(stream, CA_VSTREAM_CTL_START_DEADLINE, CA_VSTREAM_CTL_END);
}
/* 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)
{
const char *myname = "smtp_stream_setup";
if (msg_verbose)
msg_info("%s: maxtime=%d enable_deadline=%d",
myname, maxtime, enable_deadline);
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_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;
int next_char;
/*
* 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.
*/
smtp_timeout_reset(stream);
last_char = (bound == 0 ? vstring_get(vp, stream) :
vstring_get_bound(vp, stream, 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);
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 */ ;
/*
* 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_fputs - write one line to SMTP peer */
void smtp_fputs(const char *cp, ssize_t todo, VSTREAM *stream)
{
ssize_t 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)
{
ssize_t 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_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");
}