dalli 2.7.11 → 3.0.3

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