dalli 3.0.4 → 3.0.5
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.
- checksums.yaml +4 -4
- data/Gemfile +11 -5
- data/History.md +7 -0
- data/README.md +25 -134
- data/lib/dalli/cas/client.rb +2 -0
- data/lib/dalli/client.rb +183 -193
- data/lib/dalli/compressor.rb +13 -4
- data/lib/dalli/key_manager.rb +113 -0
- data/lib/dalli/options.rb +2 -2
- data/lib/dalli/protocol/binary/request_formatter.rb +109 -0
- data/lib/dalli/protocol/binary/response_processor.rb +149 -0
- data/lib/dalli/protocol/binary/sasl_authentication.rb +57 -0
- data/lib/dalli/protocol/binary.rb +271 -430
- data/lib/dalli/protocol/server_config_parser.rb +23 -6
- data/lib/dalli/protocol/value_marshaller.rb +59 -0
- data/lib/dalli/protocol/value_serializer.rb +91 -0
- data/lib/dalli/protocol.rb +2 -3
- data/lib/dalli/ring.rb +91 -35
- data/lib/dalli/server.rb +2 -2
- data/lib/dalli/servers_arg_normalizer.rb +54 -0
- data/lib/dalli/socket.rb +96 -52
- data/lib/dalli/version.rb +3 -1
- data/lib/dalli.rb +31 -14
- data/lib/rack/session/dalli.rb +28 -18
- metadata +70 -6
data/lib/dalli/options.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require 'monitor'
|
4
4
|
|
5
5
|
module Dalli
|
6
6
|
# Make Dalli threadsafe by using a lock around all
|
@@ -13,7 +13,7 @@ module Dalli
|
|
13
13
|
obj.init_threadsafe
|
14
14
|
end
|
15
15
|
|
16
|
-
def request(
|
16
|
+
def request(opcode, *args)
|
17
17
|
@lock.synchronize do
|
18
18
|
super
|
19
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? && count.positive?
|
136
|
+
|
137
|
+
raise Dalli::NetworkError, "Unexpected message format: #{extra_len} #{count}"
|
138
|
+
end
|
139
|
+
|
140
|
+
def auth_response
|
141
|
+
(extra_len, _type, status, count) = read_header.unpack(RESP_HEADER)
|
142
|
+
validate_auth_format(extra_len, count)
|
143
|
+
content = read(count)
|
144
|
+
[status, content]
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,57 @@
|
|
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
|
+
# TODO: Determine if this substitution is needed
|
15
|
+
content.tr("\u0000", ' ')
|
16
|
+
mechanisms = content.split
|
17
|
+
[status, mechanisms]
|
18
|
+
end
|
19
|
+
|
20
|
+
PLAIN_AUTH = 'PLAIN'
|
21
|
+
|
22
|
+
def supported_mechanisms!(mechanisms)
|
23
|
+
unless mechanisms.include?(PLAIN_AUTH)
|
24
|
+
raise NotImplementedError,
|
25
|
+
'Dalli only supports the PLAIN authentication mechanism'
|
26
|
+
end
|
27
|
+
[PLAIN_AUTH]
|
28
|
+
end
|
29
|
+
|
30
|
+
def authenticate_with_plain
|
31
|
+
write(RequestFormatter.standard_request(opkey: :auth_request,
|
32
|
+
key: PLAIN_AUTH,
|
33
|
+
value: "\x0#{username}\x0#{password}"))
|
34
|
+
@response_processor.auth_response
|
35
|
+
end
|
36
|
+
|
37
|
+
def authenticate_connection
|
38
|
+
Dalli.logger.info { "Dalli/SASL authenticating as #{username}" }
|
39
|
+
|
40
|
+
status, mechanisms = perform_auth_negotiation
|
41
|
+
return Dalli.logger.debug('Authentication not required/supported by server') if status == 0x81
|
42
|
+
|
43
|
+
supported_mechanisms!(mechanisms)
|
44
|
+
status, content = authenticate_with_plain
|
45
|
+
|
46
|
+
return Dalli.logger.info("Dalli/SASL: #{content}") if status.zero?
|
47
|
+
|
48
|
+
raise Dalli::DalliError, "Error authenticating: #{status}" unless status == 0x21
|
49
|
+
|
50
|
+
raise NotImplementedError, 'No two-step authentication mechanisms supported'
|
51
|
+
# (step, msg) = sasl.receive('challenge', content)
|
52
|
+
# raise Dalli::NetworkError, "Authentication failed" if sasl.failed? || step != 'response'
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|