dalli 3.0.4 → 3.1.1

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,200 @@
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
+ # Response codes taken from:
13
+ # https://github.com/memcached/memcached/wiki/BinaryProtocolRevamped#response-status
14
+ RESPONSE_CODES = {
15
+ 0 => 'No error',
16
+ 1 => 'Key not found',
17
+ 2 => 'Key exists',
18
+ 3 => 'Value too large',
19
+ 4 => 'Invalid arguments',
20
+ 5 => 'Item not stored',
21
+ 6 => 'Incr/decr on a non-numeric value',
22
+ 7 => 'The vbucket belongs to another server',
23
+ 8 => 'Authentication error',
24
+ 9 => 'Authentication continue',
25
+ 0x20 => 'Authentication required',
26
+ 0x81 => 'Unknown command',
27
+ 0x82 => 'Out of memory',
28
+ 0x83 => 'Not supported',
29
+ 0x84 => 'Internal error',
30
+ 0x85 => 'Busy',
31
+ 0x86 => 'Temporary failure'
32
+ }.freeze
33
+
34
+ def initialize(io_source, value_marshaller)
35
+ @io_source = io_source
36
+ @value_marshaller = value_marshaller
37
+ end
38
+
39
+ def read(num_bytes)
40
+ @io_source.read(num_bytes)
41
+ end
42
+
43
+ def read_response
44
+ resp_header = ResponseHeader.new(read_header)
45
+ body = read(resp_header.body_len) if resp_header.body_len.positive?
46
+ [resp_header, body]
47
+ end
48
+
49
+ def unpack_response_body(extra_len, key_len, body, unpack)
50
+ bitflags = extra_len.positive? ? body.byteslice(0, extra_len).unpack1('N') : 0x0
51
+ key = body.byteslice(extra_len, key_len) if key_len.positive?
52
+ value = body.byteslice(extra_len + key_len, body.bytesize - (extra_len + key_len))
53
+ value = unpack ? @value_marshaller.retrieve(value, bitflags) : value
54
+ [key, value]
55
+ end
56
+
57
+ def read_header
58
+ read(ResponseHeader::SIZE) || raise(Dalli::NetworkError, 'No response')
59
+ end
60
+
61
+ def raise_on_not_ok!(resp_header)
62
+ return if resp_header.ok?
63
+
64
+ raise Dalli::DalliError, "Response error #{resp_header.status}: #{RESPONSE_CODES[resp_header.status]}"
65
+ end
66
+
67
+ def generic_response(unpack: false, cache_nils: false)
68
+ resp_header, body = read_response
69
+
70
+ return false if resp_header.not_stored? # Not stored, normal status for add operation
71
+ return cache_nils ? ::Dalli::NOT_FOUND : nil if resp_header.not_found?
72
+
73
+ raise_on_not_ok!(resp_header)
74
+ return true unless body
75
+
76
+ unpack_response_body(resp_header.extra_len, resp_header.key_len, body, unpack).last
77
+ end
78
+
79
+ ##
80
+ # Response for a storage operation. Returns the cas on success. False
81
+ # if the value wasn't stored. And raises an error on all other error
82
+ # codes from memcached.
83
+ ##
84
+ def storage_response
85
+ resp_header, = read_response
86
+ return false if resp_header.not_stored? # Not stored, normal status for add operation
87
+
88
+ raise_on_not_ok!(resp_header)
89
+ resp_header.cas
90
+ end
91
+
92
+ def no_body_response
93
+ resp_header, = read_response
94
+ return false if resp_header.not_stored? # Not stored, possible status for append/prepend
95
+
96
+ raise_on_not_ok!(resp_header)
97
+ true
98
+ end
99
+
100
+ def data_cas_response(unpack: true)
101
+ resp_header, body = read_response
102
+ return [nil, resp_header.cas] if resp_header.not_found?
103
+ return [nil, false] if resp_header.not_stored?
104
+
105
+ raise_on_not_ok!(resp_header)
106
+ return [nil, resp_header.cas] unless body
107
+
108
+ [unpack_response_body(resp_header.extra_len, resp_header.key_len, body, unpack).last, resp_header.cas]
109
+ end
110
+
111
+ def cas_response
112
+ data_cas_response.last
113
+ end
114
+
115
+ def multi_with_keys_response
116
+ hash = {}
117
+ loop do
118
+ resp_header, body = read_response
119
+ # This is the response to the terminating noop / end of stat
120
+ return hash if resp_header.ok? && resp_header.key_len.zero?
121
+
122
+ # Ignore any responses with non-zero status codes,
123
+ # such as errors from set operations. That allows
124
+ # this code to be used at the end of a multi
125
+ # block to clear any error responses from inside the multi.
126
+ next unless resp_header.ok?
127
+
128
+ key, value = unpack_response_body(resp_header.extra_len, resp_header.key_len, body, true)
129
+ hash[key] = value
130
+ end
131
+ end
132
+
133
+ def decr_incr_response
134
+ body = generic_response
135
+ body ? body.unpack1('Q>') : body
136
+ end
137
+
138
+ def validate_auth_format(extra_len, count)
139
+ return if extra_len.zero?
140
+
141
+ raise Dalli::NetworkError, "Unexpected message format: #{extra_len} #{count}"
142
+ end
143
+
144
+ def auth_response(buf = read_header)
145
+ resp_header = ResponseHeader.new(buf)
146
+ body_len = resp_header.body_len
147
+ validate_auth_format(resp_header.extra_len, body_len)
148
+ content = read(body_len) if body_len.positive?
149
+ [resp_header.status, content]
150
+ end
151
+
152
+ def contains_header?(buf)
153
+ return false unless buf
154
+
155
+ buf.bytesize >= ResponseHeader::SIZE
156
+ end
157
+
158
+ def response_header_from_buffer(buf)
159
+ header = buf.slice(0, ResponseHeader::SIZE)
160
+ ResponseHeader.new(header)
161
+ end
162
+
163
+ ##
164
+ # This method returns an array of values used in a pipelined
165
+ # getk process. The first value is the number of bytes by
166
+ # which to advance the pointer in the buffer. If the
167
+ # complete response is found in the buffer, this will
168
+ # be the response size. Otherwise it is zero.
169
+ #
170
+ # The remaining three values in the array are the ResponseHeader,
171
+ # key, and value.
172
+ ##
173
+ def getk_response_from_buffer(buf)
174
+ # There's no header in the buffer, so don't advance
175
+ return [0, nil, nil, nil] unless contains_header?(buf)
176
+
177
+ resp_header = response_header_from_buffer(buf)
178
+ body_len = resp_header.body_len
179
+
180
+ # We have a complete response that has no body.
181
+ # This is either the response to the terminating
182
+ # noop or, if the status is not zero, an intermediate
183
+ # error response that needs to be discarded.
184
+ return [ResponseHeader::SIZE, resp_header, nil, nil] if body_len.zero?
185
+
186
+ resp_size = ResponseHeader::SIZE + body_len
187
+ # The header is in the buffer, but the body is not. As we don't have
188
+ # a complete response, don't advance the buffer
189
+ return [0, nil, nil, nil] unless buf.bytesize >= resp_size
190
+
191
+ # The full response is in our buffer, so parse it and return
192
+ # the values
193
+ body = buf.slice(ResponseHeader::SIZE, body_len)
194
+ key, value = unpack_response_body(resp_header.extra_len, resp_header.key_len, body, true)
195
+ [resp_size, resp_header, key, value]
196
+ end
197
+ end
198
+ end
199
+ end
200
+ 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