diff options
Diffstat (limited to 'src/lib-smtp/smtp-reply-parser.c')
-rw-r--r-- | src/lib-smtp/smtp-reply-parser.c | 657 |
1 files changed, 657 insertions, 0 deletions
diff --git a/src/lib-smtp/smtp-reply-parser.c b/src/lib-smtp/smtp-reply-parser.c new file mode 100644 index 0000000..34dc27d --- /dev/null +++ b/src/lib-smtp/smtp-reply-parser.c @@ -0,0 +1,657 @@ +/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "array.h" +#include "str.h" +#include "strfuncs.h" +#include "istream.h" +#include "smtp-parser.h" + +#include "smtp-reply-parser.h" + +#include <ctype.h> + +/* From RFC 5321: + + Reply-line = *( Reply-code "-" [ textstring ] CRLF ) + Reply-code [ SP textstring ] CRLF + Reply-code = %x32-35 %x30-35 %x30-39 + textstring = 1*(%d09 / %d32-126) ; HT, SP, Printable US-ASCII + + Greeting = ( "220 " (Domain / address-literal) + [ SP textstring ] CRLF ) / + ( "220-" (Domain / address-literal) + [ SP textstring ] CRLF + *( "220-" [ textstring ] CRLF ) + "220" [ SP textstring ] CRLF ) + + ehlo-ok-rsp = ( "250" SP Domain [ SP ehlo-greet ] CRLF ) + / ( "250-" Domain [ SP ehlo-greet ] CRLF + *( "250-" ehlo-line CRLF ) + "250" SP ehlo-line CRLF ) + ehlo-greet = 1*(%d0-9 / %d11-12 / %d14-127) + ; string of any characters other than CR or LF + ehlo-line = ehlo-keyword *( SP ehlo-param ) + ehlo-keyword = (ALPHA / DIGIT) *(ALPHA / DIGIT / "-") + ; additional syntax of ehlo-params depends on + ; ehlo-keyword + ehlo-param = 1*(%d33-126) + ; any CHAR excluding <SP> and all + ; control characters (US-ASCII 0-31 and 127 + ; inclusive) + + From RFC 2034: + + status-code ::= class "." subject "." detail + class ::= "2" / "4" / "5" + subject ::= 1*3digit + detail ::= 1*3digit + */ + +enum smtp_reply_parser_state { + SMTP_REPLY_PARSE_STATE_INIT = 0, + SMTP_REPLY_PARSE_STATE_CODE, + SMTP_REPLY_PARSE_STATE_SEP, + SMTP_REPLY_PARSE_STATE_TEXT, + SMTP_REPLY_PARSE_STATE_EHLO_SPACE, + SMTP_REPLY_PARSE_STATE_EHLO_GREET, + SMTP_REPLY_PARSE_STATE_CR, + SMTP_REPLY_PARSE_STATE_CRLF, + SMTP_REPLY_PARSE_STATE_LF +}; + +struct smtp_reply_parser_state_data { + enum smtp_reply_parser_state state; + unsigned int line; + + struct smtp_reply *reply; + ARRAY_TYPE(const_string) reply_lines; + size_t reply_size; + + bool last_line:1; +}; + +struct smtp_reply_parser { + struct istream *input; + + size_t max_reply_size; + + const unsigned char *begin, *cur, *end; + + string_t *strbuf; + + struct smtp_reply_parser_state_data state; + pool_t reply_pool; + + char *error; + + bool enhanced_codes:1; + bool ehlo:1; +}; + +bool smtp_reply_parse_enhanced_code(const char *text, + struct smtp_reply_enhanced_code *enh_code_r, + const char **pos_r) +{ + const char *p = text; + unsigned int digits, x, y, z; + + i_zero(enh_code_r); + + /* status-code ::= class "." subject "." detail + class ::= "2" / "4" / "5" + subject ::= 1*3digit + detail ::= 1*3digit + */ + + /* class */ + if (p[1] != '.' || (p[0] != '2' && p[0] != '4' && p[0] != '5')) + return FALSE; + x = p[0] - '0'; + p += 2; + + /* subject */ + digits = 0; + y = 0; + while (*p != '\0' && i_isdigit(*p) && digits++ < 3) { + y = y*10 + (*p - '0'); + p++; + } + if (digits == 0 || *p != '.') + return FALSE; + p++; + + /* detail */ + digits = 0; + z = 0; + while (*p != '\0' && i_isdigit(*p) && digits++ < 3) { + z = z*10 + (*p - '0'); + p++; + } + if (digits == 0 || (pos_r == NULL && *p != '\0')) + return FALSE; + + if (pos_r != NULL) { + /* code is syntactically valid; strip code from textstring */ + *pos_r = p; + } + + enh_code_r->x = x; + enh_code_r->y = y; + enh_code_r->z = z; + return TRUE; +} + +static inline void ATTR_FORMAT(2, 3) +smtp_reply_parser_error(struct smtp_reply_parser *parser, + const char *format, ...) +{ + va_list args; + + i_free(parser->error); + + va_start(args, format); + parser->error = i_strdup_vprintf(format, args); + va_end(args); +} + +struct smtp_reply_parser * +smtp_reply_parser_init(struct istream *input, size_t max_reply_size) +{ + struct smtp_reply_parser *parser; + + parser = i_new(struct smtp_reply_parser, 1); + parser->max_reply_size = + (max_reply_size > 0 ? max_reply_size : SIZE_MAX); + parser->input = input; + i_stream_ref(input); + parser->strbuf = str_new(default_pool, 128); + return parser; +} + +void smtp_reply_parser_deinit(struct smtp_reply_parser **_parser) +{ + struct smtp_reply_parser *parser = *_parser; + + *_parser = NULL; + + str_free(&parser->strbuf); + pool_unref(&parser->reply_pool); + i_stream_unref(&parser->input); + i_free(parser->error); + i_free(parser); +} + +void smtp_reply_parser_set_stream(struct smtp_reply_parser *parser, + struct istream *input) +{ + i_stream_unref(&parser->input); + if (input != NULL) { + parser->input = input; + i_stream_ref(parser->input); + } +} + +static void +smtp_reply_parser_restart(struct smtp_reply_parser *parser) +{ + str_truncate(parser->strbuf, 0); + pool_unref(&parser->reply_pool); + i_zero(&parser->state); + + parser->reply_pool = pool_alloconly_create("smtp_reply", 1024); + parser->state.reply = p_new(parser->reply_pool, struct smtp_reply, 1); + p_array_init(&parser->state.reply_lines, parser->reply_pool, 8); + +} + +static int smtp_reply_parse_code +(struct smtp_reply_parser *parser, unsigned int *code_r) +{ + const unsigned char *first = parser->cur; + const unsigned char *p; + + /* Reply-code = %x32-35 %x30-35 %x30-39 + */ + while (parser->cur < parser->end && i_isdigit(*parser->cur)) + parser->cur++; + + if (str_len(parser->strbuf) + (parser->cur-first) > 3) + return -1; + + str_append_data(parser->strbuf, first, parser->cur - first); + if (parser->cur == parser->end) + return 0; + if (str_len(parser->strbuf) != 3) + return -1; + p = str_data(parser->strbuf); + if (p[0] < '2' || p[0] > '5' || p[1] > '5') + return -1; + *code_r = (p[0] - '0')*100 + (p[1] - '0')*10 + (p[2] - '0'); + str_truncate(parser->strbuf, 0); + return 1; +} + +static int smtp_reply_parse_textstring(struct smtp_reply_parser *parser) +{ + const unsigned char *first = parser->cur; + + /* textstring = 1*(%d09 / %d32-126) ; HT, SP, Printable US-ASCII + */ + while (parser->cur < parser->end && smtp_char_is_textstr(*parser->cur)) + parser->cur++; + + if (((parser->cur-first) + parser->state.reply_size + + str_len(parser->strbuf)) > parser->max_reply_size) { + smtp_reply_parser_error(parser, + "Reply exceeds size limit"); + return -1; + } + + str_append_data(parser->strbuf, first, parser->cur - first); + if (parser->cur == parser->end) + return 0; + return 1; +} + +static int smtp_reply_parse_ehlo_domain(struct smtp_reply_parser *parser) +{ + const unsigned char *first = parser->cur; + + /* Domain [ SP ... + */ + while (parser->cur < parser->end && *parser->cur != ' ' && + smtp_char_is_textstr(*parser->cur)) + parser->cur++; + + if (((parser->cur-first) + parser->state.reply_size + + str_len(parser->strbuf)) > parser->max_reply_size) { + smtp_reply_parser_error(parser, + "Reply exceeds size limit"); + return -1; + } + str_append_data(parser->strbuf, first, parser->cur - first); + if (parser->cur == parser->end) + return 0; + return 1; +} + +static int smtp_reply_parse_ehlo_greet(struct smtp_reply_parser *parser) +{ + const unsigned char *first = parser->cur; + + /* ehlo-greet = 1*(%d0-9 / %d11-12 / %d14-127) + * + * The greet is not supposed to be empty, but we don't really care + */ + + if (parser->cur == parser->end) + return 0; + if (smtp_char_is_ehlo_greet(*parser->cur)) { + for (;;) { + while (parser->cur < parser->end && + smtp_char_is_textstr(*parser->cur)) + parser->cur++; + + if (((parser->cur-first) + parser->state.reply_size + + str_len(parser->strbuf)) > + parser->max_reply_size) { + smtp_reply_parser_error(parser, + "Reply exceeds size limit"); + return -1; + } + + /* sanitize bad characters */ + str_append_data(parser->strbuf, + first, parser->cur - first); + + if (parser->cur == parser->end) + return 0; + if (!smtp_char_is_ehlo_greet(*parser->cur)) + break; + str_append_c(parser->strbuf, ' '); + parser->cur++; + first = parser->cur; + } + } + return 1; +} + +static inline const char *_chr_sanitize(unsigned char c) +{ + if (c >= 0x20 && c < 0x7F) + return t_strdup_printf("'%c'", c); + return t_strdup_printf("0x%02x", c); +} + +static void +smtp_reply_parser_parse_enhanced_code(const char *text, + struct smtp_reply_parser *parser, + const char **pos_r) +{ + struct smtp_reply_enhanced_code code; + struct smtp_reply_enhanced_code *cur_code = + &parser->state.reply->enhanced_code; + + if (cur_code->x == 9) + return; /* failed on earlier line */ + + if (!smtp_reply_parse_enhanced_code(text, &code, pos_r)) { + /* failed to parse an enhanced code */ + i_zero(cur_code); + cur_code->x = 9; + return; + } + + if (**pos_r != ' ' && **pos_r != '\r' && **pos_r != '\n') + return; + (*pos_r)++; + + /* check for match with status */ + if (code.x != parser->state.reply->status / 100) { + /* ignore code */ + return; + } + + /* check for code consistency */ + if (parser->state.line > 0 && + (cur_code->x != code.x || cur_code->y != code.y || + cur_code->z != code.z)) { + /* ignore code */ + return; + } + + *cur_code = code; +} + +static void smtp_reply_parser_finish_line(struct smtp_reply_parser *parser) +{ + const char *text = str_c(parser->strbuf); + + if (parser->enhanced_codes && str_len(parser->strbuf) > 5) + smtp_reply_parser_parse_enhanced_code(text, parser, &text); + + parser->state.line++; + parser->state.reply_size += str_len(parser->strbuf); + text = p_strdup(parser->reply_pool, text); + array_push_back(&parser->state.reply_lines, &text); + str_truncate(parser->strbuf, 0); +} + +static int smtp_reply_parse_more(struct smtp_reply_parser *parser) +{ + unsigned int status; + int ret; + + /* + Reply-line = *( Reply-code "-" [ textstring ] CRLF ) + Reply-code [ SP textstring ] CRLF + Reply-code = %x32-35 %x30-35 %x30-39 + + ehlo-ok-rsp = ( "250" SP Domain [ SP ehlo-greet ] CRLF ) + / ( "250-" Domain [ SP ehlo-greet ] CRLF + *( "250-" ehlo-line CRLF ) + "250" SP ehlo-line CRLF ) + */ + + for (;;) { + switch (parser->state.state) { + case SMTP_REPLY_PARSE_STATE_INIT: + smtp_reply_parser_restart(parser); + parser->state.state = SMTP_REPLY_PARSE_STATE_CODE; + /* fall through */ + /* Reply-code */ + case SMTP_REPLY_PARSE_STATE_CODE: + if ((ret=smtp_reply_parse_code(parser, &status)) <= 0) { + if (ret < 0) { + smtp_reply_parser_error(parser, + "Invalid status code in reply"); + } + return ret; + } + if (parser->state.line == 0) { + parser->state.reply->status = status; + } else if (status != parser->state.reply->status) { + smtp_reply_parser_error(parser, + "Inconsistent status codes in reply"); + return -1; + } + parser->state.state = SMTP_REPLY_PARSE_STATE_SEP; + if (parser->cur == parser->end) + return 0; + /* fall through */ + /* "-" / SP / CRLF */ + case SMTP_REPLY_PARSE_STATE_SEP: + switch (*parser->cur) { + /* "-" [ textstring ] CRLF */ + case '-': + parser->cur++; + parser->state.last_line = FALSE; + parser->state.state = + SMTP_REPLY_PARSE_STATE_TEXT; + break; + /* SP [ textstring ] CRLF ; allow missing text */ + case ' ': + parser->cur++; + parser->state.state = + SMTP_REPLY_PARSE_STATE_TEXT; + parser->state.last_line = TRUE; + break; + /* CRLF */ + case '\r': + case '\n': + parser->state.last_line = TRUE; + parser->state.state = SMTP_REPLY_PARSE_STATE_CR; + break; + default: + smtp_reply_parser_error(parser, + "Encountered unexpected %s after reply status code", + _chr_sanitize(*parser->cur)); + return -1; + } + if (parser->state.state != SMTP_REPLY_PARSE_STATE_TEXT) + break; + /* fall through */ + /* textstring / (Domain [ SP ehlo-greet ]) */ + case SMTP_REPLY_PARSE_STATE_TEXT: + if (parser->ehlo && + parser->state.reply->status == 250 && + parser->state.line == 0) { + /* handle first line of EHLO success response + differently because it can contain control + characters (WHY??!) */ + if ((ret=smtp_reply_parse_ehlo_domain(parser)) <= 0) + return ret; + parser->state.state = + SMTP_REPLY_PARSE_STATE_EHLO_SPACE; + if (parser->cur == parser->end) + return 0; + break; + } + if ((ret=smtp_reply_parse_textstring(parser)) <= 0) + return ret; + parser->state.state = SMTP_REPLY_PARSE_STATE_CR; + if (parser->cur == parser->end) + return 0; + /* fall through */ + /* CR */ + case SMTP_REPLY_PARSE_STATE_CR: + if (*parser->cur == '\r') { + parser->cur++; + parser->state.state = + SMTP_REPLY_PARSE_STATE_CRLF; + } else { + parser->state.state = + SMTP_REPLY_PARSE_STATE_LF; + } + if (parser->cur == parser->end) + return 0; + /* fall through */ + /* CRLF / LF */ + case SMTP_REPLY_PARSE_STATE_CRLF: + case SMTP_REPLY_PARSE_STATE_LF: + if (*parser->cur != '\n') { + if (parser->state.state == + SMTP_REPLY_PARSE_STATE_CRLF) { + smtp_reply_parser_error(parser, + "Encountered stray CR in reply text"); + } else { + smtp_reply_parser_error(parser, + "Encountered stray %s in reply text", + _chr_sanitize(*parser->cur)); + } + return -1; + } + parser->cur++; + smtp_reply_parser_finish_line(parser); + if (parser->state.last_line) { + parser->state.state = + SMTP_REPLY_PARSE_STATE_INIT; + return 1; + } + parser->state.state = SMTP_REPLY_PARSE_STATE_CODE; + break; + /* SP ehlo-greet */ + case SMTP_REPLY_PARSE_STATE_EHLO_SPACE: + if (*parser->cur != ' ') { + parser->state.state = SMTP_REPLY_PARSE_STATE_CR; + break; + } + parser->cur++; + str_append_c(parser->strbuf, ' '); + parser->state.state = SMTP_REPLY_PARSE_STATE_EHLO_GREET; + if (parser->cur == parser->end) + return 0; + /* fall through */ + /* ehlo-greet */ + case SMTP_REPLY_PARSE_STATE_EHLO_GREET: + if ((ret=smtp_reply_parse_ehlo_greet(parser)) <= 0) + return ret; + parser->state.state = SMTP_REPLY_PARSE_STATE_CR; + if (parser->cur == parser->end) + return 0; + break; + default: + i_unreached(); + } + } + + i_unreached(); + return -1; +} + +static int smtp_reply_parse(struct smtp_reply_parser *parser) +{ + size_t size; + int ret; + + while ((ret = i_stream_read_more(parser->input, + &parser->begin, &size)) > 0) { + parser->cur = parser->begin; + parser->end = parser->cur + size; + + if ((ret = smtp_reply_parse_more(parser)) < 0) + return -1; + + i_stream_skip(parser->input, parser->cur - parser->begin); + if (ret > 0) + return 1; + } + + i_assert(ret != -2); + if (ret < 0) { + i_assert(parser->input->eof); + if (parser->input->stream_errno == 0) { + if (parser->state.state == SMTP_REPLY_PARSE_STATE_INIT) + return 0; + smtp_reply_parser_error(parser, + "Premature end of input"); + } else { + smtp_reply_parser_error(parser, + "Stream error: %s", + i_stream_get_error(parser->input)); + } + } + return ret; +} + +int smtp_reply_parse_next(struct smtp_reply_parser *parser, + bool enhanced_codes, struct smtp_reply **reply_r, + const char **error_r) +{ + int ret; + + i_assert(parser->state.state == SMTP_REPLY_PARSE_STATE_INIT || + (parser->enhanced_codes == enhanced_codes && !parser->ehlo)); + + parser->enhanced_codes = enhanced_codes; + parser->ehlo = FALSE; + + i_free_and_null(parser->error); + + /* + Reply-line = *( Reply-code "-" [ textstring ] CRLF ) + Reply-code [ SP textstring ] CRLF + Reply-code = %x32-35 %x30-35 %x30-39 + textstring = 1*(%d09 / %d32-126) ; HT, SP, Printable US-ASCII + + Greeting is not handled specially here. + */ + if ((ret=smtp_reply_parse(parser)) <= 0) { + *error_r = parser->error; + return ret; + } + + i_assert(array_count(&parser->state.reply_lines) > 0); + array_append_zero(&parser->state.reply_lines); + + parser->state.state = SMTP_REPLY_PARSE_STATE_INIT; + parser->state.reply->text_lines = + array_front(&parser->state.reply_lines); + *reply_r = parser->state.reply; + return 1; +} + +int smtp_reply_parse_ehlo(struct smtp_reply_parser *parser, + struct smtp_reply **reply_r, const char **error_r) +{ + int ret; + + i_assert(parser->state.state == SMTP_REPLY_PARSE_STATE_INIT || + (!parser->enhanced_codes && parser->ehlo)); + + parser->enhanced_codes = FALSE; + parser->ehlo = TRUE; + + i_free_and_null(parser->error); + + /* + ehlo-ok-rsp = ( "250" SP Domain [ SP ehlo-greet ] CRLF ) + / ( "250-" Domain [ SP ehlo-greet ] CRLF + *( "250-" ehlo-line CRLF ) + "250" SP ehlo-line CRLF ) + ehlo-greet = 1*(%d0-9 / %d11-12 / %d14-127) + ; string of any characters other than CR or LF + ehlo-line = ehlo-keyword *( SP ehlo-param ) + ehlo-keyword = (ALPHA / DIGIT) *(ALPHA / DIGIT / "-") + ; additional syntax of ehlo-params depends on + ; ehlo-keyword + ehlo-param = 1*(%d33-126) + ; any CHAR excluding <SP> and all + ; control characters (US-ASCII 0-31 and 127 + ; inclusive) + */ + if ((ret=smtp_reply_parse(parser)) <= 0) { + *error_r = parser->error; + return ret; + } + + i_assert(array_count(&parser->state.reply_lines) > 0); + array_append_zero(&parser->state.reply_lines); + + parser->state.state = SMTP_REPLY_PARSE_STATE_INIT; + parser->state.reply->text_lines = + array_front(&parser->state.reply_lines); + *reply_r = parser->state.reply; + return 1; +} |