diff --git a/lib/net/imap.rb b/lib/net/imap.rb index bc6b7bea..ba67df6a 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -1242,6 +1242,9 @@ def starttls(**options) # +SASL-IR+ capability, below). Defaults to the #config value for # {sasl_ir}[rdoc-ref:Config#sasl_ir], which defaults to +true+. # + # The +registry+ kwarg can be used to select the mechanism implementation + # from a custom registry. See SASL.authenticator and SASL::Authenticators. + # # All other arguments are forwarded to the registered SASL authenticator for # the requested mechanism. The documentation for each individual # mechanism must be consulted for its specific parameters. diff --git a/lib/net/imap/sasl.rb b/lib/net/imap/sasl.rb index 359ad390..985b08dd 100644 --- a/lib/net/imap/sasl.rb +++ b/lib/net/imap/sasl.rb @@ -114,8 +114,8 @@ module SASL # messages has not passed integrity checks. AuthenticationFailed = Class.new(Error) - # Indicates that authentication cannot proceed because one of the server's - # ended authentication prematurely. + # Indicates that authentication cannot proceed because the server ended + # authentication prematurely. class AuthenticationIncomplete < AuthenticationFailed # The success response from the server attr_reader :response @@ -159,7 +159,10 @@ def initialize(response, message = "authentication ended prematurely") # Returns the default global SASL::Authenticators instance. def self.authenticators; @authenticators ||= Authenticators.new end - # Delegates to registry.new See Authenticators#new. + # Creates a new SASL authenticator, using SASL::Authenticators#new. + # + # +registry+ defaults to SASL.authenticators. All other arguments are + # forwarded to to registry.new. def self.authenticator(*args, registry: authenticators, **kwargs, &block) registry.new(*args, **kwargs, &block) end diff --git a/lib/net/imap/sasl/authentication_exchange.rb b/lib/net/imap/sasl/authentication_exchange.rb index 3276580c..8a31f926 100644 --- a/lib/net/imap/sasl/authentication_exchange.rb +++ b/lib/net/imap/sasl/authentication_exchange.rb @@ -9,39 +9,70 @@ module SASL # TODO: catch exceptions in #process and send #cancel_response. # TODO: raise an error if the command succeeds after being canceled. # TODO: use with more clients, to verify the API can accommodate them. + # TODO: pass ClientAdapter#service to SASL.authenticator # - # Create an AuthenticationExchange from a client adapter and a mechanism - # authenticator: - # def authenticate(mechanism, ...) - # authenticator = SASL.authenticator(mechanism, ...) - # SASL::AuthenticationExchange.new( - # sasl_adapter, mechanism, authenticator - # ).authenticate - # end - # - # private + # An AuthenticationExchange represents a single attempt to authenticate + # a SASL client to a SASL server. It is created from a client adapter, a + # mechanism name, and a mechanism authenticator. When #authenticate is + # called, it will send the appropriate authenticate command to the server, + # returning the client response on success and raising an exception on + # failure. # - # def sasl_adapter = MyClientAdapter.new(self, &method(:send_command)) + # In most cases, the client will not need to use + # SASL::AuthenticationExchange directly at all. Instead, use + # SASL::ClientAdapter#authenticate. If customizations are needed, the + # custom client adapter is probably the best place for that code. # - # Or delegate creation of the authenticator to ::build: # def authenticate(...) - # SASL::AuthenticationExchange.build(sasl_adapter, ...) - # .authenticate + # MyClient::SASLAdapter.new(self).authenticate(...) # end # - # As a convenience, ::authenticate combines ::build and #authenticate: + # SASL::ClientAdapter#authenticate delegates to ::authenticate, like so: + # # def authenticate(...) + # sasl_adapter = MyClient::SASLAdapter.new(self) # SASL::AuthenticationExchange.authenticate(sasl_adapter, ...) # end # - # Likewise, ClientAdapter#authenticate delegates to #authenticate: - # def authenticate(...) = sasl_adapter.authenticate(...) + # ::authenticate simply delegates to ::build and #authenticate, like so: + # + # def authenticate(...) + # sasl_adapter = MyClient::SASLAdapter.new(self) + # SASL::AuthenticationExchange + # .build(sasl_adapter, ...) + # .authenticate + # end + # + # And ::build delegates to SASL.authenticator and ::new, like so: + # + # def authenticate(mechanism, ...) + # sasl_adapter = MyClient::SASLAdapter.new(self) + # authenticator = SASL.authenticator(mechanism, ...) + # SASL::AuthenticationExchange + # .new(sasl_adapter, mechanism, authenticator) + # .authenticate + # end # class AuthenticationExchange # Convenience method for build(...).authenticate + # + # See also: SASL::ClientAdapter#authenticate def self.authenticate(...) build(...).authenticate end - # Use +registry+ to override the global Authenticators registry. + # Convenience method to combine the creation of a new authenticator and + # a new Authentication exchange. + # + # +client+ must be an instance of SASL::ClientAdapter. + # + # +mechanism+ must be a SASL mechanism name, as a string or symbol. + # + # +sasl_ir+ allows or disallows sending an "initial response", depending + # also on whether the server capabilities, mechanism authenticator, and + # client adapter all support it. Defaults to +true+. + # + # +mechanism+, +args+, +kwargs+, and +block+ are all forwarded to + # SASL.authenticator. Use the +registry+ kwarg to override the global + # SASL::Authenticators registry. def self.build(client, mechanism, *args, sasl_ir: true, **kwargs, &block) authenticator = SASL.authenticator(mechanism, *args, **kwargs, &block) new(client, mechanism, authenticator, sasl_ir: sasl_ir) diff --git a/lib/net/imap/sasl/client_adapter.rb b/lib/net/imap/sasl/client_adapter.rb index ea9e993a..8573ff64 100644 --- a/lib/net/imap/sasl/client_adapter.rb +++ b/lib/net/imap/sasl/client_adapter.rb @@ -19,31 +19,53 @@ module SASL class ClientAdapter include ProtocolAdapters::Generic - attr_reader :client, :command_proc + # The client that handles communication with the protocol server. + attr_reader :client # +command_proc+ can used to avoid exposing private methods on #client. - # It should run a command with the arguments sent to it, yield each - # continuation payload, respond to the server with the result of each - # yield, and return the result. Non-successful results *MUST* raise an - # exception. Exceptions in the block *MUST* cause the command to fail. + # It's value is set by the block that is passed to ::new, and it is used + # by the default implementation of #run_command. Subclasses that + # override #run_command may use #command_proc for any other purpose they + # find useful. # - # Subclasses that override #run_command may use #command_proc for - # other purposes. + # In the default implementation of #run_command, command_proc is called + # with the protocols authenticate +command+ name, the +mechanism+ name, + # an _optional_ +initial_response+ argument, and a +continuations+ + # block. command_proc must run the protocol command with the arguments + # sent to it, _yield_ the payload of each continuation, respond to the + # continuation with the result of each _yield_, and _return_ the + # command's successful result. Non-successful results *MUST* raise + # an exception. + attr_reader :command_proc + + # By default, this simply sets the #client and #command_proc attributes. + # Subclasses may override it, for example: to set the appropriate + # command_proc automatically. def initialize(client, &command_proc) @client, @command_proc = client, command_proc end - # Delegates to AuthenticationExchange.authenticate. + # Attempt to authenticate #client to the server. + # + # By default, this simply delegates to + # AuthenticationExchange.authenticate. def authenticate(...) AuthenticationExchange.authenticate(self, ...) end - # Do the protocol and server both support an initial response? + # Do the protocol, server, and client all support an initial response? + # + # By default, this simply delegates to client.sasl_ir_capable?. def sasl_ir_capable?; client.sasl_ir_capable? end # Does the server advertise support for the mechanism? + # + # By default, this simply delegates to client.auth_capable?. def auth_capable?(mechanism); client.auth_capable?(mechanism) end - # Runs the authenticate command with +mechanism+ and +initial_response+. - # When +initial_response+ is nil, an initial response must NOT be sent. + # Calls command_proc with +command_name+ (see + # SASL::ProtocolAdapters::Generic#command_name), + # +mechanism+, +initial_response+, and a +continuations_handler+ block. + # The +initial_response+ is optional; when it's nil, it won't be sent to + # command_proc. # # Yields each continuation payload, responds to the server with the # result of each yield, and returns the result. Non-successful results @@ -51,10 +73,10 @@ def auth_capable?(mechanism); client.auth_capable?(mechanism) end # command to fail. # # Subclasses that override this may use #command_proc differently. - def run_command(mechanism, initial_response = nil, &block) + def run_command(mechanism, initial_response = nil, &continuations_handler) command_proc or raise Error, "initialize with block or override" args = [command_name, mechanism, initial_response].compact - command_proc.call(*args, &block) + command_proc.call(*args, &continuations_handler) end # Returns an array of server responses errors raised by run_command. @@ -62,9 +84,13 @@ def run_command(mechanism, initial_response = nil, &block) def response_errors; [] end # Drop the connection gracefully. + # + # By default, this simply delegates to client.drop_connection. def drop_connection; client.drop_connection end # Drop the connection abruptly. + # + # By default, this simply delegates to client.drop_connection!. def drop_connection!; client.drop_connection! end end end