зеркало из https://github.com/github/ruby.git
[ruby/net-imap] Move each authenticator to its own file
Also updates rdoc with SASL specifications and deprecations. Of these four, only `PLAIN` isn't deprecated! +@@authenticators+ was changed to a class instance var +@authenticators+. No one should have been using the class variable directly, so that should be fine. https://github.com/ruby/net-imap/commit/23f241b081
This commit is contained in:
Родитель
a7f7479872
Коммит
331005812f
206
lib/net/imap.rb
206
lib/net/imap.rb
|
@ -16,8 +16,6 @@
|
||||||
|
|
||||||
require "socket"
|
require "socket"
|
||||||
require "monitor"
|
require "monitor"
|
||||||
require "digest/md5"
|
|
||||||
require "strscan"
|
|
||||||
require 'net/protocol'
|
require 'net/protocol'
|
||||||
begin
|
begin
|
||||||
require "openssl"
|
require "openssl"
|
||||||
|
@ -292,31 +290,6 @@ module Net
|
||||||
@@max_flag_count = count
|
@@max_flag_count = count
|
||||||
end
|
end
|
||||||
|
|
||||||
# Adds an authenticator for Net::IMAP#authenticate. +auth_type+
|
|
||||||
# is the type of authentication this authenticator supports
|
|
||||||
# (for instance, "LOGIN"). The +authenticator+ is an object
|
|
||||||
# which defines a process() method to handle authentication with
|
|
||||||
# the server. See Net::IMAP::LoginAuthenticator,
|
|
||||||
# Net::IMAP::CramMD5Authenticator, and Net::IMAP::DigestMD5Authenticator
|
|
||||||
# for examples.
|
|
||||||
#
|
|
||||||
#
|
|
||||||
# If +auth_type+ refers to an existing authenticator, it will be
|
|
||||||
# replaced by the new one.
|
|
||||||
def self.add_authenticator(auth_type, authenticator)
|
|
||||||
@@authenticators[auth_type] = authenticator
|
|
||||||
end
|
|
||||||
|
|
||||||
# Builds an authenticator for Net::IMAP#authenticate.
|
|
||||||
def self.authenticator(auth_type, *args)
|
|
||||||
auth_type = auth_type.upcase
|
|
||||||
unless @@authenticators.has_key?(auth_type)
|
|
||||||
raise ArgumentError,
|
|
||||||
format('unknown auth type - "%s"', auth_type)
|
|
||||||
end
|
|
||||||
@@authenticators[auth_type].new(*args)
|
|
||||||
end
|
|
||||||
|
|
||||||
# The default port for IMAP connections, port 143
|
# The default port for IMAP connections, port 143
|
||||||
def self.default_port
|
def self.default_port
|
||||||
return PORT
|
return PORT
|
||||||
|
@ -1124,7 +1097,6 @@ module Net
|
||||||
SSL_PORT = 993 # :nodoc:
|
SSL_PORT = 993 # :nodoc:
|
||||||
|
|
||||||
@@debug = false
|
@@debug = false
|
||||||
@@authenticators = {}
|
|
||||||
@@max_flag_count = 10000
|
@@max_flag_count = 10000
|
||||||
|
|
||||||
# :call-seq:
|
# :call-seq:
|
||||||
|
@ -3901,182 +3873,6 @@ module Net
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Authenticator for the "LOGIN" authentication type. See
|
|
||||||
# #authenticate().
|
|
||||||
class LoginAuthenticator
|
|
||||||
def process(data)
|
|
||||||
case @state
|
|
||||||
when STATE_USER
|
|
||||||
@state = STATE_PASSWORD
|
|
||||||
return @user
|
|
||||||
when STATE_PASSWORD
|
|
||||||
return @password
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
STATE_USER = :USER
|
|
||||||
STATE_PASSWORD = :PASSWORD
|
|
||||||
|
|
||||||
def initialize(user, password)
|
|
||||||
@user = user
|
|
||||||
@password = password
|
|
||||||
@state = STATE_USER
|
|
||||||
end
|
|
||||||
end
|
|
||||||
add_authenticator "LOGIN", LoginAuthenticator
|
|
||||||
|
|
||||||
# Authenticator for the "PLAIN" authentication type. See
|
|
||||||
# #authenticate().
|
|
||||||
class PlainAuthenticator
|
|
||||||
def process(data)
|
|
||||||
return "\0#{@user}\0#{@password}"
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def initialize(user, password)
|
|
||||||
@user = user
|
|
||||||
@password = password
|
|
||||||
end
|
|
||||||
end
|
|
||||||
add_authenticator "PLAIN", PlainAuthenticator
|
|
||||||
|
|
||||||
# Authenticator for the "CRAM-MD5" authentication type. See
|
|
||||||
# #authenticate().
|
|
||||||
class CramMD5Authenticator
|
|
||||||
def process(challenge)
|
|
||||||
digest = hmac_md5(challenge, @password)
|
|
||||||
return @user + " " + digest
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def initialize(user, password)
|
|
||||||
@user = user
|
|
||||||
@password = password
|
|
||||||
end
|
|
||||||
|
|
||||||
def hmac_md5(text, key)
|
|
||||||
if key.length > 64
|
|
||||||
key = Digest::MD5.digest(key)
|
|
||||||
end
|
|
||||||
|
|
||||||
k_ipad = key + "\0" * (64 - key.length)
|
|
||||||
k_opad = key + "\0" * (64 - key.length)
|
|
||||||
for i in 0..63
|
|
||||||
k_ipad[i] = (k_ipad[i].ord ^ 0x36).chr
|
|
||||||
k_opad[i] = (k_opad[i].ord ^ 0x5c).chr
|
|
||||||
end
|
|
||||||
|
|
||||||
digest = Digest::MD5.digest(k_ipad + text)
|
|
||||||
|
|
||||||
return Digest::MD5.hexdigest(k_opad + digest)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
add_authenticator "CRAM-MD5", CramMD5Authenticator
|
|
||||||
|
|
||||||
# Authenticator for the "DIGEST-MD5" authentication type. See
|
|
||||||
# #authenticate().
|
|
||||||
class DigestMD5Authenticator
|
|
||||||
def process(challenge)
|
|
||||||
case @stage
|
|
||||||
when STAGE_ONE
|
|
||||||
@stage = STAGE_TWO
|
|
||||||
sparams = {}
|
|
||||||
c = StringScanner.new(challenge)
|
|
||||||
while c.scan(/(?:\s*,)?\s*(\w+)=("(?:[^\\"]+|\\.)*"|[^,]+)\s*/)
|
|
||||||
k, v = c[1], c[2]
|
|
||||||
if v =~ /^"(.*)"$/
|
|
||||||
v = $1
|
|
||||||
if v =~ /,/
|
|
||||||
v = v.split(',')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
sparams[k] = v
|
|
||||||
end
|
|
||||||
|
|
||||||
raise DataFormatError, "Bad Challenge: '#{challenge}'" unless c.rest.size == 0
|
|
||||||
raise Error, "Server does not support auth (qop = #{sparams['qop'].join(',')})" unless sparams['qop'].include?("auth")
|
|
||||||
|
|
||||||
response = {
|
|
||||||
:nonce => sparams['nonce'],
|
|
||||||
:username => @user,
|
|
||||||
:realm => sparams['realm'],
|
|
||||||
:cnonce => Digest::MD5.hexdigest("%.15f:%.15f:%d" % [Time.now.to_f, rand, Process.pid.to_s]),
|
|
||||||
:'digest-uri' => 'imap/' + sparams['realm'],
|
|
||||||
:qop => 'auth',
|
|
||||||
:maxbuf => 65535,
|
|
||||||
:nc => "%08d" % nc(sparams['nonce']),
|
|
||||||
:charset => sparams['charset'],
|
|
||||||
}
|
|
||||||
|
|
||||||
response[:authzid] = @authname unless @authname.nil?
|
|
||||||
|
|
||||||
# now, the real thing
|
|
||||||
a0 = Digest::MD5.digest( [ response.values_at(:username, :realm), @password ].join(':') )
|
|
||||||
|
|
||||||
a1 = [ a0, response.values_at(:nonce,:cnonce) ].join(':')
|
|
||||||
a1 << ':' + response[:authzid] unless response[:authzid].nil?
|
|
||||||
|
|
||||||
a2 = "AUTHENTICATE:" + response[:'digest-uri']
|
|
||||||
a2 << ":00000000000000000000000000000000" if response[:qop] and response[:qop] =~ /^auth-(?:conf|int)$/
|
|
||||||
|
|
||||||
response[:response] = Digest::MD5.hexdigest(
|
|
||||||
[
|
|
||||||
Digest::MD5.hexdigest(a1),
|
|
||||||
response.values_at(:nonce, :nc, :cnonce, :qop),
|
|
||||||
Digest::MD5.hexdigest(a2)
|
|
||||||
].join(':')
|
|
||||||
)
|
|
||||||
|
|
||||||
return response.keys.map {|key| qdval(key.to_s, response[key]) }.join(',')
|
|
||||||
when STAGE_TWO
|
|
||||||
@stage = nil
|
|
||||||
# if at the second stage, return an empty string
|
|
||||||
if challenge =~ /rspauth=/
|
|
||||||
return ''
|
|
||||||
else
|
|
||||||
raise ResponseParseError, challenge
|
|
||||||
end
|
|
||||||
else
|
|
||||||
raise ResponseParseError, challenge
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def initialize(user, password, authname = nil)
|
|
||||||
@user, @password, @authname = user, password, authname
|
|
||||||
@nc, @stage = {}, STAGE_ONE
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
STAGE_ONE = :stage_one
|
|
||||||
STAGE_TWO = :stage_two
|
|
||||||
|
|
||||||
def nc(nonce)
|
|
||||||
if @nc.has_key? nonce
|
|
||||||
@nc[nonce] = @nc[nonce] + 1
|
|
||||||
else
|
|
||||||
@nc[nonce] = 1
|
|
||||||
end
|
|
||||||
return @nc[nonce]
|
|
||||||
end
|
|
||||||
|
|
||||||
# some responses need quoting
|
|
||||||
def qdval(k, v)
|
|
||||||
return if k.nil? or v.nil?
|
|
||||||
if %w"username authzid realm nonce cnonce digest-uri qop".include? k
|
|
||||||
v.gsub!(/([\\"])/, "\\\1")
|
|
||||||
return '%s="%s"' % [k, v]
|
|
||||||
else
|
|
||||||
return '%s=%s' % [k, v]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
add_authenticator "DIGEST-MD5", DigestMD5Authenticator
|
|
||||||
|
|
||||||
# Superclass of IMAP errors.
|
# Superclass of IMAP errors.
|
||||||
class Error < StandardError
|
class Error < StandardError
|
||||||
end
|
end
|
||||||
|
@ -4130,3 +3926,5 @@ module Net
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
require_relative "imap/authenticators"
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Registry for SASL authenticators used by Net::IMAP.
|
||||||
|
module Net::IMAP::Authenticators
|
||||||
|
|
||||||
|
# Adds an authenticator for Net::IMAP#authenticate. +auth_type+ is the
|
||||||
|
# {SASL mechanism}[https://www.iana.org/assignments/sasl-mechanisms/sasl-mechanisms.xhtml]
|
||||||
|
# supported by +authenticator+ (for instance, "+LOGIN+"). The +authenticator+
|
||||||
|
# is an object which defines a +#process+ method to handle authentication with
|
||||||
|
# the server. See Net::IMAP::LoginAuthenticator,
|
||||||
|
# Net::IMAP::CramMD5Authenticator, and Net::IMAP::DigestMD5Authenticator for
|
||||||
|
# examples.
|
||||||
|
#
|
||||||
|
# If +auth_type+ refers to an existing authenticator, it will be
|
||||||
|
# replaced by the new one.
|
||||||
|
def add_authenticator(auth_type, authenticator)
|
||||||
|
authenticators[auth_type] = authenticator
|
||||||
|
end
|
||||||
|
|
||||||
|
# Builds an authenticator for Net::IMAP#authenticate. +args+ will be passed
|
||||||
|
# directly to the chosen authenticator's +#initialize+.
|
||||||
|
def authenticator(auth_type, *args)
|
||||||
|
auth_type = auth_type.upcase
|
||||||
|
unless authenticators.has_key?(auth_type)
|
||||||
|
raise ArgumentError,
|
||||||
|
format('unknown auth type - "%s"', auth_type)
|
||||||
|
end
|
||||||
|
authenticators[auth_type].new(*args)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def authenticators
|
||||||
|
@authenticators ||= {}
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
Net::IMAP.extend Net::IMAP::Authenticators
|
||||||
|
|
||||||
|
require_relative "authenticators/login"
|
||||||
|
require_relative "authenticators/plain"
|
||||||
|
require_relative "authenticators/cram_md5"
|
||||||
|
require_relative "authenticators/digest_md5"
|
|
@ -0,0 +1,47 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "digest/md5"
|
||||||
|
|
||||||
|
# Authenticator for the "+CRAM-MD5+" SASL mechanism. See
|
||||||
|
# Net::IMAP#authenticate.
|
||||||
|
#
|
||||||
|
# == Deprecated
|
||||||
|
#
|
||||||
|
# +CRAM-MD5+ should be considered obsolete and insecure. It is included for
|
||||||
|
# backward compatibility with historic servers.
|
||||||
|
# {draft-ietf-sasl-crammd5-to-historic}[https://tools.ietf.org/html/draft-ietf-sasl-crammd5-to-historic-00.html]
|
||||||
|
# recommends using +SCRAM-*+ or +PLAIN+ protected by TLS instead. Additionally,
|
||||||
|
# RFC8314[https://tools.ietf.org/html/rfc8314] discourage the use of cleartext
|
||||||
|
# and recommends TLS version 1.2 or greater be used for all traffic.
|
||||||
|
class Net::IMAP::CramMD5Authenticator
|
||||||
|
def process(challenge)
|
||||||
|
digest = hmac_md5(challenge, @password)
|
||||||
|
return @user + " " + digest
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def initialize(user, password)
|
||||||
|
@user = user
|
||||||
|
@password = password
|
||||||
|
end
|
||||||
|
|
||||||
|
def hmac_md5(text, key)
|
||||||
|
if key.length > 64
|
||||||
|
key = Digest::MD5.digest(key)
|
||||||
|
end
|
||||||
|
|
||||||
|
k_ipad = key + "\0" * (64 - key.length)
|
||||||
|
k_opad = key + "\0" * (64 - key.length)
|
||||||
|
for i in 0..63
|
||||||
|
k_ipad[i] = (k_ipad[i].ord ^ 0x36).chr
|
||||||
|
k_opad[i] = (k_opad[i].ord ^ 0x5c).chr
|
||||||
|
end
|
||||||
|
|
||||||
|
digest = Digest::MD5.digest(k_ipad + text)
|
||||||
|
|
||||||
|
return Digest::MD5.hexdigest(k_opad + digest)
|
||||||
|
end
|
||||||
|
|
||||||
|
Net::IMAP.add_authenticator "PLAIN", self
|
||||||
|
end
|
|
@ -0,0 +1,111 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "digest/md5"
|
||||||
|
require "strscan"
|
||||||
|
|
||||||
|
# Net::IMAP authenticator for the "`DIGEST-MD5`" SASL mechanism type. See
|
||||||
|
# Net::IMAP#authenticate.
|
||||||
|
#
|
||||||
|
# == Deprecated
|
||||||
|
#
|
||||||
|
# "+DIGEST-MD5+" has been deprecated by
|
||||||
|
# {RFC6331}[https://tools.ietf.org/html/rfc6331] and should not be used. It
|
||||||
|
# is included for backward compatibility with historic servers.
|
||||||
|
class Net::IMAP::DigestMD5Authenticator
|
||||||
|
def process(challenge)
|
||||||
|
case @stage
|
||||||
|
when STAGE_ONE
|
||||||
|
@stage = STAGE_TWO
|
||||||
|
sparams = {}
|
||||||
|
c = StringScanner.new(challenge)
|
||||||
|
while c.scan(/(?:\s*,)?\s*(\w+)=("(?:[^\\"]+|\\.)*"|[^,]+)\s*/)
|
||||||
|
k, v = c[1], c[2]
|
||||||
|
if v =~ /^"(.*)"$/
|
||||||
|
v = $1
|
||||||
|
if v =~ /,/
|
||||||
|
v = v.split(',')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
sparams[k] = v
|
||||||
|
end
|
||||||
|
|
||||||
|
raise DataFormatError, "Bad Challenge: '#{challenge}'" unless c.rest.size == 0
|
||||||
|
raise Error, "Server does not support auth (qop = #{sparams['qop'].join(',')})" unless sparams['qop'].include?("auth")
|
||||||
|
|
||||||
|
response = {
|
||||||
|
:nonce => sparams['nonce'],
|
||||||
|
:username => @user,
|
||||||
|
:realm => sparams['realm'],
|
||||||
|
:cnonce => Digest::MD5.hexdigest("%.15f:%.15f:%d" % [Time.now.to_f, rand, Process.pid.to_s]),
|
||||||
|
:'digest-uri' => 'imap/' + sparams['realm'],
|
||||||
|
:qop => 'auth',
|
||||||
|
:maxbuf => 65535,
|
||||||
|
:nc => "%08d" % nc(sparams['nonce']),
|
||||||
|
:charset => sparams['charset'],
|
||||||
|
}
|
||||||
|
|
||||||
|
response[:authzid] = @authname unless @authname.nil?
|
||||||
|
|
||||||
|
# now, the real thing
|
||||||
|
a0 = Digest::MD5.digest( [ response.values_at(:username, :realm), @password ].join(':') )
|
||||||
|
|
||||||
|
a1 = [ a0, response.values_at(:nonce,:cnonce) ].join(':')
|
||||||
|
a1 << ':' + response[:authzid] unless response[:authzid].nil?
|
||||||
|
|
||||||
|
a2 = "AUTHENTICATE:" + response[:'digest-uri']
|
||||||
|
a2 << ":00000000000000000000000000000000" if response[:qop] and response[:qop] =~ /^auth-(?:conf|int)$/
|
||||||
|
|
||||||
|
response[:response] = Digest::MD5.hexdigest(
|
||||||
|
[
|
||||||
|
Digest::MD5.hexdigest(a1),
|
||||||
|
response.values_at(:nonce, :nc, :cnonce, :qop),
|
||||||
|
Digest::MD5.hexdigest(a2)
|
||||||
|
].join(':')
|
||||||
|
)
|
||||||
|
|
||||||
|
return response.keys.map {|key| qdval(key.to_s, response[key]) }.join(',')
|
||||||
|
when STAGE_TWO
|
||||||
|
@stage = nil
|
||||||
|
# if at the second stage, return an empty string
|
||||||
|
if challenge =~ /rspauth=/
|
||||||
|
return ''
|
||||||
|
else
|
||||||
|
raise ResponseParseError, challenge
|
||||||
|
end
|
||||||
|
else
|
||||||
|
raise ResponseParseError, challenge
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(user, password, authname = nil)
|
||||||
|
@user, @password, @authname = user, password, authname
|
||||||
|
@nc, @stage = {}, STAGE_ONE
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
STAGE_ONE = :stage_one
|
||||||
|
STAGE_TWO = :stage_two
|
||||||
|
|
||||||
|
def nc(nonce)
|
||||||
|
if @nc.has_key? nonce
|
||||||
|
@nc[nonce] = @nc[nonce] + 1
|
||||||
|
else
|
||||||
|
@nc[nonce] = 1
|
||||||
|
end
|
||||||
|
return @nc[nonce]
|
||||||
|
end
|
||||||
|
|
||||||
|
# some responses need quoting
|
||||||
|
def qdval(k, v)
|
||||||
|
return if k.nil? or v.nil?
|
||||||
|
if %w"username authzid realm nonce cnonce digest-uri qop".include? k
|
||||||
|
v.gsub!(/([\\"])/, "\\\1")
|
||||||
|
return '%s="%s"' % [k, v]
|
||||||
|
else
|
||||||
|
return '%s=%s' % [k, v]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Net::IMAP.add_authenticator "DIGEST-MD5", self
|
||||||
|
end
|
|
@ -0,0 +1,34 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Authenticator for the "+LOGIN+" SASL mechanism. See Net::IMAP#authenticate.
|
||||||
|
#
|
||||||
|
# == Deprecated
|
||||||
|
#
|
||||||
|
# The {SASL mechanisms
|
||||||
|
# registry}[https://www.iana.org/assignments/sasl-mechanisms/sasl-mechanisms.xhtml]
|
||||||
|
# marks "LOGIN" as obsoleted in favor of "PLAIN". See also
|
||||||
|
# {draft-murchison-sasl-login}[https://www.iana.org/go/draft-murchison-sasl-login].
|
||||||
|
class Net::IMAP::LoginAuthenticator
|
||||||
|
def process(data)
|
||||||
|
case @state
|
||||||
|
when STATE_USER
|
||||||
|
@state = STATE_PASSWORD
|
||||||
|
return @user
|
||||||
|
when STATE_PASSWORD
|
||||||
|
return @password
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
STATE_USER = :USER
|
||||||
|
STATE_PASSWORD = :PASSWORD
|
||||||
|
|
||||||
|
def initialize(user, password)
|
||||||
|
@user = user
|
||||||
|
@password = password
|
||||||
|
@state = STATE_USER
|
||||||
|
end
|
||||||
|
|
||||||
|
Net::IMAP.add_authenticator "LOGIN", self
|
||||||
|
end
|
|
@ -0,0 +1,19 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Authenticator for the "+PLAIN+" SASL mechanism. See Net::IMAP#authenticate.
|
||||||
|
#
|
||||||
|
# See RFC4616[https://tools.ietf.org/html/rfc4616] for the specification.
|
||||||
|
class Net::IMAP::PlainAuthenticator
|
||||||
|
def process(data)
|
||||||
|
return "\0#{@user}\0#{@password}"
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def initialize(user, password)
|
||||||
|
@user = user
|
||||||
|
@password = password
|
||||||
|
end
|
||||||
|
|
||||||
|
Net::IMAP.add_authenticator "PLAIN", self
|
||||||
|
end
|
Загрузка…
Ссылка в новой задаче