/* 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 /* 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 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 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; }