dalli 2.7.11 → 3.0.0

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