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