|
| 1 | +# frozen_string_literal: true |
| 2 | + |
| 3 | +module Net |
| 4 | + class IMAP |
| 5 | + module SASL |
| 6 | + |
| 7 | + # This API is *experimental*. |
| 8 | + # |
| 9 | + # TODO: catch exceptions in #process and send #cancel_response. |
| 10 | + # TODO: raise an error if the command succeeds after being canceled. |
| 11 | + # TODO: use with more clients, to verify the API can accommodate them. |
| 12 | + # |
| 13 | + # An abstract base class for implementing a SASL authentication exchange. |
| 14 | + # Different clients will each have their own adapter subclass, overridden |
| 15 | + # to match their needs. Methods to override are documented as protected. |
| 16 | + class ClientAdapter |
| 17 | + # Subclasses must redefine this if their command isn't "AUTHENTICATE". |
| 18 | + COMMAND_NAME = "AUTHENTICATE" |
| 19 | + |
| 20 | + # Subclasses should redefine this to include all server responses errors |
| 21 | + # raised by send_command_with_continuations. |
| 22 | + RESPONSE_ERRORS = [].freeze |
| 23 | + |
| 24 | + # Convenience method for <tt>new(...).authenticate</tt> |
| 25 | + def self.authenticate(...) new(...).authenticate end |
| 26 | + |
| 27 | + attr_reader :client, :mechanism, :authenticator |
| 28 | + |
| 29 | + # Can be supplied by +client+, to avoid exposing private methods. |
| 30 | + attr_reader :command_proc |
| 31 | + |
| 32 | + # When +sasl_ir+ is false, sending an initial response is prohibited. |
| 33 | + # +command_proc+ can used to avoid exposing private methods on #client. |
| 34 | + def initialize(client, mechanism, authenticator, sasl_ir: true, |
| 35 | + &command_proc) |
| 36 | + @client = client |
| 37 | + @mechanism = mechanism |
| 38 | + @authenticator = authenticator |
| 39 | + @sasl_ir = sasl_ir |
| 40 | + @command_proc = command_proc |
| 41 | + end |
| 42 | + |
| 43 | + # Call #authenticate to execute an authentication exchange for #client |
| 44 | + # using #authenticator. Authentication failures will raise an |
| 45 | + # exception. Any exceptions other than those in RESPONSE_ERRORS will |
| 46 | + # drop the connection. |
| 47 | + def authenticate |
| 48 | + response = process_ir if send_initial_response? |
| 49 | + args = authenticate_command_args(response) |
| 50 | + send_command_with_continuations(*args) { process _1 } |
| 51 | + .tap { raise AuthenticationIncomplete, _1 unless done? } |
| 52 | + rescue *self.class::RESPONSE_ERRORS => ex |
| 53 | + raise transform_exception(ex) |
| 54 | + rescue => ex |
| 55 | + drop_connection |
| 56 | + raise transform_exception(ex) |
| 57 | + rescue Exception |
| 58 | + drop_connection! |
| 59 | + raise |
| 60 | + end |
| 61 | + |
| 62 | + protected |
| 63 | + |
| 64 | + # Override if the arguments for send_command_with_continuations aren't |
| 65 | + # simply <tt>(COMMAND_NAME, mechanism, initial_response = nil)</tt>. |
| 66 | + def authenticate_command_args(initial_response = nil) |
| 67 | + [self.class::COMMAND_NAME, mechanism, initial_response].compact |
| 68 | + end |
| 69 | + |
| 70 | + def encode_ir(string) string.empty? ? "=" : encode(string) end |
| 71 | + def encode(string) [string].pack("m0") end |
| 72 | + def decode(string) string.unpack1("m0") end |
| 73 | + def cancel_response; "*" end |
| 74 | + |
| 75 | + # Override if the protocol always/never supports SASL-IR, the capability |
| 76 | + # isn't named +SASL-IR+, or #client doesn't respond to +capable?+. |
| 77 | + def supports_initial_response?; client.capable?("SASL-IR") end |
| 78 | + |
| 79 | + # Override if #client doesn't respond to +auth_capable?+. |
| 80 | + def supports_mechanism?; client.auth_capable?(mechanism) end |
| 81 | + |
| 82 | + # Runs the authenticate_command_args, yields each continuation payload, |
| 83 | + # responds to the server with the result of each yield, and returns the |
| 84 | + # result. Non-successful results *MUST* raise an exception. Exceptions |
| 85 | + # in the block *MUST* cause the command to fail. |
| 86 | + # |
| 87 | + # The default simply forwards all arguments to command_proc. |
| 88 | + # Subclasses that override this may use command_proc differently. |
| 89 | + def send_command_with_continuations(...) |
| 90 | + command_proc or raise Error, "initialize with block or override" |
| 91 | + command_proc.call(...) |
| 92 | + end |
| 93 | + |
| 94 | + # Override to logout and disconnect the connection gracefully. |
| 95 | + def drop_connection; client.disconnect end |
| 96 | + |
| 97 | + # Override to drop the connection abruptly. |
| 98 | + def drop_connection!; client.disconnect end |
| 99 | + |
| 100 | + # Override to transform any StandardError to a different exception. |
| 101 | + def transform_exception(exception) exception end |
| 102 | + |
| 103 | + private |
| 104 | + |
| 105 | + # Subclasses shouldn't override the following |
| 106 | + |
| 107 | + def send_initial_response? |
| 108 | + @sasl_ir && |
| 109 | + authenticator.respond_to?(:initial_response?) && |
| 110 | + authenticator.initial_response? && |
| 111 | + supports_initial_response? && |
| 112 | + supports_mechanism? |
| 113 | + end |
| 114 | + |
| 115 | + def process_ir; encode_ir authenticator.process nil end |
| 116 | + def process(data) encode authenticator.process decode data end |
| 117 | + |
| 118 | + def done?; !authenticator.respond_to?(:done?) || authenticator.done? end |
| 119 | + |
| 120 | + end |
| 121 | + end |
| 122 | + end |
| 123 | +end |
0 commit comments