Skip to content

Commit

Permalink
🔀 Merge pull request #320 from ruby/sasl/client-adapter-api
Browse files Browse the repository at this point in the history
🔒 📚  Improvements and docs for SASL::ClientAdapter
  • Loading branch information
nevans authored Sep 13, 2024
2 parents 585bf65 + 9f483ce commit 34c1064
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 25 deletions.
65 changes: 45 additions & 20 deletions lib/net/imap/sasl/client_adapter.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require "forwardable"

module Net
class IMAP
module SASL
Expand All @@ -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.
Expand Down Expand Up @@ -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 <tt>client.sasl_ir_capable?</tt>.
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 <tt>client.auth_capable?</tt>.
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),
Expand All @@ -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 <tt>client.drop_connection</tt>.
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 <tt>client.drop_connection!</tt>.
def drop_connection!; client.drop_connection! end
end
end
end
Expand Down
64 changes: 60 additions & 4 deletions lib/net/imap/sasl/protocol_adapters.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 <tt>"AUTHENTICATE"</tt>.
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 <tt>"host"</tt>, 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
# <tt>"="</tt> 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 <tt>"*"</tt>.
def cancel_response; "*" end
end

Expand Down
1 change: 0 additions & 1 deletion lib/net/imap/sasl_adapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 34c1064

Please sign in to comment.