dalli 0.9.0 → 0.9.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.
- data/Gemfile +5 -1
- data/History.md +14 -0
- data/Performance.md +19 -18
- data/README.md +14 -1
- data/TODO.md +6 -0
- data/dalli.gemspec +5 -2
- data/lib/dalli.rb +10 -4
- data/lib/dalli/client.rb +58 -12
- data/lib/dalli/ring.rb +12 -8
- data/lib/dalli/sasl/anonymous.rb +14 -0
- data/lib/dalli/sasl/base.rb +89 -0
- data/lib/dalli/sasl/base64.rb +14 -0
- data/lib/dalli/sasl/digest_md5.rb +175 -0
- data/lib/dalli/sasl/plain.rb +18 -0
- data/lib/dalli/server.rb +124 -12
- data/lib/dalli/version.rb +1 -1
- data/test/helper.rb +8 -0
- data/test/memcached_mock.rb +49 -3
- data/test/test_active_support.rb +60 -47
- data/test/test_benchmark.rb +79 -76
- data/test/test_dalli.rb +230 -69
- data/test/test_network.rb +5 -9
- metadata +29 -9
@@ -0,0 +1,175 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
|
3
|
+
module SASL
|
4
|
+
##
|
5
|
+
# RFC 2831:
|
6
|
+
# http://tools.ietf.org/html/rfc2831
|
7
|
+
class DigestMD5 < Mechanism
|
8
|
+
attr_writer :cnonce
|
9
|
+
|
10
|
+
def initialize(*a)
|
11
|
+
super
|
12
|
+
@nonce_count = 0
|
13
|
+
end
|
14
|
+
|
15
|
+
def start
|
16
|
+
@state = nil
|
17
|
+
unless defined? @nonce
|
18
|
+
['auth', nil]
|
19
|
+
else
|
20
|
+
# reauthentication
|
21
|
+
receive('challenge', '')
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def receive(message_name, content)
|
26
|
+
if message_name == 'challenge'
|
27
|
+
c = decode_challenge(content)
|
28
|
+
|
29
|
+
unless c['rspauth']
|
30
|
+
response = {}
|
31
|
+
if defined?(@nonce) && response['nonce'].nil?
|
32
|
+
# Could be reauth
|
33
|
+
else
|
34
|
+
# No reauth:
|
35
|
+
@nonce_count = 0
|
36
|
+
end
|
37
|
+
@nonce ||= c['nonce']
|
38
|
+
response['nonce'] = @nonce
|
39
|
+
response['charset'] = 'utf-8'
|
40
|
+
response['username'] = preferences.username
|
41
|
+
response['realm'] = c['realm'] || preferences.realm
|
42
|
+
@cnonce = generate_nonce unless defined? @cnonce
|
43
|
+
response['cnonce'] = @cnonce
|
44
|
+
@nc = next_nc
|
45
|
+
response['nc'] = @nc
|
46
|
+
@qop = c['qop'] || 'auth'
|
47
|
+
response['qop'] = @qop
|
48
|
+
response['digest-uri'] = preferences.digest_uri #"memcached/#{self.hostname}"
|
49
|
+
response['response'] = response_value(response['nonce'], response['nc'], response['cnonce'], response['qop'])
|
50
|
+
['response', encode_response(response)]
|
51
|
+
else
|
52
|
+
rspauth_expected = response_value(@nonce, @nc, @cnonce, @qop, '')
|
53
|
+
p :rspauth_received=>c['rspauth'], :rspauth_expected=>rspauth_expected
|
54
|
+
if c['rspauth'] == rspauth_expected
|
55
|
+
['response', nil]
|
56
|
+
else
|
57
|
+
# Bogus server?
|
58
|
+
@state = :failure
|
59
|
+
['failure', nil]
|
60
|
+
end
|
61
|
+
end
|
62
|
+
else
|
63
|
+
# No challenge? Might be success or failure
|
64
|
+
super
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def decode_challenge(text)
|
71
|
+
challenge = {}
|
72
|
+
|
73
|
+
state = :key
|
74
|
+
key = ''
|
75
|
+
value = ''
|
76
|
+
|
77
|
+
text.scan(/./) do |ch|
|
78
|
+
if state == :key
|
79
|
+
if ch == '='
|
80
|
+
state = :value
|
81
|
+
elsif ch =~ /\S/
|
82
|
+
key += ch
|
83
|
+
end
|
84
|
+
|
85
|
+
elsif state == :value
|
86
|
+
if ch == ','
|
87
|
+
challenge[key] = value
|
88
|
+
key = ''
|
89
|
+
value = ''
|
90
|
+
state = :key
|
91
|
+
elsif ch == '"' and value == ''
|
92
|
+
state = :quote
|
93
|
+
else
|
94
|
+
value += ch
|
95
|
+
end
|
96
|
+
|
97
|
+
elsif state == :quote
|
98
|
+
if ch == '"'
|
99
|
+
state = :value
|
100
|
+
else
|
101
|
+
value += ch
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
challenge[key] = value unless key == ''
|
106
|
+
|
107
|
+
p :decode_challenge => challenge
|
108
|
+
challenge
|
109
|
+
end
|
110
|
+
|
111
|
+
def encode_response(response)
|
112
|
+
p :encode_response => response
|
113
|
+
response.collect do |k,v|
|
114
|
+
if v.include?('"')
|
115
|
+
v.sub!('\\', '\\\\')
|
116
|
+
v.sub!('"', '\\"')
|
117
|
+
"#{k}=\"#{v}\""
|
118
|
+
else
|
119
|
+
"#{k}=#{v}"
|
120
|
+
end
|
121
|
+
end.join(',')
|
122
|
+
end
|
123
|
+
|
124
|
+
def generate_nonce
|
125
|
+
nonce = ''
|
126
|
+
while nonce.length < 16
|
127
|
+
c = rand(128).chr
|
128
|
+
nonce += c if c =~ /^[a-zA-Z0-9]$/
|
129
|
+
end
|
130
|
+
nonce
|
131
|
+
end
|
132
|
+
|
133
|
+
##
|
134
|
+
# Function from RFC2831
|
135
|
+
def h(s); Digest::MD5.digest(s); end
|
136
|
+
##
|
137
|
+
# Function from RFC2831
|
138
|
+
def hh(s); Digest::MD5.hexdigest(s); end
|
139
|
+
|
140
|
+
##
|
141
|
+
# Calculate the value for the response field
|
142
|
+
def response_value(nonce, nc, cnonce, qop, a2_prefix='AUTHENTICATE')
|
143
|
+
p :response_value => {:nonce=>nonce,
|
144
|
+
:cnonce=>cnonce,
|
145
|
+
:qop=>qop,
|
146
|
+
:username=>preferences.username,
|
147
|
+
:realm=>preferences.realm,
|
148
|
+
:password=>preferences.password,
|
149
|
+
:authzid=>preferences.authzid}
|
150
|
+
a1_h = h("#{preferences.username}:#{preferences.realm}:#{preferences.password}")
|
151
|
+
a1 = "#{a1_h}:#{nonce}:#{cnonce}"
|
152
|
+
if preferences.authzid
|
153
|
+
a1 += ":#{preferences.authzid}"
|
154
|
+
end
|
155
|
+
if qop && (qop.downcase == 'auth-int' || qop.downcase == 'auth-conf')
|
156
|
+
a2 = "#{a2_prefix}:#{preferences.digest_uri}:00000000000000000000000000000000"
|
157
|
+
else
|
158
|
+
a2 = "#{a2_prefix}:#{preferences.digest_uri}"
|
159
|
+
end
|
160
|
+
hh("#{hh(a1)}:#{nonce}:#{nc}:#{cnonce}:#{qop}:#{hh(a2)}")
|
161
|
+
end
|
162
|
+
|
163
|
+
def next_nc
|
164
|
+
@nonce_count += 1
|
165
|
+
s = @nonce_count.to_s
|
166
|
+
s = "0#{s}" while s.length < 8
|
167
|
+
s
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
# TODO: need to test
|
172
|
+
#MECHANISMS['DIGEST-MD5'] = SASL::DigestMD5
|
173
|
+
|
174
|
+
end
|
175
|
+
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module SASL
|
2
|
+
##
|
3
|
+
# RFC 4616:
|
4
|
+
# http://tools.ietf.org/html/rfc4616
|
5
|
+
class Plain < Mechanism
|
6
|
+
|
7
|
+
def start
|
8
|
+
@state = nil
|
9
|
+
message = [preferences.authzid.to_s,
|
10
|
+
preferences.username,
|
11
|
+
preferences.password].join("\000")
|
12
|
+
['auth', message]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
MECHANISMS['PLAIN'] = SASL::Plain
|
17
|
+
|
18
|
+
end
|
data/lib/dalli/server.rb
CHANGED
@@ -16,6 +16,7 @@ module Dalli
|
|
16
16
|
Dalli.logger.debug { "#{@hostname}:#{@port} running memcached v#{request(:version)}" }
|
17
17
|
end
|
18
18
|
|
19
|
+
# Chokepoint method for instrumentation
|
19
20
|
def request(op, *args)
|
20
21
|
begin
|
21
22
|
send(op, *args)
|
@@ -24,8 +25,8 @@ module Dalli
|
|
24
25
|
rescue Dalli::DalliError
|
25
26
|
raise
|
26
27
|
rescue Exception => ex
|
27
|
-
|
28
|
-
|
28
|
+
Dalli.logger.error "Unexpected exception in Dalli: #{ex.class.name}: #{ex.message}"
|
29
|
+
Dalli.logger.error ex.backtrace.join("\n\t")
|
29
30
|
down!
|
30
31
|
end
|
31
32
|
end
|
@@ -44,6 +45,8 @@ module Dalli
|
|
44
45
|
def unlock!
|
45
46
|
end
|
46
47
|
|
48
|
+
# NOTE: Additional public methods should be overridden in Dalli::Threadsafe
|
49
|
+
|
47
50
|
private
|
48
51
|
|
49
52
|
def down!
|
@@ -66,7 +69,7 @@ module Dalli
|
|
66
69
|
write(req)
|
67
70
|
end
|
68
71
|
|
69
|
-
def set(key, value, ttl
|
72
|
+
def set(key, value, ttl)
|
70
73
|
raise Dalli::DalliError, "Value too large, memcached can only store 1MB of data per key" if value.size > ONE_MB
|
71
74
|
|
72
75
|
req = [REQUEST, OPCODES[:set], key.size, 8, 0, 0, value.size + key.size + 8, 0, 0, 0, ttl, key, value].pack(FORMAT[:set])
|
@@ -80,10 +83,10 @@ module Dalli
|
|
80
83
|
generic_response
|
81
84
|
end
|
82
85
|
|
83
|
-
def add(key, value, ttl)
|
86
|
+
def add(key, value, ttl, cas)
|
84
87
|
raise Dalli::DalliError, "Value too large, memcached can only store 1MB of data per key" if value.size > ONE_MB
|
85
88
|
|
86
|
-
req = [REQUEST, OPCODES[:add], key.size, 8, 0, 0, value.size + key.size + 8, 0,
|
89
|
+
req = [REQUEST, OPCODES[:add], key.size, 8, 0, 0, value.size + key.size + 8, 0, cas, 0, ttl, key, value].pack(FORMAT[:add])
|
87
90
|
write(req)
|
88
91
|
generic_response
|
89
92
|
end
|
@@ -99,15 +102,29 @@ module Dalli
|
|
99
102
|
write(req)
|
100
103
|
generic_response
|
101
104
|
end
|
102
|
-
|
103
|
-
def decr(key, count)
|
104
|
-
|
105
|
+
|
106
|
+
def decr(key, count, ttl, default)
|
107
|
+
expiry = default ? ttl : 0xFFFFFFFF
|
108
|
+
default ||= 0
|
109
|
+
(h, l) = split(count)
|
110
|
+
(dh, dl) = split(default)
|
111
|
+
req = [REQUEST, OPCODES[:decr], key.size, 20, 0, 0, key.size + 20, 0, 0, h, l, dh, dl, expiry, key].pack(FORMAT[:decr])
|
112
|
+
write(req)
|
113
|
+
body = generic_response
|
114
|
+
body ? longlong(*body.unpack('NN')) : body
|
105
115
|
end
|
106
116
|
|
107
|
-
def incr(key, count)
|
108
|
-
|
117
|
+
def incr(key, count, ttl, default)
|
118
|
+
expiry = default ? ttl : 0xFFFFFFFF
|
119
|
+
default ||= 0
|
120
|
+
(h, l) = split(count)
|
121
|
+
(dh, dl) = split(default)
|
122
|
+
req = [REQUEST, OPCODES[:incr], key.size, 20, 0, 0, key.size + 20, 0, 0, h, l, dh, dl, expiry, key].pack(FORMAT[:incr])
|
123
|
+
write(req)
|
124
|
+
body = generic_response
|
125
|
+
body ? longlong(*body.unpack('NN')) : body
|
109
126
|
end
|
110
|
-
|
127
|
+
|
111
128
|
# Noop is a keepalive operation but also used to demarcate the end of a set of pipelined commands.
|
112
129
|
# We need to read all the responses at once.
|
113
130
|
def noop
|
@@ -140,6 +157,27 @@ module Dalli
|
|
140
157
|
keyvalue_response
|
141
158
|
end
|
142
159
|
|
160
|
+
def cas(key)
|
161
|
+
req = [REQUEST, OPCODES[:get], key.size, 0, 0, 0, key.size, 0, 0, key].pack(FORMAT[:get])
|
162
|
+
write(req)
|
163
|
+
cas_response
|
164
|
+
end
|
165
|
+
|
166
|
+
def cas_response
|
167
|
+
header = read(24)
|
168
|
+
raise Dalli::NetworkError, 'No response' if !header
|
169
|
+
(extras, status, count, _, cas) = header.unpack(CAS_HEADER)
|
170
|
+
data = read(count) if count > 0
|
171
|
+
if status == 1
|
172
|
+
nil
|
173
|
+
elsif status != 0
|
174
|
+
raise Dalli::DalliError, "Response error #{status}: #{RESPONSE_CODES[status]}"
|
175
|
+
elsif data
|
176
|
+
data = data[extras..-1] if extras != 0
|
177
|
+
end
|
178
|
+
[data, cas]
|
179
|
+
end
|
180
|
+
|
143
181
|
def generic_response
|
144
182
|
header = read(24)
|
145
183
|
raise Dalli::NetworkError, 'No response' if !header
|
@@ -147,6 +185,8 @@ module Dalli
|
|
147
185
|
data = read(count) if count > 0
|
148
186
|
if status == 1
|
149
187
|
nil
|
188
|
+
elsif status == 2
|
189
|
+
false # Not stored, normal status for add operation
|
150
190
|
elsif status != 0
|
151
191
|
raise Dalli::DalliError, "Response error #{status}: #{RESPONSE_CODES[status]}"
|
152
192
|
elsif data
|
@@ -209,6 +249,7 @@ module Dalli
|
|
209
249
|
# end ugly code
|
210
250
|
|
211
251
|
sock.setsockopt Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1
|
252
|
+
sasl_authentication(sock) if Dalli::Server.need_auth?
|
212
253
|
sock
|
213
254
|
end
|
214
255
|
end
|
@@ -237,7 +278,6 @@ module Dalli
|
|
237
278
|
raise Timeout::Error, "IO timeout"
|
238
279
|
end
|
239
280
|
end
|
240
|
-
raise Errno::EINVAL, "Not enough data to fulfill read request: #{value.inspect}" if value.size != count
|
241
281
|
value
|
242
282
|
rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED, Errno::EBADF, Errno::EINVAL, Timeout::Error, EOFError
|
243
283
|
down!
|
@@ -245,6 +285,15 @@ module Dalli
|
|
245
285
|
end
|
246
286
|
end
|
247
287
|
|
288
|
+
def split(n)
|
289
|
+
[0xFFFFFFFF & n, n >> 32]
|
290
|
+
end
|
291
|
+
|
292
|
+
def longlong(a, b)
|
293
|
+
a | (b << 32)
|
294
|
+
end
|
295
|
+
|
296
|
+
CAS_HEADER = '@4vnNNQ'
|
248
297
|
NORMAL_HEADER = '@4vnN'
|
249
298
|
KV_HEADER = '@2n@6nN'
|
250
299
|
|
@@ -259,6 +308,7 @@ module Dalli
|
|
259
308
|
4 => 'Invalid arguments',
|
260
309
|
5 => 'Item not stored',
|
261
310
|
6 => 'Incr/decr on a non-numeric value',
|
311
|
+
0x20 => 'Authentication required',
|
262
312
|
0x81 => 'Unknown command',
|
263
313
|
0x82 => 'Out of memory',
|
264
314
|
}
|
@@ -278,6 +328,9 @@ module Dalli
|
|
278
328
|
:append => 0x0E,
|
279
329
|
:prepend => 0x0F,
|
280
330
|
:stat => 0x10,
|
331
|
+
:auth_negotiation => 0x20,
|
332
|
+
:auth_request => 0x21,
|
333
|
+
:auth_continue => 0x22,
|
281
334
|
}
|
282
335
|
|
283
336
|
HEADER = "CCnCCnNNQ"
|
@@ -296,8 +349,67 @@ module Dalli
|
|
296
349
|
:stat => 'a*',
|
297
350
|
:append => 'a*a*',
|
298
351
|
:prepend => 'a*a*',
|
352
|
+
:auth_request => 'a*a*',
|
353
|
+
:auth_continue => 'a*a*',
|
299
354
|
}
|
300
355
|
FORMAT = OP_FORMAT.inject({}) { |memo, (k, v)| memo[k] = HEADER + v; memo }
|
356
|
+
|
357
|
+
|
358
|
+
#######
|
359
|
+
# SASL authentication support for NorthScale
|
360
|
+
#######
|
361
|
+
|
362
|
+
def self.need_auth?
|
363
|
+
ENV['MEMCACHE_USERNAME']
|
364
|
+
end
|
301
365
|
|
366
|
+
def init_sasl
|
367
|
+
require 'dalli/sasl/base'
|
368
|
+
require 'dalli/sasl/base64'
|
369
|
+
require 'dalli/sasl/digest_md5'
|
370
|
+
require 'dalli/sasl/plain'
|
371
|
+
end
|
372
|
+
|
373
|
+
def username
|
374
|
+
ENV['MEMCACHE_USERNAME']
|
375
|
+
end
|
376
|
+
|
377
|
+
def password
|
378
|
+
ENV['MEMCACHE_PASSWORD']
|
379
|
+
end
|
380
|
+
|
381
|
+
def sasl_authentication(socket)
|
382
|
+
init_sasl if !defined?(::SASL)
|
383
|
+
|
384
|
+
# negotiate
|
385
|
+
req = [REQUEST, OPCODES[:auth_negotiation], 0, 0, 0, 0, 0, 0, 0].pack(FORMAT[:noop])
|
386
|
+
socket.write(req)
|
387
|
+
header = socket.read(24)
|
388
|
+
raise Dalli::NetworkError, 'No response' if !header
|
389
|
+
(extras, status, count) = header.unpack(NORMAL_HEADER)
|
390
|
+
raise Dalli::NetworkError, "Unexpected message format: #{extras} #{count}" unless extras == 0 && count > 0
|
391
|
+
content = socket.read(count)
|
392
|
+
return (Dalli.logger.debug("Authentication not required/supported by server")) if status == 0x81
|
393
|
+
mechanisms = content.split(' ')
|
394
|
+
|
395
|
+
# request
|
396
|
+
sasl = ::SASL.new(mechanisms)
|
397
|
+
msg = sasl.start[1]
|
398
|
+
mechanism = sasl.name
|
399
|
+
#p [mechanism, msg]
|
400
|
+
req = [REQUEST, OPCODES[:auth_request], mechanism.size, 0, 0, 0, mechanism.size + msg.size, 0, 0, mechanism, msg].pack(FORMAT[:auth_request])
|
401
|
+
socket.write(req)
|
402
|
+
|
403
|
+
header = socket.read(24)
|
404
|
+
raise Dalli::NetworkError, 'No response' if !header
|
405
|
+
(extras, status, count) = header.unpack(NORMAL_HEADER)
|
406
|
+
raise Dalli::NetworkError, "Unexpected message format: #{extras} #{count}" unless extras == 0 && count > 0
|
407
|
+
content = socket.read(count)
|
408
|
+
raise Dalli::NetworkError, "Error authenticating: #{status}" unless status == 0x21
|
409
|
+
(step, msg) = sasl.receive('challenge', content)
|
410
|
+
raise Dalli::NetworkError, "Authentication failed" if sasl.failed? || step != 'response'
|
411
|
+
|
412
|
+
|
413
|
+
end
|
302
414
|
end
|
303
415
|
end
|
data/lib/dalli/version.rb
CHANGED