Skip to content

Commit a7e3137

Browse files
committed
🚧 Handle cancellations more carefully...
1 parent 6be8eba commit a7e3137

File tree

3 files changed

+77
-12
lines changed

3 files changed

+77
-12
lines changed

lib/net/imap/sasl.rb

+9-1
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,15 @@ module SASL
103103

104104
# Indicates an authentication exchange that will be or has been canceled
105105
# by the client, not due to any error or failure during processing.
106-
AuthenticationCanceled = Class.new(Error)
106+
class AuthenticationCanceled < Error
107+
# The error response from the server
108+
attr_reader :response
109+
110+
def initialize(message = "authentication canceled", response: nil)
111+
super(message)
112+
@response = response
113+
end
114+
end
107115

108116
# Indicates an error when processing a server challenge, e.g: an invalid
109117
# or unparsable challenge. An underlying exception may be available as

lib/net/imap/sasl/authentication_exchange.rb

+52-5
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ def self.build(client, mechanism, *args, sasl_ir: true, **kwargs, &block)
8282
# An exception that has been raised by <tt>authenticator.process</tt>.
8383
attr_reader :process_error
8484

85+
# An exception that represents an error response from the server.
86+
attr_reader :response_error
87+
8588
def initialize(client, mechanism, authenticator, sasl_ir: true)
8689
@client = client
8790
@mechanism = Authenticators.normalize_name(mechanism)
@@ -103,9 +106,11 @@ def initialize(client, mechanism, authenticator, sasl_ir: true)
103106
# Unfortunately, the original error will not be the +#cause+ for the
104107
# client error. But it will be available on #process_error.
105108
def authenticate
106-
client.run_command(mechanism, initial_response) { process _1 }
107-
.tap { raise process_error if process_error }
108-
.tap { raise AuthenticationIncomplete, _1 unless done? }
109+
handle_cancellation do
110+
client.run_command(mechanism, initial_response) { process _1 }
111+
.tap { raise process_error if process_error }
112+
.tap { raise AuthenticationIncomplete, _1 unless done? }
113+
end
109114
rescue AuthenticationCanceled, *client.response_errors
110115
raise # but don't drop the connection
111116
rescue
@@ -141,11 +146,53 @@ def process(challenge)
141146
@processed = true
142147
return client.cancel_response if process_error
143148
client.encode authenticator.process client.decode challenge
144-
rescue => process_error
145-
@process_error = process_error
149+
rescue AuthenticationCanceled => error
150+
@process_error = error
151+
client.cancel_response
152+
rescue => error
153+
@process_error = begin
154+
raise AuthenticationError, "error while processing server challenge"
155+
rescue
156+
$!
157+
end
146158
client.cancel_response
147159
end
148160

161+
# | process | response | => result |
162+
# |---------|----------|------------------------------------------|
163+
# | success | success | success |
164+
# | success | error | reraise response error |
165+
# | error | success | raise incomplete error (cause = process) |
166+
# | error | error | raise canceled error (cause = process) |
167+
def handle_cancellation
168+
result = begin
169+
yield
170+
rescue *client.response_errors => error
171+
@response_error = error
172+
raise unless process_error
173+
end
174+
raise_mutual_cancellation! if process_error && response_error
175+
raise_incomplete_cancel!(result) if process_error && !response_error
176+
result
177+
end
178+
179+
def raise_mutual_cancellation!
180+
raise process_error # sets the cause
181+
rescue
182+
raise AuthenticationCanceled.new(
183+
"authentication canceled (see error #cause and #response)",
184+
response: response_error
185+
)
186+
end
187+
188+
def raise_incomplete_cancellation!
189+
raise process_error # sets the cause
190+
rescue
191+
raise AuthenticationIncomplete.new(
192+
response_error, "server ignored canceled authentication"
193+
)
194+
end
195+
149196
end
150197
end
151198
end

test/net/imap/test_imap.rb

+16-6
Original file line numberDiff line numberDiff line change
@@ -1073,14 +1073,24 @@ def test_id
10731073
) do |server, imap|
10741074
registry = Net::IMAP::SASL::Authenticators.new(use_defaults: false)
10751075
registry.add_authenticator :plain, ->(*a, **kw, &b) {
1076-
->(challenge) {
1076+
obj = Object.new
1077+
obj.define_singleton_method(:process) do |challenge|
10771078
raise(Net::IMAP::SASL::AuthenticationCanceled,
1078-
"a: %p, kw: %p, b: %p" % [a, kw, b])
1079-
}
1079+
"a: %p, kw: %p, b: %p, c: %p" % [a, kw, b, challenge])
1080+
end
1081+
obj
10801082
}
1081-
assert_raise_with_message(Net::IMAP::BadResponseError, "canceled") do
1082-
imap.authenticate(:plain, hello: :world, registry: registry)
1083-
end
1083+
error = nil
1084+
assert_raise_with_message(Net::IMAP::SASL::AuthenticationCanceled,
1085+
/authentication canceled/i) do
1086+
imap.authenticate(:plain, foo: :bar, registry: registry)
1087+
rescue => error
1088+
raise # for assert_raise
1089+
end
1090+
assert_kind_of Net::IMAP::SASL::AuthenticationCanceled, error.cause
1091+
assert_equal 'a: [], kw: {:foo=>:bar}, b: nil, c: ""', error.cause.to_s
1092+
assert_kind_of Net::IMAP::BadResponseError, error.response
1093+
assert_equal "canceled", error.response.to_s
10841094
refute imap.disconnected?
10851095
end
10861096
end

0 commit comments

Comments
 (0)