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.

@@ -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(String.new(""), "w")
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, "rb")
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
- require 'thread'
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::Server.extend(Dalli::Threadsafe)
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(op, *args)
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