dalli 2.7.11 → 3.0.6
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of dalli might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/Gemfile +7 -6
- data/History.md +69 -0
- data/README.md +26 -200
- data/lib/dalli/cas/client.rb +1 -57
- data/lib/dalli/client.rb +272 -209
- data/lib/dalli/compressor.rb +12 -2
- data/lib/dalli/key_manager.rb +113 -0
- data/lib/dalli/options.rb +3 -4
- data/lib/dalli/protocol/binary/request_formatter.rb +109 -0
- data/lib/dalli/protocol/binary/response_processor.rb +149 -0
- data/lib/dalli/protocol/binary/sasl_authentication.rb +60 -0
- data/lib/dalli/protocol/binary.rb +544 -0
- data/lib/dalli/protocol/server_config_parser.rb +84 -0
- data/lib/dalli/protocol/ttl_sanitizer.rb +45 -0
- data/lib/dalli/protocol/value_compressor.rb +85 -0
- data/lib/dalli/protocol/value_marshaller.rb +59 -0
- data/lib/dalli/protocol/value_serializer.rb +91 -0
- data/lib/dalli/protocol.rb +8 -0
- data/lib/dalli/ring.rb +86 -81
- data/lib/dalli/server.rb +3 -749
- data/lib/dalli/servers_arg_normalizer.rb +54 -0
- data/lib/dalli/socket.rb +115 -137
- data/lib/dalli/version.rb +4 -1
- data/lib/dalli.rb +32 -14
- data/lib/rack/session/dalli.rb +46 -55
- metadata +103 -10
- data/lib/action_dispatch/middleware/session/dalli_store.rb +0 -82
- data/lib/active_support/cache/dalli_store.rb +0 -441
- data/lib/dalli/railtie.rb +0 -8
data/lib/dalli/compressor.rb
CHANGED
@@ -1,8 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'zlib'
|
3
4
|
require 'stringio'
|
4
5
|
|
5
6
|
module Dalli
|
7
|
+
##
|
8
|
+
# Default compressor used by Dalli, that uses
|
9
|
+
# Zlib DEFLATE to compress data.
|
10
|
+
##
|
6
11
|
class Compressor
|
7
12
|
def self.compress(data)
|
8
13
|
Zlib::Deflate.deflate(data)
|
@@ -13,9 +18,14 @@ module Dalli
|
|
13
18
|
end
|
14
19
|
end
|
15
20
|
|
21
|
+
##
|
22
|
+
# Alternate compressor for Dalli, that uses
|
23
|
+
# Gzip. Gzip adds a checksum to each compressed
|
24
|
+
# entry.
|
25
|
+
##
|
16
26
|
class GzipCompressor
|
17
27
|
def self.compress(data)
|
18
|
-
io = StringIO.new(
|
28
|
+
io = StringIO.new(+'', 'w')
|
19
29
|
gz = Zlib::GzipWriter.new(io)
|
20
30
|
gz.write(data)
|
21
31
|
gz.close
|
@@ -23,7 +33,7 @@ module Dalli
|
|
23
33
|
end
|
24
34
|
|
25
35
|
def self.decompress(data)
|
26
|
-
io = StringIO.new(data,
|
36
|
+
io = StringIO.new(data, 'rb')
|
27
37
|
Zlib::GzipReader.new(io).read
|
28
38
|
end
|
29
39
|
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'digest/md5'
|
4
|
+
|
5
|
+
module Dalli
|
6
|
+
##
|
7
|
+
# This class manages and validates keys sent to Memcached, ensuring
|
8
|
+
# that they meet Memcached key length requirements, and supporting
|
9
|
+
# the implementation of optional namespaces on a per-Dalli client
|
10
|
+
# basis.
|
11
|
+
##
|
12
|
+
class KeyManager
|
13
|
+
MAX_KEY_LENGTH = 250
|
14
|
+
|
15
|
+
NAMESPACE_SEPARATOR = ':'
|
16
|
+
|
17
|
+
# This is a hard coded md5 for historical reasons
|
18
|
+
TRUNCATED_KEY_SEPARATOR = ':md5:'
|
19
|
+
|
20
|
+
# This is 249 for historical reasons
|
21
|
+
TRUNCATED_KEY_TARGET_SIZE = 249
|
22
|
+
|
23
|
+
DEFAULTS = {
|
24
|
+
digest_class: ::Digest::MD5
|
25
|
+
}.freeze
|
26
|
+
|
27
|
+
OPTIONS = %i[digest_class namespace].freeze
|
28
|
+
|
29
|
+
attr_reader :namespace
|
30
|
+
|
31
|
+
def initialize(client_options)
|
32
|
+
@key_options =
|
33
|
+
DEFAULTS.merge(client_options.select { |k, _| OPTIONS.include?(k) })
|
34
|
+
validate_digest_class_option(@key_options)
|
35
|
+
|
36
|
+
@namespace = namespace_from_options
|
37
|
+
end
|
38
|
+
|
39
|
+
##
|
40
|
+
# Validates the key, and transforms as needed.
|
41
|
+
#
|
42
|
+
# If the key is nil or empty, raises ArgumentError. Whitespace
|
43
|
+
# characters are allowed for historical reasons, but likely shouldn't
|
44
|
+
# be used.
|
45
|
+
# If the key (with namespace) is shorter than the memcached maximum
|
46
|
+
# allowed key length, just returns the argument key
|
47
|
+
# Otherwise computes a "truncated" key that uses a truncated prefix
|
48
|
+
# combined with a 32-byte hex digest of the whole key.
|
49
|
+
##
|
50
|
+
def validate_key(key)
|
51
|
+
raise ArgumentError, 'key cannot be blank' unless key&.length&.positive?
|
52
|
+
|
53
|
+
key = key_with_namespace(key)
|
54
|
+
key.length > MAX_KEY_LENGTH ? truncated_key(key) : key
|
55
|
+
end
|
56
|
+
|
57
|
+
##
|
58
|
+
# Returns the key with the namespace prefixed, if a namespace is
|
59
|
+
# defined. Otherwise just returns the key
|
60
|
+
##
|
61
|
+
def key_with_namespace(key)
|
62
|
+
return key if namespace.nil?
|
63
|
+
|
64
|
+
"#{namespace}#{NAMESPACE_SEPARATOR}#{key}"
|
65
|
+
end
|
66
|
+
|
67
|
+
def key_without_namespace(key)
|
68
|
+
return key if namespace.nil?
|
69
|
+
|
70
|
+
key.sub(namespace_regexp, '')
|
71
|
+
end
|
72
|
+
|
73
|
+
def digest_class
|
74
|
+
@digest_class ||= @key_options[:digest_class]
|
75
|
+
end
|
76
|
+
|
77
|
+
def namespace_regexp
|
78
|
+
@namespace_regexp ||= /\A#{Regexp.escape(namespace)}:/.freeze unless namespace.nil?
|
79
|
+
end
|
80
|
+
|
81
|
+
def validate_digest_class_option(opts)
|
82
|
+
return if opts[:digest_class].respond_to?(:hexdigest)
|
83
|
+
|
84
|
+
raise ArgumentError, 'The digest_class object must respond to the hexdigest method'
|
85
|
+
end
|
86
|
+
|
87
|
+
def namespace_from_options
|
88
|
+
raw_namespace = @key_options[:namespace]
|
89
|
+
return nil unless raw_namespace
|
90
|
+
return raw_namespace.call.to_s if raw_namespace.is_a?(Proc)
|
91
|
+
|
92
|
+
raw_namespace.to_s
|
93
|
+
end
|
94
|
+
|
95
|
+
##
|
96
|
+
# Produces a truncated key, if the raw key is longer than the maximum allowed
|
97
|
+
# length. The truncated key is produced by generating a hex digest
|
98
|
+
# of the key, and appending that to a truncated section of the key.
|
99
|
+
##
|
100
|
+
def truncated_key(key)
|
101
|
+
digest = digest_class.hexdigest(key)
|
102
|
+
"#{key[0, prefix_length(digest)]}#{TRUNCATED_KEY_SEPARATOR}#{digest}"
|
103
|
+
end
|
104
|
+
|
105
|
+
def prefix_length(digest)
|
106
|
+
return TRUNCATED_KEY_TARGET_SIZE - (TRUNCATED_KEY_SEPARATOR.length + digest.length) if namespace.nil?
|
107
|
+
|
108
|
+
# For historical reasons, truncated keys with namespaces had a length of 250 rather
|
109
|
+
# than 249
|
110
|
+
TRUNCATED_KEY_TARGET_SIZE + 1 - (TRUNCATED_KEY_SEPARATOR.length + digest.length)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
data/lib/dalli/options.rb
CHANGED
@@ -1,20 +1,19 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
|
2
|
+
|
3
3
|
require 'monitor'
|
4
4
|
|
5
5
|
module Dalli
|
6
|
-
|
7
6
|
# Make Dalli threadsafe by using a lock around all
|
8
7
|
# public server methods.
|
9
8
|
#
|
10
|
-
# Dalli::
|
9
|
+
# Dalli::Protocol::Binary.extend(Dalli::Threadsafe)
|
11
10
|
#
|
12
11
|
module Threadsafe
|
13
12
|
def self.extended(obj)
|
14
13
|
obj.init_threadsafe
|
15
14
|
end
|
16
15
|
|
17
|
-
def request(
|
16
|
+
def request(opcode, *args)
|
18
17
|
@lock.synchronize do
|
19
18
|
super
|
20
19
|
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dalli
|
4
|
+
module Protocol
|
5
|
+
class Binary
|
6
|
+
##
|
7
|
+
# Class that encapsulates logic for formatting binary protocol requests
|
8
|
+
# to memcached.
|
9
|
+
##
|
10
|
+
class RequestFormatter
|
11
|
+
REQUEST = 0x80
|
12
|
+
|
13
|
+
OPCODES = {
|
14
|
+
get: 0x00,
|
15
|
+
set: 0x01,
|
16
|
+
add: 0x02,
|
17
|
+
replace: 0x03,
|
18
|
+
delete: 0x04,
|
19
|
+
incr: 0x05,
|
20
|
+
decr: 0x06,
|
21
|
+
flush: 0x08,
|
22
|
+
noop: 0x0A,
|
23
|
+
version: 0x0B,
|
24
|
+
getkq: 0x0D,
|
25
|
+
append: 0x0E,
|
26
|
+
prepend: 0x0F,
|
27
|
+
stat: 0x10,
|
28
|
+
setq: 0x11,
|
29
|
+
addq: 0x12,
|
30
|
+
replaceq: 0x13,
|
31
|
+
deleteq: 0x14,
|
32
|
+
incrq: 0x15,
|
33
|
+
decrq: 0x16,
|
34
|
+
auth_negotiation: 0x20,
|
35
|
+
auth_request: 0x21,
|
36
|
+
auth_continue: 0x22,
|
37
|
+
touch: 0x1C,
|
38
|
+
gat: 0x1D
|
39
|
+
}.freeze
|
40
|
+
|
41
|
+
REQ_HEADER_FORMAT = 'CCnCCnNNQ'
|
42
|
+
|
43
|
+
KEY_ONLY = 'a*'
|
44
|
+
TTL_AND_KEY = 'Na*'
|
45
|
+
KEY_AND_VALUE = 'a*a*'
|
46
|
+
INCR_DECR = 'NNNNNa*'
|
47
|
+
TTL_ONLY = 'N'
|
48
|
+
NO_BODY = ''
|
49
|
+
|
50
|
+
BODY_FORMATS = {
|
51
|
+
get: KEY_ONLY,
|
52
|
+
getkq: KEY_ONLY,
|
53
|
+
delete: KEY_ONLY,
|
54
|
+
deleteq: KEY_ONLY,
|
55
|
+
stat: KEY_ONLY,
|
56
|
+
|
57
|
+
append: KEY_AND_VALUE,
|
58
|
+
prepend: KEY_AND_VALUE,
|
59
|
+
auth_request: KEY_AND_VALUE,
|
60
|
+
auth_continue: KEY_AND_VALUE,
|
61
|
+
|
62
|
+
set: 'NNa*a*',
|
63
|
+
setq: 'NNa*a*',
|
64
|
+
add: 'NNa*a*',
|
65
|
+
addq: 'NNa*a*',
|
66
|
+
replace: 'NNa*a*',
|
67
|
+
replaceq: 'NNa*a*',
|
68
|
+
|
69
|
+
incr: INCR_DECR,
|
70
|
+
decr: INCR_DECR,
|
71
|
+
|
72
|
+
flush: TTL_ONLY,
|
73
|
+
|
74
|
+
noop: NO_BODY,
|
75
|
+
auth_negotiation: NO_BODY,
|
76
|
+
version: NO_BODY,
|
77
|
+
|
78
|
+
touch: TTL_AND_KEY,
|
79
|
+
gat: TTL_AND_KEY
|
80
|
+
}.freeze
|
81
|
+
FORMAT = BODY_FORMATS.transform_values { |v| REQ_HEADER_FORMAT + v; }
|
82
|
+
|
83
|
+
# rubocop:disable Metrics/ParameterLists
|
84
|
+
def self.standard_request(opkey:, key: nil, value: nil, opaque: 0, cas: 0, bitflags: nil, ttl: nil)
|
85
|
+
extra_len = (bitflags.nil? ? 0 : 4) + (ttl.nil? ? 0 : 4)
|
86
|
+
key_len = key.nil? ? 0 : key.bytesize
|
87
|
+
value_len = value.nil? ? 0 : value.bytesize
|
88
|
+
header = [REQUEST, OPCODES[opkey], key_len, extra_len, 0, 0, extra_len + key_len + value_len, opaque, cas]
|
89
|
+
body = [bitflags, ttl, key, value].reject(&:nil?)
|
90
|
+
(header + body).pack(FORMAT[opkey])
|
91
|
+
end
|
92
|
+
# rubocop:enable Metrics/ParameterLists
|
93
|
+
|
94
|
+
def self.decr_incr_request(opkey:, key: nil, count: nil, initial: nil, expiry: nil)
|
95
|
+
extra_len = 20
|
96
|
+
(h, l) = as_8byte_uint(count)
|
97
|
+
(dh, dl) = as_8byte_uint(initial)
|
98
|
+
header = [REQUEST, OPCODES[opkey], key.bytesize, extra_len, 0, 0, key.bytesize + extra_len, 0, 0]
|
99
|
+
body = [h, l, dh, dl, expiry, key]
|
100
|
+
(header + body).pack(FORMAT[opkey])
|
101
|
+
end
|
102
|
+
|
103
|
+
def self.as_8byte_uint(val)
|
104
|
+
[val >> 32, 0xFFFFFFFF & val]
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dalli
|
4
|
+
module Protocol
|
5
|
+
class Binary
|
6
|
+
##
|
7
|
+
# Class that encapsulates logic for processing binary protocol responses
|
8
|
+
# from memcached. Includes logic for pulling data from an IO source
|
9
|
+
# and parsing into local values. Handles errors on unexpected values.
|
10
|
+
##
|
11
|
+
class ResponseProcessor
|
12
|
+
RESP_HEADER = '@2nCCnNNQ'
|
13
|
+
RESP_HEADER_SIZE = 24
|
14
|
+
|
15
|
+
# Response codes taken from:
|
16
|
+
# https://github.com/memcached/memcached/wiki/BinaryProtocolRevamped#response-status
|
17
|
+
RESPONSE_CODES = {
|
18
|
+
0 => 'No error',
|
19
|
+
1 => 'Key not found',
|
20
|
+
2 => 'Key exists',
|
21
|
+
3 => 'Value too large',
|
22
|
+
4 => 'Invalid arguments',
|
23
|
+
5 => 'Item not stored',
|
24
|
+
6 => 'Incr/decr on a non-numeric value',
|
25
|
+
7 => 'The vbucket belongs to another server',
|
26
|
+
8 => 'Authentication error',
|
27
|
+
9 => 'Authentication continue',
|
28
|
+
0x20 => 'Authentication required',
|
29
|
+
0x81 => 'Unknown command',
|
30
|
+
0x82 => 'Out of memory',
|
31
|
+
0x83 => 'Not supported',
|
32
|
+
0x84 => 'Internal error',
|
33
|
+
0x85 => 'Busy',
|
34
|
+
0x86 => 'Temporary failure'
|
35
|
+
}.freeze
|
36
|
+
|
37
|
+
def initialize(io_source, value_marshaller)
|
38
|
+
@io_source = io_source
|
39
|
+
@value_marshaller = value_marshaller
|
40
|
+
end
|
41
|
+
|
42
|
+
def read(num_bytes)
|
43
|
+
@io_source.read(num_bytes)
|
44
|
+
end
|
45
|
+
|
46
|
+
def read_response
|
47
|
+
status, extra_len, key_len, body_len, cas = unpack_header(read_header)
|
48
|
+
body = read(body_len) if body_len.positive?
|
49
|
+
[status, extra_len, body, cas, key_len]
|
50
|
+
end
|
51
|
+
|
52
|
+
def unpack_header(header)
|
53
|
+
(key_len, extra_len, _, status, body_len, _, cas) = header.unpack(RESP_HEADER)
|
54
|
+
[status, extra_len, key_len, body_len, cas]
|
55
|
+
end
|
56
|
+
|
57
|
+
def unpack_response_body(extra_len, key_len, body, unpack)
|
58
|
+
bitflags = extra_len.positive? ? body.byteslice(0, extra_len).unpack1('N') : 0x0
|
59
|
+
key = body.byteslice(extra_len, key_len) if key_len.positive?
|
60
|
+
value = body.byteslice(extra_len + key_len, body.bytesize - (extra_len + key_len))
|
61
|
+
value = unpack ? @value_marshaller.retrieve(value, bitflags) : value
|
62
|
+
[key, value]
|
63
|
+
end
|
64
|
+
|
65
|
+
def read_header
|
66
|
+
read(RESP_HEADER_SIZE) || raise(Dalli::NetworkError, 'No response')
|
67
|
+
end
|
68
|
+
|
69
|
+
def not_found?(status)
|
70
|
+
status == 1
|
71
|
+
end
|
72
|
+
|
73
|
+
NOT_STORED_STATUSES = [2, 5].freeze
|
74
|
+
def not_stored?(status)
|
75
|
+
NOT_STORED_STATUSES.include?(status)
|
76
|
+
end
|
77
|
+
|
78
|
+
def raise_on_not_ok_status!(status)
|
79
|
+
return if status.zero?
|
80
|
+
|
81
|
+
raise Dalli::DalliError, "Response error #{status}: #{RESPONSE_CODES[status]}"
|
82
|
+
end
|
83
|
+
|
84
|
+
def generic_response(unpack: false, cache_nils: false)
|
85
|
+
status, extra_len, body, _, key_len = read_response
|
86
|
+
|
87
|
+
return cache_nils ? ::Dalli::NOT_FOUND : nil if not_found?(status)
|
88
|
+
return false if not_stored?(status) # Not stored, normal status for add operation
|
89
|
+
|
90
|
+
raise_on_not_ok_status!(status)
|
91
|
+
return true unless body
|
92
|
+
|
93
|
+
unpack_response_body(extra_len, key_len, body, unpack).last
|
94
|
+
end
|
95
|
+
|
96
|
+
def data_cas_response
|
97
|
+
status, extra_len, body, cas, key_len = read_response
|
98
|
+
return [nil, cas] if not_found?(status)
|
99
|
+
return [nil, false] if not_stored?(status)
|
100
|
+
|
101
|
+
raise_on_not_ok_status!(status)
|
102
|
+
return [nil, cas] unless body
|
103
|
+
|
104
|
+
[unpack_response_body(extra_len, key_len, body, true).last, cas]
|
105
|
+
end
|
106
|
+
|
107
|
+
def cas_response
|
108
|
+
data_cas_response.last
|
109
|
+
end
|
110
|
+
|
111
|
+
def multi_with_keys_response
|
112
|
+
hash = {}
|
113
|
+
loop do
|
114
|
+
status, extra_len, body, _, key_len = read_response
|
115
|
+
# This is the response to the terminating noop / end of stat
|
116
|
+
return hash if status.zero? && key_len.zero?
|
117
|
+
|
118
|
+
# Ignore any responses with non-zero status codes,
|
119
|
+
# such as errors from set operations. That allows
|
120
|
+
# this code to be used at the end of a multi
|
121
|
+
# block to clear any error responses from inside the multi.
|
122
|
+
next unless status.zero?
|
123
|
+
|
124
|
+
key, value = unpack_response_body(extra_len, key_len, body, true)
|
125
|
+
hash[key] = value
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def decr_incr_response
|
130
|
+
body = generic_response
|
131
|
+
body ? body.unpack1('Q>') : body
|
132
|
+
end
|
133
|
+
|
134
|
+
def validate_auth_format(extra_len, count)
|
135
|
+
return if extra_len.zero?
|
136
|
+
|
137
|
+
raise Dalli::NetworkError, "Unexpected message format: #{extra_len} #{count}"
|
138
|
+
end
|
139
|
+
|
140
|
+
def auth_response
|
141
|
+
(_, extra_len, _, status, body_len,) = read_header.unpack(RESP_HEADER)
|
142
|
+
validate_auth_format(extra_len, body_len)
|
143
|
+
content = read(body_len) if body_len.positive?
|
144
|
+
[status, content]
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dalli
|
4
|
+
module Protocol
|
5
|
+
class Binary
|
6
|
+
##
|
7
|
+
# Code to support SASL authentication
|
8
|
+
##
|
9
|
+
module SaslAuthentication
|
10
|
+
def perform_auth_negotiation
|
11
|
+
write(RequestFormatter.standard_request(opkey: :auth_negotiation))
|
12
|
+
|
13
|
+
status, content = @response_processor.auth_response
|
14
|
+
return [status, []] if content.nil?
|
15
|
+
|
16
|
+
# Substitute spaces for the \x00 returned by
|
17
|
+
# memcached as a separator for easier
|
18
|
+
content&.tr("\u0000", ' ')
|
19
|
+
mechanisms = content&.split
|
20
|
+
[status, mechanisms]
|
21
|
+
end
|
22
|
+
|
23
|
+
PLAIN_AUTH = 'PLAIN'
|
24
|
+
|
25
|
+
def supported_mechanisms!(mechanisms)
|
26
|
+
unless mechanisms.include?(PLAIN_AUTH)
|
27
|
+
raise NotImplementedError,
|
28
|
+
'Dalli only supports the PLAIN authentication mechanism'
|
29
|
+
end
|
30
|
+
[PLAIN_AUTH]
|
31
|
+
end
|
32
|
+
|
33
|
+
def authenticate_with_plain
|
34
|
+
write(RequestFormatter.standard_request(opkey: :auth_request,
|
35
|
+
key: PLAIN_AUTH,
|
36
|
+
value: "\x0#{username}\x0#{password}"))
|
37
|
+
@response_processor.auth_response
|
38
|
+
end
|
39
|
+
|
40
|
+
def authenticate_connection
|
41
|
+
Dalli.logger.info { "Dalli/SASL authenticating as #{username}" }
|
42
|
+
|
43
|
+
status, mechanisms = perform_auth_negotiation
|
44
|
+
return Dalli.logger.debug('Authentication not required/supported by server') if status == 0x81
|
45
|
+
|
46
|
+
supported_mechanisms!(mechanisms)
|
47
|
+
status, content = authenticate_with_plain
|
48
|
+
|
49
|
+
return Dalli.logger.info("Dalli/SASL: #{content}") if status.zero?
|
50
|
+
|
51
|
+
raise Dalli::DalliError, "Error authenticating: 0x#{status.to_s(16)}" unless status == 0x21
|
52
|
+
|
53
|
+
raise NotImplementedError, 'No two-step authentication mechanisms supported'
|
54
|
+
# (step, msg) = sasl.receive('challenge', content)
|
55
|
+
# raise Dalli::NetworkError, "Authentication failed" if sasl.failed? || step != 'response'
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|