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.

@@ -0,0 +1,14 @@
1
+ begin
2
+ require 'base64'
3
+ rescue LoadError
4
+ # Ruby 1.9 compat
5
+ module Base64
6
+ def self.encode64(data)
7
+ [data].pack('m')
8
+ end
9
+
10
+ def self.decode64(data64)
11
+ data64.unpack('m')[0]
12
+ end
13
+ end
14
+ end
@@ -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
@@ -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
- puts "Unexpected exception: #{ex.class.name}: #{ex.message}"
28
- puts ex.backtrace.join("\n")
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=0)
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, 0, 0, ttl, key, value].pack(FORMAT[:add])
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
- raise NotImplementedError
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
- raise NotImplementedError
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
@@ -1,3 +1,3 @@
1
1
  module Dalli
2
- VERSION = '0.9.0'
2
+ VERSION = '0.9.1'
3
3
  end