jashmenn-dalli 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Gemfile +7 -0
- data/History.md +169 -0
- data/LICENSE +20 -0
- data/Performance.md +77 -0
- data/README.md +177 -0
- data/Rakefile +34 -0
- data/Upgrade.md +45 -0
- data/dalli.gemspec +33 -0
- data/lib/action_controller/session/dalli_store.rb +62 -0
- data/lib/action_dispatch/middleware/session/dalli_store.rb +67 -0
- data/lib/active_support/cache/dalli_store.rb +185 -0
- data/lib/active_support/cache/dalli_store23.rb +172 -0
- data/lib/dalli.rb +43 -0
- data/lib/dalli/client.rb +264 -0
- data/lib/dalli/compatibility.rb +52 -0
- data/lib/dalli/memcache-client.rb +1 -0
- data/lib/dalli/options.rb +44 -0
- data/lib/dalli/ring.rb +105 -0
- data/lib/dalli/server.rb +516 -0
- data/lib/dalli/socket.rb +37 -0
- data/lib/dalli/version.rb +3 -0
- data/test/abstract_unit.rb +284 -0
- data/test/benchmark_test.rb +170 -0
- data/test/helper.rb +54 -0
- data/test/memcached_mock.rb +106 -0
- data/test/test_active_support.rb +177 -0
- data/test/test_compatibility.rb +33 -0
- data/test/test_dalli.rb +398 -0
- data/test/test_encoding.rb +34 -0
- data/test/test_failover.rb +89 -0
- data/test/test_network.rb +54 -0
- data/test/test_ring.rb +89 -0
- data/test/test_sasl.rb +79 -0
- data/test/test_session_store.rb +230 -0
- metadata +145 -0
data/lib/dalli/server.rb
ADDED
@@ -0,0 +1,516 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'timeout'
|
3
|
+
require 'zlib'
|
4
|
+
|
5
|
+
module Dalli
|
6
|
+
class Server
|
7
|
+
attr_accessor :hostname
|
8
|
+
attr_accessor :port
|
9
|
+
attr_accessor :weight
|
10
|
+
attr_accessor :options
|
11
|
+
|
12
|
+
DEFAULTS = {
|
13
|
+
# seconds between trying to contact a remote server
|
14
|
+
:down_retry_delay => 1,
|
15
|
+
# connect/read/write timeout for socket operations
|
16
|
+
:socket_timeout => 0.5,
|
17
|
+
# times a socket operation may fail before considering the server dead
|
18
|
+
:socket_max_failures => 2,
|
19
|
+
# amount of time to sleep between retries when a failure occurs
|
20
|
+
:socket_failure_delay => 0.01,
|
21
|
+
# max size of value in bytes (default is 1 MB, can be overriden with "memcached -I <size>")
|
22
|
+
:value_max_bytes => 1024 * 1024,
|
23
|
+
:username => nil,
|
24
|
+
:password => nil,
|
25
|
+
}
|
26
|
+
|
27
|
+
def initialize(attribs, options = {})
|
28
|
+
(@hostname, @port, @weight) = attribs.split(':')
|
29
|
+
@port ||= 11211
|
30
|
+
@port = Integer(@port)
|
31
|
+
@weight ||= 1
|
32
|
+
@weight = Integer(@weight)
|
33
|
+
@fail_count = 0
|
34
|
+
@down_at = nil
|
35
|
+
@last_down_at = nil
|
36
|
+
@options = DEFAULTS.merge(options)
|
37
|
+
@sock = nil
|
38
|
+
@msg = nil
|
39
|
+
end
|
40
|
+
|
41
|
+
# Chokepoint method for instrumentation
|
42
|
+
def request(op, *args)
|
43
|
+
raise Dalli::NetworkError, "#{hostname}:#{port} is down: #{@error} #{@msg}" unless alive?
|
44
|
+
begin
|
45
|
+
send(op, *args)
|
46
|
+
rescue Dalli::NetworkError
|
47
|
+
raise
|
48
|
+
rescue Dalli::MarshalError => ex
|
49
|
+
Dalli.logger.error "Marshalling error for key '#{args.first}': #{ex.message}"
|
50
|
+
Dalli.logger.error "You are trying to cache a Ruby object which cannot be serialized to memcached."
|
51
|
+
Dalli.logger.error ex.backtrace.join("\n\t")
|
52
|
+
false
|
53
|
+
rescue Dalli::DalliError
|
54
|
+
raise
|
55
|
+
rescue Exception => ex
|
56
|
+
Dalli.logger.error "Unexpected exception in Dalli: #{ex.class.name}: #{ex.message}"
|
57
|
+
Dalli.logger.error "This is a bug in Dalli, please enter an issue in Github if it does not already exist."
|
58
|
+
Dalli.logger.error ex.backtrace.join("\n\t")
|
59
|
+
down!
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def alive?
|
64
|
+
return true if @sock
|
65
|
+
|
66
|
+
if @last_down_at && @last_down_at + options[:down_retry_delay] >= Time.now
|
67
|
+
time = @last_down_at + options[:down_retry_delay] - Time.now
|
68
|
+
Dalli.logger.debug { "down_retry_delay not reached for #{hostname}:#{port} (%.3f seconds left)" % time }
|
69
|
+
return false
|
70
|
+
end
|
71
|
+
|
72
|
+
connect
|
73
|
+
!!@sock
|
74
|
+
rescue Dalli::NetworkError
|
75
|
+
false
|
76
|
+
end
|
77
|
+
|
78
|
+
def close
|
79
|
+
return unless @sock
|
80
|
+
@sock.close rescue nil
|
81
|
+
@sock = nil
|
82
|
+
end
|
83
|
+
|
84
|
+
def lock!
|
85
|
+
end
|
86
|
+
|
87
|
+
def unlock!
|
88
|
+
end
|
89
|
+
|
90
|
+
# NOTE: Additional public methods should be overridden in Dalli::Threadsafe
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def failure!
|
95
|
+
Dalli.logger.info { "#{hostname}:#{port} failed (count: #{@fail_count})" }
|
96
|
+
|
97
|
+
@fail_count += 1
|
98
|
+
if @fail_count >= options[:socket_max_failures]
|
99
|
+
down!
|
100
|
+
else
|
101
|
+
sleep(options[:socket_failure_delay]) if options[:socket_failure_delay]
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def down!
|
106
|
+
close
|
107
|
+
|
108
|
+
@last_down_at = Time.now
|
109
|
+
|
110
|
+
if @down_at
|
111
|
+
time = Time.now - @down_at
|
112
|
+
Dalli.logger.debug { "#{hostname}:#{port} is still down (for %.3f seconds now)" % time }
|
113
|
+
else
|
114
|
+
@down_at = @last_down_at
|
115
|
+
Dalli.logger.warn { "#{hostname}:#{port} is down" }
|
116
|
+
end
|
117
|
+
|
118
|
+
@error = $! && $!.class.name
|
119
|
+
@msg = @msg || ($! && $!.message && !$!.message.empty? && $!.message)
|
120
|
+
raise Dalli::NetworkError, "#{hostname}:#{port} is down: #{@error} #{@msg}"
|
121
|
+
end
|
122
|
+
|
123
|
+
def up!
|
124
|
+
if @down_at
|
125
|
+
time = Time.now - @down_at
|
126
|
+
Dalli.logger.warn { "#{hostname}:#{port} is back (downtime was %.3f seconds)" % time }
|
127
|
+
end
|
128
|
+
|
129
|
+
@fail_count = 0
|
130
|
+
@down_at = nil
|
131
|
+
@last_down_at = nil
|
132
|
+
@msg = nil
|
133
|
+
@error = nil
|
134
|
+
end
|
135
|
+
|
136
|
+
def multi?
|
137
|
+
Thread.current[:dalli_multi]
|
138
|
+
end
|
139
|
+
|
140
|
+
def get(key)
|
141
|
+
req = [REQUEST, OPCODES[:get], key.bytesize, 0, 0, 0, key.bytesize, 0, 0, key].pack(FORMAT[:get])
|
142
|
+
write(req)
|
143
|
+
generic_response(true)
|
144
|
+
end
|
145
|
+
|
146
|
+
def getkq(key)
|
147
|
+
req = [REQUEST, OPCODES[:getkq], key.bytesize, 0, 0, 0, key.bytesize, 0, 0, key].pack(FORMAT[:getkq])
|
148
|
+
write(req)
|
149
|
+
end
|
150
|
+
|
151
|
+
def set(key, value, ttl, options)
|
152
|
+
(value, flags) = serialize(key, value, options)
|
153
|
+
|
154
|
+
req = [REQUEST, OPCODES[multi? ? :setq : :set], key.bytesize, 8, 0, 0, value.bytesize + key.bytesize + 8, 0, 0, flags, ttl, key, value].pack(FORMAT[:set])
|
155
|
+
write(req)
|
156
|
+
generic_response unless multi?
|
157
|
+
end
|
158
|
+
|
159
|
+
def add(key, value, ttl, cas, options)
|
160
|
+
(value, flags) = serialize(key, value, options)
|
161
|
+
|
162
|
+
req = [REQUEST, OPCODES[multi? ? :addq : :add], key.bytesize, 8, 0, 0, value.bytesize + key.bytesize + 8, 0, cas, flags, ttl, key, value].pack(FORMAT[:add])
|
163
|
+
write(req)
|
164
|
+
generic_response unless multi?
|
165
|
+
end
|
166
|
+
|
167
|
+
def replace(key, value, ttl, options)
|
168
|
+
(value, flags) = serialize(key, value, options)
|
169
|
+
req = [REQUEST, OPCODES[multi? ? :replaceq : :replace], key.bytesize, 8, 0, 0, value.bytesize + key.bytesize + 8, 0, 0, flags, ttl, key, value].pack(FORMAT[:replace])
|
170
|
+
write(req)
|
171
|
+
generic_response unless multi?
|
172
|
+
end
|
173
|
+
|
174
|
+
def delete(key)
|
175
|
+
req = [REQUEST, OPCODES[multi? ? :deleteq : :delete], key.bytesize, 0, 0, 0, key.bytesize, 0, 0, key].pack(FORMAT[:delete])
|
176
|
+
write(req)
|
177
|
+
generic_response unless multi?
|
178
|
+
end
|
179
|
+
|
180
|
+
def flush(ttl)
|
181
|
+
req = [REQUEST, OPCODES[:flush], 0, 4, 0, 0, 4, 0, 0, 0].pack(FORMAT[:flush])
|
182
|
+
write(req)
|
183
|
+
generic_response
|
184
|
+
end
|
185
|
+
|
186
|
+
def decr(key, count, ttl, default)
|
187
|
+
expiry = default ? ttl : 0xFFFFFFFF
|
188
|
+
default ||= 0
|
189
|
+
(h, l) = split(count)
|
190
|
+
(dh, dl) = split(default)
|
191
|
+
req = [REQUEST, OPCODES[:decr], key.bytesize, 20, 0, 0, key.bytesize + 20, 0, 0, h, l, dh, dl, expiry, key].pack(FORMAT[:decr])
|
192
|
+
write(req)
|
193
|
+
body = generic_response
|
194
|
+
body ? longlong(*body.unpack('NN')) : body
|
195
|
+
end
|
196
|
+
|
197
|
+
def incr(key, count, ttl, default)
|
198
|
+
expiry = default ? ttl : 0xFFFFFFFF
|
199
|
+
default ||= 0
|
200
|
+
(h, l) = split(count)
|
201
|
+
(dh, dl) = split(default)
|
202
|
+
req = [REQUEST, OPCODES[:incr], key.bytesize, 20, 0, 0, key.bytesize + 20, 0, 0, h, l, dh, dl, expiry, key].pack(FORMAT[:incr])
|
203
|
+
write(req)
|
204
|
+
body = generic_response
|
205
|
+
body ? longlong(*body.unpack('NN')) : body
|
206
|
+
end
|
207
|
+
|
208
|
+
# Noop is a keepalive operation but also used to demarcate the end of a set of pipelined commands.
|
209
|
+
# We need to read all the responses at once.
|
210
|
+
def noop
|
211
|
+
req = [REQUEST, OPCODES[:noop], 0, 0, 0, 0, 0, 0, 0].pack(FORMAT[:noop])
|
212
|
+
write(req)
|
213
|
+
multi_response
|
214
|
+
end
|
215
|
+
|
216
|
+
def append(key, value)
|
217
|
+
req = [REQUEST, OPCODES[:append], key.bytesize, 0, 0, 0, value.bytesize + key.bytesize, 0, 0, key, value].pack(FORMAT[:append])
|
218
|
+
write(req)
|
219
|
+
generic_response
|
220
|
+
end
|
221
|
+
|
222
|
+
def prepend(key, value)
|
223
|
+
req = [REQUEST, OPCODES[:prepend], key.bytesize, 0, 0, 0, value.bytesize + key.bytesize, 0, 0, key, value].pack(FORMAT[:prepend])
|
224
|
+
write(req)
|
225
|
+
generic_response
|
226
|
+
end
|
227
|
+
|
228
|
+
def stats(info='')
|
229
|
+
req = [REQUEST, OPCODES[:stat], info.bytesize, 0, 0, 0, info.bytesize, 0, 0, info].pack(FORMAT[:stat])
|
230
|
+
write(req)
|
231
|
+
keyvalue_response
|
232
|
+
end
|
233
|
+
|
234
|
+
def cas(key)
|
235
|
+
req = [REQUEST, OPCODES[:get], key.bytesize, 0, 0, 0, key.bytesize, 0, 0, key].pack(FORMAT[:get])
|
236
|
+
write(req)
|
237
|
+
cas_response
|
238
|
+
end
|
239
|
+
|
240
|
+
def version
|
241
|
+
req = [REQUEST, OPCODES[:version], 0, 0, 0, 0, 0, 0, 0].pack(FORMAT[:noop])
|
242
|
+
write(req)
|
243
|
+
generic_response
|
244
|
+
end
|
245
|
+
|
246
|
+
COMPRESSION_MIN_SIZE = 1024
|
247
|
+
|
248
|
+
# http://www.hjp.at/zettel/m/memcached_flags.rxml
|
249
|
+
# Looks like most clients use bit 0 to indicate native language serialization
|
250
|
+
# and bit 1 to indicate gzip compression.
|
251
|
+
FLAG_MARSHALLED = 0x1
|
252
|
+
FLAG_COMPRESSED = 0x2
|
253
|
+
|
254
|
+
def serialize(key, value, options=nil)
|
255
|
+
marshalled = false
|
256
|
+
value = unless options && options[:raw]
|
257
|
+
marshalled = true
|
258
|
+
begin
|
259
|
+
Marshal.dump(value)
|
260
|
+
rescue => ex
|
261
|
+
# Marshalling can throw several different types of generic Ruby exceptions.
|
262
|
+
# Convert to a specific exception so we can special case it higher up the stack.
|
263
|
+
exc = Dalli::MarshalError.new(ex.message)
|
264
|
+
exc.set_backtrace ex.backtrace
|
265
|
+
raise exc
|
266
|
+
end
|
267
|
+
else
|
268
|
+
value.to_s
|
269
|
+
end
|
270
|
+
compressed = false
|
271
|
+
if @options[:compression] && value.bytesize >= COMPRESSION_MIN_SIZE
|
272
|
+
value = Zlib::Deflate.deflate(value)
|
273
|
+
compressed = true
|
274
|
+
end
|
275
|
+
raise Dalli::DalliError, "Value too large, memcached can only store #{@options[:value_max_bytes]} bytes per key [key: #{key}, size: #{value.bytesize}]" if value.bytesize > @options[:value_max_bytes]
|
276
|
+
flags = 0
|
277
|
+
flags |= FLAG_COMPRESSED if compressed
|
278
|
+
flags |= FLAG_MARSHALLED if marshalled
|
279
|
+
[value, flags]
|
280
|
+
end
|
281
|
+
|
282
|
+
def deserialize(value, flags)
|
283
|
+
value = Zlib::Inflate.inflate(value) if (flags & FLAG_COMPRESSED) != 0
|
284
|
+
value = Marshal.load(value) if (flags & FLAG_MARSHALLED) != 0
|
285
|
+
value
|
286
|
+
rescue TypeError, ArgumentError
|
287
|
+
raise DalliError, "Unable to unmarshal value: #{$!.message}"
|
288
|
+
rescue Zlib::Error
|
289
|
+
raise DalliError, "Unable to uncompress value: #{$!.message}"
|
290
|
+
end
|
291
|
+
|
292
|
+
def cas_response
|
293
|
+
header = read(24)
|
294
|
+
raise Dalli::NetworkError, 'No response' if !header
|
295
|
+
(extras, type, status, count, _, cas) = header.unpack(CAS_HEADER)
|
296
|
+
data = read(count) if count > 0
|
297
|
+
if status == 1
|
298
|
+
nil
|
299
|
+
elsif status != 0
|
300
|
+
raise Dalli::DalliError, "Response error #{status}: #{RESPONSE_CODES[status]}"
|
301
|
+
elsif data
|
302
|
+
flags = data[0...extras].unpack('N')[0]
|
303
|
+
value = data[extras..-1]
|
304
|
+
data = deserialize(value, flags)
|
305
|
+
end
|
306
|
+
[data, cas]
|
307
|
+
end
|
308
|
+
|
309
|
+
CAS_HEADER = '@4CCnNNQ'
|
310
|
+
NORMAL_HEADER = '@4CCnN'
|
311
|
+
KV_HEADER = '@2n@6nN'
|
312
|
+
|
313
|
+
def generic_response(unpack=false)
|
314
|
+
header = read(24)
|
315
|
+
raise Dalli::NetworkError, 'No response' if !header
|
316
|
+
(extras, type, status, count) = header.unpack(NORMAL_HEADER)
|
317
|
+
data = read(count) if count > 0
|
318
|
+
if status == 1
|
319
|
+
nil
|
320
|
+
elsif status == 2 || status == 5
|
321
|
+
false # Not stored, normal status for add operation
|
322
|
+
elsif status != 0
|
323
|
+
raise Dalli::DalliError, "Response error #{status}: #{RESPONSE_CODES[status]}"
|
324
|
+
elsif data
|
325
|
+
flags = data[0...extras].unpack('N')[0]
|
326
|
+
value = data[extras..-1]
|
327
|
+
unpack ? deserialize(value, flags) : value
|
328
|
+
else
|
329
|
+
true
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
def keyvalue_response
|
334
|
+
hash = {}
|
335
|
+
loop do
|
336
|
+
header = read(24)
|
337
|
+
raise Dalli::NetworkError, 'No response' if !header
|
338
|
+
(key_length, status, body_length) = header.unpack(KV_HEADER)
|
339
|
+
return hash if key_length == 0
|
340
|
+
key = read(key_length)
|
341
|
+
value = read(body_length - key_length) if body_length - key_length > 0
|
342
|
+
hash[key] = value
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
def multi_response
|
347
|
+
hash = {}
|
348
|
+
loop do
|
349
|
+
header = read(24)
|
350
|
+
raise Dalli::NetworkError, 'No response' if !header
|
351
|
+
(key_length, status, body_length) = header.unpack(KV_HEADER)
|
352
|
+
return hash if key_length == 0
|
353
|
+
flags = read(4).unpack('N')[0]
|
354
|
+
key = read(key_length)
|
355
|
+
value = read(body_length - key_length - 4) if body_length - key_length - 4 > 0
|
356
|
+
hash[key] = deserialize(value, flags)
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
def write(bytes)
|
361
|
+
begin
|
362
|
+
@sock.write(bytes)
|
363
|
+
rescue SystemCallError
|
364
|
+
failure!
|
365
|
+
retry
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
def read(count)
|
370
|
+
begin
|
371
|
+
@sock.readfull(count)
|
372
|
+
rescue SystemCallError, Timeout::Error, EOFError
|
373
|
+
failure!
|
374
|
+
retry
|
375
|
+
end
|
376
|
+
end
|
377
|
+
|
378
|
+
def connect
|
379
|
+
Dalli.logger.debug { "Dalli::Server#connect #{hostname}:#{port}" }
|
380
|
+
|
381
|
+
begin
|
382
|
+
@sock = KSocket.open(hostname, port, :timeout => options[:socket_timeout])
|
383
|
+
@version = version # trigger actual connect
|
384
|
+
sasl_authentication if need_auth?
|
385
|
+
up!
|
386
|
+
rescue Dalli::DalliError # SASL auth failure
|
387
|
+
raise
|
388
|
+
rescue SystemCallError, Timeout::Error, EOFError
|
389
|
+
failure!
|
390
|
+
retry
|
391
|
+
end
|
392
|
+
end
|
393
|
+
|
394
|
+
def split(n)
|
395
|
+
[n >> 32, 0xFFFFFFFF & n]
|
396
|
+
end
|
397
|
+
|
398
|
+
def longlong(a, b)
|
399
|
+
(a << 32) | b
|
400
|
+
end
|
401
|
+
|
402
|
+
REQUEST = 0x80
|
403
|
+
RESPONSE = 0x81
|
404
|
+
|
405
|
+
RESPONSE_CODES = {
|
406
|
+
0 => 'No error',
|
407
|
+
1 => 'Key not found',
|
408
|
+
2 => 'Key exists',
|
409
|
+
3 => 'Value too large',
|
410
|
+
4 => 'Invalid arguments',
|
411
|
+
5 => 'Item not stored',
|
412
|
+
6 => 'Incr/decr on a non-numeric value',
|
413
|
+
0x20 => 'Authentication required',
|
414
|
+
0x81 => 'Unknown command',
|
415
|
+
0x82 => 'Out of memory',
|
416
|
+
}
|
417
|
+
|
418
|
+
OPCODES = {
|
419
|
+
:get => 0x00,
|
420
|
+
:set => 0x01,
|
421
|
+
:add => 0x02,
|
422
|
+
:replace => 0x03,
|
423
|
+
:delete => 0x04,
|
424
|
+
:incr => 0x05,
|
425
|
+
:decr => 0x06,
|
426
|
+
:flush => 0x08,
|
427
|
+
:noop => 0x0A,
|
428
|
+
:version => 0x0B,
|
429
|
+
:getkq => 0x0D,
|
430
|
+
:append => 0x0E,
|
431
|
+
:prepend => 0x0F,
|
432
|
+
:stat => 0x10,
|
433
|
+
:setq => 0x11,
|
434
|
+
:addq => 0x12,
|
435
|
+
:replaceq => 0x13,
|
436
|
+
:deleteq => 0x14,
|
437
|
+
:incrq => 0x15,
|
438
|
+
:decrq => 0x16,
|
439
|
+
:auth_negotiation => 0x20,
|
440
|
+
:auth_request => 0x21,
|
441
|
+
:auth_continue => 0x22,
|
442
|
+
}
|
443
|
+
|
444
|
+
HEADER = "CCnCCnNNQ"
|
445
|
+
OP_FORMAT = {
|
446
|
+
:get => 'a*',
|
447
|
+
:set => 'NNa*a*',
|
448
|
+
:add => 'NNa*a*',
|
449
|
+
:replace => 'NNa*a*',
|
450
|
+
:delete => 'a*',
|
451
|
+
:incr => 'NNNNNa*',
|
452
|
+
:decr => 'NNNNNa*',
|
453
|
+
:flush => 'N',
|
454
|
+
:noop => '',
|
455
|
+
:getkq => 'a*',
|
456
|
+
:version => '',
|
457
|
+
:stat => 'a*',
|
458
|
+
:append => 'a*a*',
|
459
|
+
:prepend => 'a*a*',
|
460
|
+
:auth_request => 'a*a*',
|
461
|
+
:auth_continue => 'a*a*',
|
462
|
+
}
|
463
|
+
FORMAT = OP_FORMAT.inject({}) { |memo, (k, v)| memo[k] = HEADER + v; memo }
|
464
|
+
|
465
|
+
|
466
|
+
#######
|
467
|
+
# SASL authentication support for NorthScale
|
468
|
+
#######
|
469
|
+
|
470
|
+
def need_auth?
|
471
|
+
@options[:username] || ENV['MEMCACHE_USERNAME']
|
472
|
+
end
|
473
|
+
|
474
|
+
def username
|
475
|
+
@options[:username] || ENV['MEMCACHE_USERNAME']
|
476
|
+
end
|
477
|
+
|
478
|
+
def password
|
479
|
+
@options[:password] || ENV['MEMCACHE_PASSWORD']
|
480
|
+
end
|
481
|
+
|
482
|
+
def sasl_authentication
|
483
|
+
Dalli.logger.info { "Dalli/SASL authenticating as #{username}" }
|
484
|
+
|
485
|
+
# negotiate
|
486
|
+
req = [REQUEST, OPCODES[:auth_negotiation], 0, 0, 0, 0, 0, 0, 0].pack(FORMAT[:noop])
|
487
|
+
write(req)
|
488
|
+
header = read(24)
|
489
|
+
raise Dalli::NetworkError, 'No response' if !header
|
490
|
+
(extras, type, status, count) = header.unpack(NORMAL_HEADER)
|
491
|
+
raise Dalli::NetworkError, "Unexpected message format: #{extras} #{count}" unless extras == 0 && count > 0
|
492
|
+
content = read(count)
|
493
|
+
return (Dalli.logger.debug("Authentication not required/supported by server")) if status == 0x81
|
494
|
+
mechanisms = content.split(' ')
|
495
|
+
raise NotImplementedError, "Dalli only supports the PLAIN authentication mechanism" if !mechanisms.include?('PLAIN')
|
496
|
+
|
497
|
+
# request
|
498
|
+
mechanism = 'PLAIN'
|
499
|
+
msg = "\x0#{username}\x0#{password}"
|
500
|
+
req = [REQUEST, OPCODES[:auth_request], mechanism.bytesize, 0, 0, 0, mechanism.bytesize + msg.bytesize, 0, 0, mechanism, msg].pack(FORMAT[:auth_request])
|
501
|
+
write(req)
|
502
|
+
|
503
|
+
header = read(24)
|
504
|
+
raise Dalli::NetworkError, 'No response' if !header
|
505
|
+
(extras, type, status, count) = header.unpack(NORMAL_HEADER)
|
506
|
+
raise Dalli::NetworkError, "Unexpected message format: #{extras} #{count}" unless extras == 0 && count > 0
|
507
|
+
content = read(count)
|
508
|
+
return Dalli.logger.info("Dalli/SASL: #{content}") if status == 0
|
509
|
+
|
510
|
+
raise Dalli::DalliError, "Error authenticating: #{status}" unless status == 0x21
|
511
|
+
raise NotImplementedError, "No two-step authentication mechanisms supported"
|
512
|
+
# (step, msg) = sasl.receive('challenge', content)
|
513
|
+
# raise Dalli::NetworkError, "Authentication failed" if sasl.failed? || step != 'response'
|
514
|
+
end
|
515
|
+
end
|
516
|
+
end
|