Skip to content

Commit

Permalink
🥅 Validate response-tagged in the parser
Browse files Browse the repository at this point in the history
The `response-tagged` parser now enforces `resp-cond-state`, raising
`InvalidResponseError` for any invalid status conditions.  It is also
refactored to the new parser style.  An explicit method for `tag` is
added, which is parsed similarly to `astring_chars` and `atom`.

```abnf
   response-tagged = tag SP resp-cond-state CRLF

   resp-cond-state = ("OK" / "NO" / "BAD") SP resp-text
                       ; Status condition

   tag             = 1*<any ASTRING-CHAR except "+">
```

Currently, any exception raised by the parser will abruptly drop the
connection.  In the future, if the response handler thread is made more
robust against recoverable errors, `InvalidResponseError` should be
considered non-recoverable.  (There is a test that covers this already).
  • Loading branch information
nevans committed Oct 21, 2023
1 parent 609acd9 commit 69f4dfb
Showing 1 changed file with 44 additions and 15 deletions.
59 changes: 44 additions & 15 deletions lib/net/imap/response_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,21 @@ def parse(str)
T_TEXT = :TEXT # any char except CRLF
T_EOF = :EOF # end of response string

module ResponseConditions
OK = "OK"
NO = "NO"
BAD = "BAD"
BYE = "BYE"
PREAUTH = "PREAUTH"

RESP_COND_STATES = [OK, NO, BAD ].freeze
RESP_DATA_CONDS = [OK, NO, BAD, BYE, ].freeze
AUTH_CONDS = [OK, PREAUTH].freeze
GREETING_CONDS = [OK, BYE, PREAUTH].freeze
RESP_CONDS = [OK, NO, BAD, BYE, PREAUTH].freeze
end
include ResponseConditions

module Patterns

module CharClassSubtraction
Expand Down Expand Up @@ -320,10 +335,13 @@ def unescape_quoted(quoted)

ASTRING_TOKENS = [T_QUOTED, *ASTRING_CHARS_TOKENS, T_LITERAL].freeze

# atom = 1*ATOM-CHAR
#
# TODO: match atom entirely by regexp (in the "lexer")
def atom; -combine_adjacent(*ATOM_TOKENS) end
# tag = 1*<any ASTRING-CHAR except "+">
TAG_TOKENS = (ASTRING_CHARS_TOKENS - [T_PLUS]).freeze

# TODO: handle atom, astring_chars, and tag entirely inside the lexer
def atom; combine_adjacent(*ATOM_TOKENS) end
def astring_chars; combine_adjacent(*ASTRING_CHARS_TOKENS) end
def tag; combine_adjacent(*TAG_TOKENS) end

# the #accept version of #atom
def atom?; -combine_adjacent(*ATOM_TOKENS) if lookahead?(*ATOM_TOKENS) end
Expand All @@ -336,11 +354,6 @@ def case_insensitive__atom?
-combine_adjacent(*ATOM_TOKENS).upcase if lookahead?(*ATOM_TOKENS)
end

# TODO: handle astring_chars entirely inside the lexer
def astring_chars
combine_adjacent(*ASTRING_CHARS_TOKENS)
end

# astring = 1*ASTRING-CHAR / string
def astring
lookahead?(*ASTRING_CHARS_TOKENS) ? astring_chars : string
Expand All @@ -357,6 +370,17 @@ def label(word)
parse_error("unexpected atom %p, expected %p instead", val, word)
end

# expects "OK" or "NO" or "BAD" and raises InvalidResponseError on failure
def resp_cond_state__name
if RESP_COND_STATES.include?(actual = tagged_ext_label)
actual
else
raise InvalidResponseError, "bad response type %p, expected %s" % [
actual, RESP_COND_STATES.join(" or ")
]
end
end

# nstring = string / nil
def nstring
NIL? ? nil : string
Expand Down Expand Up @@ -452,13 +476,18 @@ def response_untagged
end
end

# RFC3501 & RFC9051:
# response-tagged = tag SP resp-cond-state CRLF
#
# resp-cond-state = ("OK" / "NO" / "BAD") SP resp-text
# ; Status condition
#
# tag = 1*<any ASTRING-CHAR except "+">
def response_tagged
tag = astring_chars
match(T_SPACE)
token = match(T_ATOM)
name = token.value.upcase
match(T_SPACE)
return TaggedResponse.new(tag, name, resp_text, @str)
tag = tag(); SP!
name = resp_cond_state__name; SP!
data = resp_text
TaggedResponse.new(tag, name, data, @str)
end

def response_cond
Expand Down

0 comments on commit 69f4dfb

Please sign in to comment.