Skip to content

Commit ab7d3ee

Browse files
authored
🔀 Merge pull request #198 from ruby/validate-response_tagged-in-the-parser
🥅 Validate response tagged in the parser
2 parents 934b858 + 69f4dfb commit ab7d3ee

File tree

4 files changed

+68
-17
lines changed

4 files changed

+68
-17
lines changed

‎lib/net/imap.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2519,7 +2519,8 @@ def get_tagged_response(tag, cmd, timeout = nil)
25192519
when /\A(?:BAD)\z/ni
25202520
raise BadResponseError, resp
25212521
else
2522-
raise UnknownResponseError, resp
2522+
disconnect
2523+
raise InvalidResponseError, "invalid tagged resp: %p" % [resp.raw.chomp]
25232524
end
25242525
end
25252526

‎lib/net/imap/errors.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,27 @@ class BadResponseError < ResponseError
4747
class ByeResponseError < ResponseError
4848
end
4949

50+
# Error raised when the server sends an invalid response.
51+
#
52+
# This is different from UnknownResponseError: the response has been
53+
# rejected. Although it may be parsable, the server is forbidden from
54+
# sending it in the current context. The client should automatically
55+
# disconnect, abruptly (without logout).
56+
#
57+
# Note that InvalidResponseError does not inherit from ResponseError: it
58+
# can be raised before the response is fully parsed. A related
59+
# ResponseParseError or ResponseError may be the #cause.
60+
class InvalidResponseError < Error
61+
end
62+
5063
# Error raised upon an unknown response from the server.
64+
#
65+
# This is different from InvalidResponseError: the response may be a
66+
# valid extension response and the server may be allowed to send it in
67+
# this context, but Net::IMAP either does not know how to parse it or
68+
# how to handle it. This could result from enabling unknown or
69+
# unhandled extensions. The connection may still be usable,
70+
# but—depending on context—it may be prudent to disconnect.
5171
class UnknownResponseError < ResponseError
5272
end
5373

‎lib/net/imap/response_parser.rb

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,21 @@ def parse(str)
5858
T_TEXT = :TEXT # any char except CRLF
5959
T_EOF = :EOF # end of response string
6060

61+
module ResponseConditions
62+
OK = "OK"
63+
NO = "NO"
64+
BAD = "BAD"
65+
BYE = "BYE"
66+
PREAUTH = "PREAUTH"
67+
68+
RESP_COND_STATES = [OK, NO, BAD ].freeze
69+
RESP_DATA_CONDS = [OK, NO, BAD, BYE, ].freeze
70+
AUTH_CONDS = [OK, PREAUTH].freeze
71+
GREETING_CONDS = [OK, BYE, PREAUTH].freeze
72+
RESP_CONDS = [OK, NO, BAD, BYE, PREAUTH].freeze
73+
end
74+
include ResponseConditions
75+
6176
module Patterns
6277

6378
module CharClassSubtraction
@@ -320,10 +335,13 @@ def unescape_quoted(quoted)
320335

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

323-
# atom = 1*ATOM-CHAR
324-
#
325-
# TODO: match atom entirely by regexp (in the "lexer")
326-
def atom; -combine_adjacent(*ATOM_TOKENS) end
338+
# tag = 1*<any ASTRING-CHAR except "+">
339+
TAG_TOKENS = (ASTRING_CHARS_TOKENS - [T_PLUS]).freeze
340+
341+
# TODO: handle atom, astring_chars, and tag entirely inside the lexer
342+
def atom; combine_adjacent(*ATOM_TOKENS) end
343+
def astring_chars; combine_adjacent(*ASTRING_CHARS_TOKENS) end
344+
def tag; combine_adjacent(*TAG_TOKENS) end
327345

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

339-
# TODO: handle astring_chars entirely inside the lexer
340-
def astring_chars
341-
combine_adjacent(*ASTRING_CHARS_TOKENS)
342-
end
343-
344357
# astring = 1*ASTRING-CHAR / string
345358
def astring
346359
lookahead?(*ASTRING_CHARS_TOKENS) ? astring_chars : string
@@ -357,6 +370,17 @@ def label(word)
357370
parse_error("unexpected atom %p, expected %p instead", val, word)
358371
end
359372

373+
# expects "OK" or "NO" or "BAD" and raises InvalidResponseError on failure
374+
def resp_cond_state__name
375+
if RESP_COND_STATES.include?(actual = tagged_ext_label)
376+
actual
377+
else
378+
raise InvalidResponseError, "bad response type %p, expected %s" % [
379+
actual, RESP_COND_STATES.join(" or ")
380+
]
381+
end
382+
end
383+
360384
# nstring = string / nil
361385
def nstring
362386
NIL? ? nil : string
@@ -452,13 +476,18 @@ def response_untagged
452476
end
453477
end
454478

479+
# RFC3501 & RFC9051:
480+
# response-tagged = tag SP resp-cond-state CRLF
481+
#
482+
# resp-cond-state = ("OK" / "NO" / "BAD") SP resp-text
483+
# ; Status condition
484+
#
485+
# tag = 1*<any ASTRING-CHAR except "+">
455486
def response_tagged
456-
tag = astring_chars
457-
match(T_SPACE)
458-
token = match(T_ATOM)
459-
name = token.value.upcase
460-
match(T_SPACE)
461-
return TaggedResponse.new(tag, name, resp_text, @str)
487+
tag = tag(); SP!
488+
name = resp_cond_state__name; SP!
489+
data = resp_text
490+
TaggedResponse.new(tag, name, data, @str)
462491
end
463492

464493
def response_cond

‎test/net/imap/test_imap.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,9 +147,10 @@ def test_starttls_stripping
147147
imap = nil
148148
starttls_stripping_test do |port|
149149
imap = Net::IMAP.new("localhost", :port => port)
150-
assert_raise(Net::IMAP::UnknownResponseError) do
150+
assert_raise(Net::IMAP::InvalidResponseError) do
151151
imap.starttls(:ca_file => CA_FILE)
152152
end
153+
assert imap.disconnected?
153154
imap
154155
end
155156
assert_equal false, imap.tls_verified?

0 commit comments

Comments
 (0)