dalli 2.7.0 → 3.0.4

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.

@@ -0,0 +1,703 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+ require "timeout"
5
+
6
+ module Dalli
7
+ module Protocol
8
+ class Binary
9
+ attr_accessor :hostname
10
+ attr_accessor :port
11
+ attr_accessor :weight
12
+ attr_accessor :options
13
+ attr_reader :sock
14
+ attr_reader :socket_type # possible values: :unix, :tcp
15
+
16
+ DEFAULTS = {
17
+ # seconds between trying to contact a remote server
18
+ down_retry_delay: 30,
19
+ # connect/read/write timeout for socket operations
20
+ socket_timeout: 1,
21
+ # times a socket operation may fail before considering the server dead
22
+ socket_max_failures: 2,
23
+ # amount of time to sleep between retries when a failure occurs
24
+ socket_failure_delay: 0.1,
25
+ # max size of value in bytes (default is 1 MB, can be overriden with "memcached -I <size>")
26
+ value_max_bytes: 1024 * 1024,
27
+ serializer: Marshal,
28
+ username: nil,
29
+ password: nil,
30
+ keepalive: true,
31
+ # max byte size for SO_SNDBUF
32
+ sndbuf: nil,
33
+ # max byte size for SO_RCVBUF
34
+ rcvbuf: nil
35
+ }
36
+
37
+ def initialize(attribs, options = {})
38
+ @hostname, @port, @weight, @socket_type = ServerConfigParser.parse(attribs)
39
+ @fail_count = 0
40
+ @down_at = nil
41
+ @last_down_at = nil
42
+ @options = DEFAULTS.merge(options)
43
+ @value_compressor = ValueCompressor.new(@options)
44
+ @sock = nil
45
+ @msg = nil
46
+ @error = nil
47
+ @pid = nil
48
+ @inprogress = nil
49
+ end
50
+
51
+ def name
52
+ if socket_type == :unix
53
+ hostname
54
+ else
55
+ "#{hostname}:#{port}"
56
+ end
57
+ end
58
+
59
+ # Chokepoint method for instrumentation
60
+ def request(op, *args)
61
+ verify_state
62
+ raise Dalli::NetworkError, "#{name} is down: #{@error} #{@msg}. If you are sure it is running, ensure memcached version is > 1.4." unless alive?
63
+ begin
64
+ send(op, *args)
65
+ rescue Dalli::MarshalError => ex
66
+ Dalli.logger.error "Marshalling error for key '#{args.first}': #{ex.message}"
67
+ Dalli.logger.error "You are trying to cache a Ruby object which cannot be serialized to memcached."
68
+ raise
69
+ rescue Dalli::DalliError, Dalli::NetworkError, Dalli::ValueOverMaxSize, Timeout::Error
70
+ raise
71
+ rescue => ex
72
+ Dalli.logger.error "Unexpected exception during Dalli request: #{ex.class.name}: #{ex.message}"
73
+ Dalli.logger.error ex.backtrace.join("\n\t")
74
+ down!
75
+ end
76
+ end
77
+
78
+ def alive?
79
+ return true if @sock
80
+
81
+ if @last_down_at && @last_down_at + options[:down_retry_delay] >= Time.now
82
+ time = @last_down_at + options[:down_retry_delay] - Time.now
83
+ Dalli.logger.debug { "down_retry_delay not reached for #{name} (%.3f seconds left)" % time }
84
+ return false
85
+ end
86
+
87
+ connect
88
+ !!@sock
89
+ rescue Dalli::NetworkError
90
+ false
91
+ end
92
+
93
+ def close
94
+ return unless @sock
95
+ begin
96
+ @sock.close
97
+ rescue
98
+ nil
99
+ end
100
+ @sock = nil
101
+ @pid = nil
102
+ @inprogress = false
103
+ end
104
+
105
+ def lock!
106
+ end
107
+
108
+ def unlock!
109
+ end
110
+
111
+ def serializer
112
+ @options[:serializer]
113
+ end
114
+
115
+ # Start reading key/value pairs from this connection. This is usually called
116
+ # after a series of GETKQ commands. A NOOP is sent, and the server begins
117
+ # flushing responses for kv pairs that were found.
118
+ #
119
+ # Returns nothing.
120
+ def multi_response_start
121
+ verify_state
122
+ write_noop
123
+ @multi_buffer = +""
124
+ @position = 0
125
+ @inprogress = true
126
+ end
127
+
128
+ # Did the last call to #multi_response_start complete successfully?
129
+ def multi_response_completed?
130
+ @multi_buffer.nil?
131
+ end
132
+
133
+ # Attempt to receive and parse as many key/value pairs as possible
134
+ # from this server. After #multi_response_start, this should be invoked
135
+ # repeatedly whenever this server's socket is readable until
136
+ # #multi_response_completed?.
137
+ #
138
+ # Returns a Hash of kv pairs received.
139
+ def multi_response_nonblock
140
+ reconnect! "multi_response has completed" if @multi_buffer.nil?
141
+
142
+ @multi_buffer << @sock.read_available
143
+ buf = @multi_buffer
144
+ pos = @position
145
+ values = {}
146
+
147
+ while buf.bytesize - pos >= 24
148
+ header = buf.slice(pos, 24)
149
+ (key_length, _, body_length, cas) = header.unpack(KV_HEADER)
150
+
151
+ if key_length == 0
152
+ # all done!
153
+ @multi_buffer = nil
154
+ @position = nil
155
+ @inprogress = false
156
+ break
157
+
158
+ elsif buf.bytesize - pos >= 24 + body_length
159
+ flags = buf.slice(pos + 24, 4).unpack1("N")
160
+ key = buf.slice(pos + 24 + 4, key_length)
161
+ value = buf.slice(pos + 24 + 4 + key_length, body_length - key_length - 4) if body_length - key_length - 4 > 0
162
+
163
+ pos = pos + 24 + body_length
164
+
165
+ begin
166
+ values[key] = [deserialize(value, flags), cas]
167
+ rescue DalliError
168
+ end
169
+
170
+ else
171
+ # not enough data yet, wait for more
172
+ break
173
+ end
174
+ end
175
+ @position = pos
176
+
177
+ values
178
+ rescue SystemCallError, Timeout::Error, EOFError => e
179
+ failure!(e)
180
+ end
181
+
182
+ # Abort an earlier #multi_response_start. Used to signal an external
183
+ # timeout. The underlying socket is disconnected, and the exception is
184
+ # swallowed.
185
+ #
186
+ # Returns nothing.
187
+ def multi_response_abort
188
+ @multi_buffer = nil
189
+ @position = nil
190
+ @inprogress = false
191
+ failure!(RuntimeError.new("External timeout"))
192
+ rescue NetworkError
193
+ true
194
+ end
195
+
196
+ # NOTE: Additional public methods should be overridden in Dalli::Threadsafe
197
+
198
+ private
199
+
200
+ def verify_state
201
+ failure!(RuntimeError.new("Already writing to socket")) if @inprogress
202
+ if @pid && @pid != Process.pid
203
+ message = "Fork detected, re-connecting child process..."
204
+ Dalli.logger.info { message }
205
+ reconnect! message
206
+ end
207
+ end
208
+
209
+ def reconnect!(message)
210
+ close
211
+ sleep(options[:socket_failure_delay]) if options[:socket_failure_delay]
212
+ raise Dalli::NetworkError, message
213
+ end
214
+
215
+ def failure!(exception)
216
+ message = "#{name} failed (count: #{@fail_count}) #{exception.class}: #{exception.message}"
217
+ Dalli.logger.warn { message }
218
+
219
+ @fail_count += 1
220
+ if @fail_count >= options[:socket_max_failures]
221
+ down!
222
+ else
223
+ reconnect! "Socket operation failed, retrying..."
224
+ end
225
+ end
226
+
227
+ def down!
228
+ close
229
+
230
+ @last_down_at = Time.now
231
+
232
+ if @down_at
233
+ time = Time.now - @down_at
234
+ Dalli.logger.debug { "#{name} is still down (for %.3f seconds now)" % time }
235
+ else
236
+ @down_at = @last_down_at
237
+ Dalli.logger.warn { "#{name} is down" }
238
+ end
239
+
240
+ @error = $!&.class&.name
241
+ @msg ||= $!&.message
242
+ raise Dalli::NetworkError, "#{name} is down: #{@error} #{@msg}"
243
+ end
244
+
245
+ def up!
246
+ if @down_at
247
+ time = Time.now - @down_at
248
+ Dalli.logger.warn { "#{name} is back (downtime was %.3f seconds)" % time }
249
+ end
250
+
251
+ @fail_count = 0
252
+ @down_at = nil
253
+ @last_down_at = nil
254
+ @msg = nil
255
+ @error = nil
256
+ end
257
+
258
+ def multi?
259
+ Thread.current[:dalli_multi]
260
+ end
261
+
262
+ def get(key, options = nil)
263
+ req = [REQUEST, OPCODES[:get], key.bytesize, 0, 0, 0, key.bytesize, 0, 0, key].pack(FORMAT[:get])
264
+ write(req)
265
+ generic_response(true, !!(options && options.is_a?(Hash) && options[:cache_nils]))
266
+ end
267
+
268
+ def send_multiget(keys)
269
+ req = +""
270
+ keys.each do |key|
271
+ req << [REQUEST, OPCODES[:getkq], key.bytesize, 0, 0, 0, key.bytesize, 0, 0, key].pack(FORMAT[:getkq])
272
+ end
273
+ # Could send noop here instead of in multi_response_start
274
+ write(req)
275
+ end
276
+
277
+ def set(key, value, ttl, cas, options)
278
+ (value, flags) = serialize(key, value, options)
279
+ ttl = TtlSanitizer.sanitize(ttl)
280
+
281
+ guard_max_value(key, value)
282
+
283
+ req = [REQUEST, OPCODES[multi? ? :setq : :set], key.bytesize, 8, 0, 0, value.bytesize + key.bytesize + 8, 0, cas, flags, ttl, key, value].pack(FORMAT[:set])
284
+ write(req)
285
+ cas_response unless multi?
286
+ end
287
+
288
+ def add(key, value, ttl, options)
289
+ (value, flags) = serialize(key, value, options)
290
+ ttl = TtlSanitizer.sanitize(ttl)
291
+
292
+ guard_max_value(key, value)
293
+
294
+ req = [REQUEST, OPCODES[multi? ? :addq : :add], key.bytesize, 8, 0, 0, value.bytesize + key.bytesize + 8, 0, 0, flags, ttl, key, value].pack(FORMAT[:add])
295
+ write(req)
296
+ cas_response unless multi?
297
+ end
298
+
299
+ def replace(key, value, ttl, cas, options)
300
+ (value, flags) = serialize(key, value, options)
301
+ ttl = TtlSanitizer.sanitize(ttl)
302
+
303
+ guard_max_value(key, value)
304
+
305
+ req = [REQUEST, OPCODES[multi? ? :replaceq : :replace], key.bytesize, 8, 0, 0, value.bytesize + key.bytesize + 8, 0, cas, flags, ttl, key, value].pack(FORMAT[:replace])
306
+ write(req)
307
+ cas_response unless multi?
308
+ end
309
+
310
+ def delete(key, cas)
311
+ req = [REQUEST, OPCODES[multi? ? :deleteq : :delete], key.bytesize, 0, 0, 0, key.bytesize, 0, cas, key].pack(FORMAT[:delete])
312
+ write(req)
313
+ generic_response unless multi?
314
+ end
315
+
316
+ def flush(ttl)
317
+ req = [REQUEST, OPCODES[:flush], 0, 4, 0, 0, 4, 0, 0, 0].pack(FORMAT[:flush])
318
+ write(req)
319
+ generic_response
320
+ end
321
+
322
+ def decr_incr(opcode, key, count, ttl, default)
323
+ expiry = default ? TtlSanitizer.sanitize(ttl) : 0xFFFFFFFF
324
+ default ||= 0
325
+ (h, l) = split(count)
326
+ (dh, dl) = split(default)
327
+ req = [REQUEST, OPCODES[opcode], key.bytesize, 20, 0, 0, key.bytesize + 20, 0, 0, h, l, dh, dl, expiry, key].pack(FORMAT[opcode])
328
+ write(req)
329
+ body = generic_response
330
+ body ? body.unpack1("Q>") : body
331
+ end
332
+
333
+ def decr(key, count, ttl, default)
334
+ decr_incr :decr, key, count, ttl, default
335
+ end
336
+
337
+ def incr(key, count, ttl, default)
338
+ decr_incr :incr, key, count, ttl, default
339
+ end
340
+
341
+ def write_append_prepend(opcode, key, value)
342
+ write_generic [REQUEST, OPCODES[opcode], key.bytesize, 0, 0, 0, value.bytesize + key.bytesize, 0, 0, key, value].pack(FORMAT[opcode])
343
+ end
344
+
345
+ def write_generic(bytes)
346
+ write(bytes)
347
+ generic_response
348
+ end
349
+
350
+ def write_noop
351
+ req = [REQUEST, OPCODES[:noop], 0, 0, 0, 0, 0, 0, 0].pack(FORMAT[:noop])
352
+ write(req)
353
+ end
354
+
355
+ # Noop is a keepalive operation but also used to demarcate the end of a set of pipelined commands.
356
+ # We need to read all the responses at once.
357
+ def noop
358
+ write_noop
359
+ multi_response
360
+ end
361
+
362
+ def append(key, value)
363
+ write_append_prepend :append, key, value
364
+ end
365
+
366
+ def prepend(key, value)
367
+ write_append_prepend :prepend, key, value
368
+ end
369
+
370
+ def stats(info = "")
371
+ req = [REQUEST, OPCODES[:stat], info.bytesize, 0, 0, 0, info.bytesize, 0, 0, info].pack(FORMAT[:stat])
372
+ write(req)
373
+ keyvalue_response
374
+ end
375
+
376
+ def reset_stats
377
+ write_generic [REQUEST, OPCODES[:stat], "reset".bytesize, 0, 0, 0, "reset".bytesize, 0, 0, "reset"].pack(FORMAT[:stat])
378
+ end
379
+
380
+ def cas(key)
381
+ req = [REQUEST, OPCODES[:get], key.bytesize, 0, 0, 0, key.bytesize, 0, 0, key].pack(FORMAT[:get])
382
+ write(req)
383
+ data_cas_response
384
+ end
385
+
386
+ def version
387
+ write_generic [REQUEST, OPCODES[:version], 0, 0, 0, 0, 0, 0, 0].pack(FORMAT[:noop])
388
+ end
389
+
390
+ def touch(key, ttl)
391
+ ttl = TtlSanitizer.sanitize(ttl)
392
+ write_generic [REQUEST, OPCODES[:touch], key.bytesize, 4, 0, 0, key.bytesize + 4, 0, 0, ttl, key].pack(FORMAT[:touch])
393
+ end
394
+
395
+ def gat(key, ttl, options = nil)
396
+ ttl = TtlSanitizer.sanitize(ttl)
397
+ req = [REQUEST, OPCODES[:gat], key.bytesize, 4, 0, 0, key.bytesize + 4, 0, 0, ttl, key].pack(FORMAT[:gat])
398
+ write(req)
399
+ generic_response(true, !!(options && options.is_a?(Hash) && options[:cache_nils]))
400
+ end
401
+
402
+ # https://www.hjp.at/zettel/m/memcached_flags.rxml
403
+ # Looks like most clients use bit 0 to indicate native language serialization
404
+ FLAG_SERIALIZED = 0x1
405
+
406
+ def serialize(key, value, options = nil)
407
+ marshalled = false
408
+ value = if options && options[:raw]
409
+ value.to_s
410
+ else
411
+ marshalled = true
412
+ begin
413
+ serializer.dump(value)
414
+ rescue Timeout::Error => e
415
+ raise e
416
+ rescue => ex
417
+ # Marshalling can throw several different types of generic Ruby exceptions.
418
+ # Convert to a specific exception so we can special case it higher up the stack.
419
+ exc = Dalli::MarshalError.new(ex.message)
420
+ exc.set_backtrace ex.backtrace
421
+ raise exc
422
+ end
423
+ end
424
+
425
+ bitflags = 0
426
+ value, bitflags = @value_compressor.store(value, options, bitflags)
427
+
428
+ bitflags |= FLAG_SERIALIZED if marshalled
429
+ [value, bitflags]
430
+ end
431
+
432
+ def deserialize(value, flags)
433
+ value = @value_compressor.retrieve(value, flags)
434
+ value = serializer.load(value) if (flags & FLAG_SERIALIZED) != 0
435
+ value
436
+ rescue TypeError
437
+ raise unless /needs to have method `_load'|exception class\/object expected|instance of IO needed|incompatible marshal file format/.match?($!.message)
438
+ raise UnmarshalError, "Unable to unmarshal value: #{$!.message}"
439
+ rescue ArgumentError
440
+ raise unless /undefined class|marshal data too short/.match?($!.message)
441
+ raise UnmarshalError, "Unable to unmarshal value: #{$!.message}"
442
+ rescue NameError
443
+ raise unless /uninitialized constant/.match?($!.message)
444
+ raise UnmarshalError, "Unable to unmarshal value: #{$!.message}"
445
+ end
446
+
447
+ def data_cas_response
448
+ (extras, _, status, count, _, cas) = read_header.unpack(CAS_HEADER)
449
+ data = read(count) if count > 0
450
+ if status == 1
451
+ nil
452
+ elsif status != 0
453
+ raise Dalli::DalliError, "Response error #{status}: #{RESPONSE_CODES[status]}"
454
+ elsif data
455
+ flags = data[0...extras].unpack1("N")
456
+ value = data[extras..-1]
457
+ data = deserialize(value, flags)
458
+ end
459
+ [data, cas]
460
+ end
461
+
462
+ CAS_HEADER = "@4CCnNNQ"
463
+ NORMAL_HEADER = "@4CCnN"
464
+ KV_HEADER = "@2n@6nN@16Q"
465
+
466
+ def guard_max_value(key, value)
467
+ return if value.bytesize <= @options[:value_max_bytes]
468
+
469
+ message = "Value for #{key} over max size: #{@options[:value_max_bytes]} <= #{value.bytesize}"
470
+ raise Dalli::ValueOverMaxSize, message
471
+ end
472
+
473
+ # Implements the NullObject pattern to store an application-defined value for 'Key not found' responses.
474
+ class NilObject; end
475
+ NOT_FOUND = NilObject.new
476
+
477
+ def generic_response(unpack = false, cache_nils = false)
478
+ (extras, _, status, count) = read_header.unpack(NORMAL_HEADER)
479
+ data = read(count) if count > 0
480
+ if status == 1
481
+ cache_nils ? NOT_FOUND : nil
482
+ elsif status == 2 || status == 5
483
+ false # Not stored, normal status for add operation
484
+ elsif status != 0
485
+ raise Dalli::DalliError, "Response error #{status}: #{RESPONSE_CODES[status]}"
486
+ elsif data
487
+ flags = data.byteslice(0, extras).unpack1("N")
488
+ value = data.byteslice(extras, data.bytesize - extras)
489
+ unpack ? deserialize(value, flags) : value
490
+ else
491
+ true
492
+ end
493
+ end
494
+
495
+ def cas_response
496
+ (_, _, status, count, _, cas) = read_header.unpack(CAS_HEADER)
497
+ read(count) if count > 0 # this is potential data that we don't care about
498
+ if status == 1
499
+ nil
500
+ elsif status == 2 || status == 5
501
+ false # Not stored, normal status for add operation
502
+ elsif status != 0
503
+ raise Dalli::DalliError, "Response error #{status}: #{RESPONSE_CODES[status]}"
504
+ else
505
+ cas
506
+ end
507
+ end
508
+
509
+ def keyvalue_response
510
+ hash = {}
511
+ loop do
512
+ (key_length, _, body_length, _) = read_header.unpack(KV_HEADER)
513
+ return hash if key_length == 0
514
+ key = read(key_length)
515
+ value = read(body_length - key_length) if body_length - key_length > 0
516
+ hash[key] = value
517
+ end
518
+ end
519
+
520
+ def multi_response
521
+ hash = {}
522
+ loop do
523
+ (key_length, _, body_length, _) = read_header.unpack(KV_HEADER)
524
+ return hash if key_length == 0
525
+ flags = read(4).unpack1("N")
526
+ key = read(key_length)
527
+ value = read(body_length - key_length - 4) if body_length - key_length - 4 > 0
528
+ hash[key] = deserialize(value, flags)
529
+ end
530
+ end
531
+
532
+ def write(bytes)
533
+ @inprogress = true
534
+ result = @sock.write(bytes)
535
+ @inprogress = false
536
+ result
537
+ rescue SystemCallError, Timeout::Error => e
538
+ failure!(e)
539
+ end
540
+
541
+ def read(count)
542
+ @inprogress = true
543
+ data = @sock.readfull(count)
544
+ @inprogress = false
545
+ data
546
+ rescue SystemCallError, Timeout::Error, EOFError => e
547
+ failure!(e)
548
+ end
549
+
550
+ def read_header
551
+ read(24) || raise(Dalli::NetworkError, "No response")
552
+ end
553
+
554
+ def connect
555
+ Dalli.logger.debug { "Dalli::Server#connect #{name}" }
556
+
557
+ begin
558
+ @pid = Process.pid
559
+ @sock = if socket_type == :unix
560
+ Dalli::Socket::UNIX.open(hostname, self, options)
561
+ else
562
+ Dalli::Socket::TCP.open(hostname, port, self, options)
563
+ end
564
+ sasl_authentication if need_auth?
565
+ @version = version # trigger actual connect
566
+ up!
567
+ rescue Dalli::DalliError # SASL auth failure
568
+ raise
569
+ rescue SystemCallError, Timeout::Error, EOFError, SocketError => e
570
+ # SocketError = DNS resolution failure
571
+ failure!(e)
572
+ end
573
+ end
574
+
575
+ def split(n)
576
+ [n >> 32, 0xFFFFFFFF & n]
577
+ end
578
+
579
+ REQUEST = 0x80
580
+ RESPONSE = 0x81
581
+
582
+ # Response codes taken from:
583
+ # https://github.com/memcached/memcached/wiki/BinaryProtocolRevamped#response-status
584
+ RESPONSE_CODES = {
585
+ 0 => "No error",
586
+ 1 => "Key not found",
587
+ 2 => "Key exists",
588
+ 3 => "Value too large",
589
+ 4 => "Invalid arguments",
590
+ 5 => "Item not stored",
591
+ 6 => "Incr/decr on a non-numeric value",
592
+ 7 => "The vbucket belongs to another server",
593
+ 8 => "Authentication error",
594
+ 9 => "Authentication continue",
595
+ 0x20 => "Authentication required",
596
+ 0x81 => "Unknown command",
597
+ 0x82 => "Out of memory",
598
+ 0x83 => "Not supported",
599
+ 0x84 => "Internal error",
600
+ 0x85 => "Busy",
601
+ 0x86 => "Temporary failure"
602
+ }
603
+
604
+ OPCODES = {
605
+ get: 0x00,
606
+ set: 0x01,
607
+ add: 0x02,
608
+ replace: 0x03,
609
+ delete: 0x04,
610
+ incr: 0x05,
611
+ decr: 0x06,
612
+ flush: 0x08,
613
+ noop: 0x0A,
614
+ version: 0x0B,
615
+ getkq: 0x0D,
616
+ append: 0x0E,
617
+ prepend: 0x0F,
618
+ stat: 0x10,
619
+ setq: 0x11,
620
+ addq: 0x12,
621
+ replaceq: 0x13,
622
+ deleteq: 0x14,
623
+ incrq: 0x15,
624
+ decrq: 0x16,
625
+ auth_negotiation: 0x20,
626
+ auth_request: 0x21,
627
+ auth_continue: 0x22,
628
+ touch: 0x1C,
629
+ gat: 0x1D
630
+ }
631
+
632
+ HEADER = "CCnCCnNNQ"
633
+ OP_FORMAT = {
634
+ get: "a*",
635
+ set: "NNa*a*",
636
+ add: "NNa*a*",
637
+ replace: "NNa*a*",
638
+ delete: "a*",
639
+ incr: "NNNNNa*",
640
+ decr: "NNNNNa*",
641
+ flush: "N",
642
+ noop: "",
643
+ getkq: "a*",
644
+ version: "",
645
+ stat: "a*",
646
+ append: "a*a*",
647
+ prepend: "a*a*",
648
+ auth_request: "a*a*",
649
+ auth_continue: "a*a*",
650
+ touch: "Na*",
651
+ gat: "Na*"
652
+ }
653
+ FORMAT = OP_FORMAT.each_with_object({}) { |(k, v), memo| memo[k] = HEADER + v; }
654
+
655
+ #######
656
+ # SASL authentication support for NorthScale
657
+ #######
658
+
659
+ def need_auth?
660
+ @options[:username] || ENV["MEMCACHE_USERNAME"]
661
+ end
662
+
663
+ def username
664
+ @options[:username] || ENV["MEMCACHE_USERNAME"]
665
+ end
666
+
667
+ def password
668
+ @options[:password] || ENV["MEMCACHE_PASSWORD"]
669
+ end
670
+
671
+ def sasl_authentication
672
+ Dalli.logger.info { "Dalli/SASL authenticating as #{username}" }
673
+
674
+ # negotiate
675
+ req = [REQUEST, OPCODES[:auth_negotiation], 0, 0, 0, 0, 0, 0, 0].pack(FORMAT[:noop])
676
+ write(req)
677
+
678
+ (extras, _type, status, count) = read_header.unpack(NORMAL_HEADER)
679
+ raise Dalli::NetworkError, "Unexpected message format: #{extras} #{count}" unless extras == 0 && count > 0
680
+ content = read(count).tr("\u0000", " ")
681
+ return Dalli.logger.debug("Authentication not required/supported by server") if status == 0x81
682
+ mechanisms = content.split(" ")
683
+ raise NotImplementedError, "Dalli only supports the PLAIN authentication mechanism" unless mechanisms.include?("PLAIN")
684
+
685
+ # request
686
+ mechanism = "PLAIN"
687
+ msg = "\x0#{username}\x0#{password}"
688
+ req = [REQUEST, OPCODES[:auth_request], mechanism.bytesize, 0, 0, 0, mechanism.bytesize + msg.bytesize, 0, 0, mechanism, msg].pack(FORMAT[:auth_request])
689
+ write(req)
690
+
691
+ (extras, _type, status, count) = read_header.unpack(NORMAL_HEADER)
692
+ raise Dalli::NetworkError, "Unexpected message format: #{extras} #{count}" unless extras == 0 && count > 0
693
+ content = read(count)
694
+ return Dalli.logger.info("Dalli/SASL: #{content}") if status == 0
695
+
696
+ raise Dalli::DalliError, "Error authenticating: #{status}" unless status == 0x21
697
+ raise NotImplementedError, "No two-step authentication mechanisms supported"
698
+ # (step, msg) = sasl.receive('challenge', content)
699
+ # raise Dalli::NetworkError, "Authentication failed" if sasl.failed? || step != 'response'
700
+ end
701
+ end
702
+ end
703
+ end