Skip to content

Commit 2e2c88e

Browse files
committed
🚧 Handle cancellations more carefully...
1 parent 14e4847 commit 2e2c88e

File tree

3 files changed

+76
-11
lines changed

3 files changed

+76
-11
lines changed

lib/net/imap/sasl.rb

Lines changed: 9 additions & 1 deletion
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

Lines changed: 52 additions & 5 deletions
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 => SASL::ClientAdapter
8790
@client = client
@@ -104,9 +107,11 @@ def initialize(client, mechanism, authenticator, sasl_ir: true)
104107
# Unfortunately, the original error will not be the +#cause+ for the
105108
# client error. But it will be available on #process_error.
106109
def authenticate
107-
client.run_command(mechanism, initial_response) { process _1 }
108-
.tap { raise process_error if process_error }
109-
.tap { raise AuthenticationIncomplete, _1 unless done? }
110+
handle_cancellation do
111+
client.run_command(mechanism, initial_response) { process _1 }
112+
.tap { raise process_error if process_error }
113+
.tap { raise AuthenticationIncomplete, _1 unless done? }
114+
end
110115
rescue AuthenticationCanceled, *client.response_errors
111116
raise # but don't drop the connection
112117
rescue
@@ -142,11 +147,53 @@ def process(challenge)
142147
@processed = true
143148
return client.cancel_response if process_error
144149
client.encode authenticator.process client.decode challenge
145-
rescue => process_error
146-
@process_error = process_error
150+
rescue AuthenticationCanceled => error
151+
@process_error = error
152+
client.cancel_response
153+
rescue => error
154+
@process_error = begin
155+
raise AuthenticationError, "error while processing server challenge"
156+
rescue
157+
$!
158+
end
147159
client.cancel_response
148160
end
149161

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

test/net/imap/test_imap.rb

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1018,14 +1018,24 @@ def test_id
10181018
) do |server, imap|
10191019
registry = Net::IMAP::SASL::Authenticators.new(use_defaults: false)
10201020
registry.add_authenticator :plain, ->(*a, **kw, &b) {
1021-
->(challenge) {
1021+
obj = Object.new
1022+
obj.define_singleton_method(:process) do |challenge|
10221023
raise(Net::IMAP::SASL::AuthenticationCanceled,
1023-
"a: %p, kw: %p, b: %p" % [a, kw, b])
1024-
}
1024+
"a: %p, kw: %p, b: %p, c: %p" % [a, kw, b, challenge])
1025+
end
1026+
obj
10251027
}
1026-
assert_raise_with_message(Net::IMAP::BadResponseError, "canceled") do
1027-
imap.authenticate(:plain, hello: :world, registry: registry)
1028+
error = nil
1029+
assert_raise_with_message(Net::IMAP::SASL::AuthenticationCanceled,
1030+
/authentication canceled/i) do
1031+
imap.authenticate(:plain, foo: :bar, registry: registry)
1032+
rescue => error
1033+
raise # for assert_raise
10281034
end
1035+
assert_kind_of Net::IMAP::SASL::AuthenticationCanceled, error.cause
1036+
assert_equal 'a: [], kw: {:foo=>:bar}, b: nil, c: ""', error.cause.to_s
1037+
assert_kind_of Net::IMAP::BadResponseError, error.response
1038+
assert_equal "canceled", error.response.to_s
10291039
refute imap.disconnected?
10301040
end
10311041
end

0 commit comments

Comments
 (0)