diff --git a/lib/net/imap/sasl/client_adapter.rb b/lib/net/imap/sasl/client_adapter.rb index 8573ff64..18fa2fff 100644 --- a/lib/net/imap/sasl/client_adapter.rb +++ b/lib/net/imap/sasl/client_adapter.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "forwardable" + module Net class IMAP module SASL @@ -8,18 +10,28 @@ module SASL # # TODO: use with more clients, to verify the API can accommodate them. # - # An abstract base class for implementing a SASL authentication exchange. - # Different clients will each have their own adapter subclass, overridden - # to match their needs. + # Represents the client to a SASL::AuthenticationExchange. By default, + # most methods simply delegate to #client. Clients should subclass + # SASL::ClientAdapter and override methods as needed to match the + # semantics of this API to their API. # - # Although the default implementations _may_ be sufficient, subclasses - # will probably need to override some methods. Additionally, subclasses - # may need to include a protocol adapter mixin, if the default + # Subclasses should also include a protocol adapter mixin when the default # ProtocolAdapters::Generic isn't sufficient. + # + # === Protocol Requirements + # + # {RFC4422 §4}[https://www.rfc-editor.org/rfc/rfc4422.html#section-4] + # lists requirements for protocol specifications to offer SASL. Where + # possible, ClientAdapter delegates the handling of these requirements to + # SASL::ProtocolAdapters. class ClientAdapter + extend Forwardable + include ProtocolAdapters::Generic # The client that handles communication with the protocol server. + # + # Most ClientAdapter methods are simply delegated to #client by default. attr_reader :client # +command_proc+ can used to avoid exposing private methods on #client. @@ -51,15 +63,17 @@ def initialize(client, &command_proc) # AuthenticationExchange.authenticate. def authenticate(...) AuthenticationExchange.authenticate(self, ...) end + ## + # method: sasl_ir_capable? # 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 + def_delegator :client, :sasl_ir_capable? - # Does the server advertise support for the mechanism? + ## + # method: auth_capable? + # call-seq: auth_capable?(mechanism) # - # By default, this simply delegates to client.auth_capable?. - def auth_capable?(mechanism); client.auth_capable?(mechanism) end + # Does the server advertise support for the +mechanism+? + def_delegator :client, :auth_capable? # Calls command_proc with +command_name+ (see # SASL::ProtocolAdapters::Generic#command_name), @@ -79,19 +93,30 @@ def run_command(mechanism, initial_response = nil, &continuations_handler) command_proc.call(*args, &continuations_handler) end + ## + # method: host + # The hostname to which the client connected. + def_delegator :client, :host + + ## + # method: port + # The destination port to which the client connected. + def_delegator :client, :port + # Returns an array of server responses errors raised by run_command. # Exceptions in this array won't drop the connection. def response_errors; [] end - # Drop the connection gracefully. - # - # By default, this simply delegates to client.drop_connection. - def drop_connection; client.drop_connection end + ## + # method: drop_connection + # Drop the connection gracefully, sending a "LOGOUT" command as needed. + def_delegator :client, :drop_connection + + ## + # method: drop_connection! + # Drop the connection abruptly, closing the socket without logging out. + def_delegator :client, :drop_connection! - # Drop the connection abruptly. - # - # By default, this simply delegates to client.drop_connection!. - def drop_connection!; client.drop_connection! end end end end diff --git a/lib/net/imap/sasl/protocol_adapters.rb b/lib/net/imap/sasl/protocol_adapters.rb index 519b4596..d9ad62c0 100644 --- a/lib/net/imap/sasl/protocol_adapters.rb +++ b/lib/net/imap/sasl/protocol_adapters.rb @@ -4,16 +4,72 @@ module Net class IMAP module SASL + # SASL::ProtocolAdapters modules are meant to be used as mixins for + # SASL::ClientAdapter and its subclasses. Where the client adapter must + # be customized for each client library, the protocol adapter mixin + # handles \SASL requirements that are part of the protocol specification, + # but not specific to any particular client library. In particular, see + # {RFC4422 §4}[https://www.rfc-editor.org/rfc/rfc4422.html#section-4] + # + # === Interface + # + # >>> + # NOTE: This API is experimental, and may change. + # + # - {#command_name}[rdoc-ref:Generic#command_name] -- The name of the + # command used to to initiate an authentication exchange. + # - {#service}[rdoc-ref:Generic#service] -- The GSSAPI service name. + # - {#encode_ir}[rdoc-ref:Generic#encode_ir]--Encodes an initial response. + # - {#decode}[rdoc-ref:Generic#decode] -- Decodes a server challenge. + # - {#encode}[rdoc-ref:Generic#encode] -- Encodes a client response. + # - {#cancel_response}[rdoc-ref:Generic#cancel_response] -- The encoded + # client response used to cancel an authentication exchange. + # + # Other protocol requirements of the \SASL authentication exchange are + # handled by SASL::ClientAdapter. + # + # === Included protocol adapters + # + # - Generic -- a basic implementation of all of the methods listed above. + # - IMAP -- An adapter for the IMAP4 protocol. + # - SMTP -- An adapter for the \SMTP protocol with the +AUTH+ capability. + # - POP -- An adapter for the POP3 protocol with the +SASL+ capability. module ProtocolAdapters - # This API is experimental, and may change. + # See SASL::ProtocolAdapters@Interface. module Generic + # The name of the protocol command used to initiate a \SASL + # authentication exchange. + # + # The generic implementation returns "AUTHENTICATE". def command_name; "AUTHENTICATE" end - def service; raise "Implement in subclass or module" end - def host; client.host end - def port; client.port end + + # A service name from the {GSSAPI/Kerberos/SASL Service Names + # registry}[https://www.iana.org/assignments/gssapi-service-names/gssapi-service-names.xhtml]. + # + # The generic implementation returns "host", which is the + # generic GSSAPI host-based service name. + def service; "host" end + + # Encodes an initial response string. + # + # The generic implementation returns the result of #encode, or returns + # "=" when +string+ is empty. def encode_ir(string) string.empty? ? "=" : encode(string) end + + # Encodes a client response string. + # + # The generic implementation returns the Base64 encoding of +string+. def encode(string) [string].pack("m0") end + + # Decodes a server challenge string. + # + # The generic implementation returns the Base64 decoding of +string+. def decode(string) string.unpack1("m0") end + + # Returns the message used by the client to abort an authentication + # exchange. + # + # The generic implementation returns "*". def cancel_response; "*" end end diff --git a/lib/net/imap/sasl_adapter.rb b/lib/net/imap/sasl_adapter.rb index 7979414e..3aa1c24e 100644 --- a/lib/net/imap/sasl_adapter.rb +++ b/lib/net/imap/sasl_adapter.rb @@ -12,7 +12,6 @@ class SASLAdapter < SASL::ClientAdapter def response_errors; RESPONSE_ERRORS end def sasl_ir_capable?; client.capable?("SASL-IR") end - def auth_capable?(mechanism); client.auth_capable?(mechanism) end def drop_connection; client.logout! end def drop_connection!; client.disconnect end end