dalli 3.2.8 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +24 -2
- data/Gemfile +2 -0
- data/lib/dalli/client.rb +4 -4
- data/lib/dalli/key_manager.rb +2 -2
- data/lib/dalli/protocol/base.rb +2 -0
- data/lib/dalli/protocol/binary.rb +1 -0
- data/lib/dalli/protocol/connection_manager.rb +5 -7
- data/lib/dalli/protocol/meta/key_regularizer.rb +1 -1
- data/lib/dalli/protocol/meta/request_formatter.rb +5 -7
- data/lib/dalli/protocol/meta/response_processor.rb +12 -4
- data/lib/dalli/protocol/meta.rb +4 -0
- data/lib/dalli/protocol/server_config_parser.rb +1 -1
- data/lib/dalli/protocol/ttl_sanitizer.rb +1 -1
- data/lib/dalli/protocol/value_compressor.rb +2 -11
- data/lib/dalli/protocol/value_marshaller.rb +1 -1
- data/lib/dalli/protocol/value_serializer.rb +49 -40
- data/lib/dalli/socket.rb +29 -6
- data/lib/dalli/version.rb +1 -1
- data/lib/dalli.rb +3 -2
- data/lib/rack/session/dalli.rb +43 -8
- metadata +19 -9
- data/lib/dalli/server.rb +0 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 86e39b2869b7c916472e88e6cbbfb6aa1f6533aa35c9e1f1c704cffcbc680f8a
|
|
4
|
+
data.tar.gz: bf2e56610c6bae187d561ccf09105e6f5b4a29eed1308d089745ef22cf5b673b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e2faab814abb4b25a53d87048f7e4e81e49c609bd64725cb18607670110da3d5b8038544598c44575831b9038500f9d8220a56ec28c16b9517a21d442212d12b
|
|
7
|
+
data.tar.gz: 3e5a7c326c908d72d2d8ce2211b27f67acebbbf97c279c5faf3ba2914ec7409df7f259a9082d82903cc9ae6b6716a137994aba849321546b8d3d3a35f8f7f5af
|
data/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,30 @@
|
|
|
1
1
|
Dalli Changelog
|
|
2
2
|
=====================
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
==========
|
|
4
|
+
4.0.0
|
|
5
|
+
==========
|
|
6
|
+
|
|
7
|
+
BREAKING CHANGES:
|
|
8
|
+
|
|
9
|
+
- Require Ruby 3.1+ (dropped support for Ruby 2.6, 2.7, and 3.0)
|
|
10
|
+
- Removed `Dalli::Server` deprecated alias - use `Dalli::Protocol::Binary` instead
|
|
11
|
+
- Removed `:compression` option - use `:compress` instead
|
|
12
|
+
- Removed `close_on_fork` method - use `reconnect_on_fork` instead
|
|
13
|
+
|
|
14
|
+
Other changes:
|
|
15
|
+
|
|
16
|
+
- Add security warning when using default Marshal serializer (silence with `silence_marshal_warning: true`)
|
|
17
|
+
- Add defense-in-depth input validation for stats command arguments
|
|
18
|
+
- Add `string_fastpath` option to skip serialization for simple strings (byroot)
|
|
19
|
+
- Meta protocol set performance improvement (danmayer)
|
|
20
|
+
- Fix connection_pool 3.0 compatibility for Rack session store
|
|
21
|
+
- Fix session recovery after deletion (stengineering0)
|
|
22
|
+
- Fix cannot read response data included terminator `\r\n` when use meta protocol (matsubara0507)
|
|
23
|
+
- Support SERVER_ERROR response from Memcached as per the [memcached spec](https://github.com/memcached/memcached/blob/e43364402195c8e822bb8f88755a60ab8bbed62a/doc/protocol.txt#L172) (grcooper)
|
|
24
|
+
- Update Socket timeout handling to use Socket#timeout= when available (nickamorim)
|
|
25
|
+
- Serializer: reraise all .load errors as UnmarshalError (olleolleolle)
|
|
26
|
+
- Reconnect gracefully when a fork is detected instead of crashing (PatrickTulskie)
|
|
27
|
+
- Update CI to test against memcached 1.6.40
|
|
6
28
|
|
|
7
29
|
3.2.8
|
|
8
30
|
==========
|
data/Gemfile
CHANGED
data/lib/dalli/client.rb
CHANGED
|
@@ -155,8 +155,8 @@ module Dalli
|
|
|
155
155
|
# - nil if the key did not exist.
|
|
156
156
|
# - false if the value was changed by someone else.
|
|
157
157
|
# - true if the value was successfully updated.
|
|
158
|
-
def cas(key, ttl = nil, req_options = nil, &
|
|
159
|
-
cas_core(key, false, ttl, req_options, &
|
|
158
|
+
def cas(key, ttl = nil, req_options = nil, &)
|
|
159
|
+
cas_core(key, false, ttl, req_options, &)
|
|
160
160
|
end
|
|
161
161
|
|
|
162
162
|
##
|
|
@@ -166,8 +166,8 @@ module Dalli
|
|
|
166
166
|
# Returns:
|
|
167
167
|
# - false if the value was changed by someone else.
|
|
168
168
|
# - true if the value was successfully updated.
|
|
169
|
-
def cas!(key, ttl = nil, req_options = nil, &
|
|
170
|
-
cas_core(key, true, ttl, req_options, &
|
|
169
|
+
def cas!(key, ttl = nil, req_options = nil, &)
|
|
170
|
+
cas_core(key, true, ttl, req_options, &)
|
|
171
171
|
end
|
|
172
172
|
|
|
173
173
|
##
|
data/lib/dalli/key_manager.rb
CHANGED
|
@@ -30,7 +30,7 @@ module Dalli
|
|
|
30
30
|
|
|
31
31
|
def initialize(client_options)
|
|
32
32
|
@key_options =
|
|
33
|
-
DEFAULTS.merge(client_options.
|
|
33
|
+
DEFAULTS.merge(client_options.slice(*OPTIONS))
|
|
34
34
|
validate_digest_class_option(@key_options)
|
|
35
35
|
|
|
36
36
|
@namespace = namespace_from_options
|
|
@@ -77,7 +77,7 @@ module Dalli
|
|
|
77
77
|
def namespace_regexp
|
|
78
78
|
return /\A#{Regexp.escape(evaluate_namespace)}:/ if namespace.is_a?(Proc)
|
|
79
79
|
|
|
80
|
-
@namespace_regexp ||= /\A#{Regexp.escape(namespace)}
|
|
80
|
+
@namespace_regexp ||= /\A#{Regexp.escape(namespace)}:/ unless namespace.nil?
|
|
81
81
|
end
|
|
82
82
|
|
|
83
83
|
def validate_digest_class_option(opts)
|
data/lib/dalli/protocol/base.rb
CHANGED
|
@@ -154,6 +154,8 @@ module Dalli
|
|
|
154
154
|
private
|
|
155
155
|
|
|
156
156
|
ALLOWED_QUIET_OPS = %i[add replace set delete incr decr append prepend flush noop].freeze
|
|
157
|
+
private_constant :ALLOWED_QUIET_OPS
|
|
158
|
+
|
|
157
159
|
def verify_allowed_quiet!(opkey)
|
|
158
160
|
return if ALLOWED_QUIET_OPS.include?(opkey)
|
|
159
161
|
|
|
@@ -119,6 +119,7 @@ module Dalli
|
|
|
119
119
|
# if the key doesn't already exist, rather than
|
|
120
120
|
# setting the initial value
|
|
121
121
|
NOT_FOUND_EXPIRY = 0xFFFFFFFF
|
|
122
|
+
private_constant :NOT_FOUND_EXPIRY
|
|
122
123
|
|
|
123
124
|
def decr_incr(opkey, key, count, ttl, initial)
|
|
124
125
|
expiry = initial ? TtlSanitizer.sanitize(ttl) : NOT_FOUND_EXPIRY
|
|
@@ -100,13 +100,13 @@ module Dalli
|
|
|
100
100
|
|
|
101
101
|
def confirm_ready!
|
|
102
102
|
close if request_in_progress?
|
|
103
|
-
|
|
103
|
+
reconnect_on_fork if fork_detected?
|
|
104
104
|
end
|
|
105
105
|
|
|
106
106
|
def confirm_in_progress!
|
|
107
107
|
raise '[Dalli] No request in progress. This may be a bug in Dalli.' unless request_in_progress?
|
|
108
108
|
|
|
109
|
-
|
|
109
|
+
reconnect_on_fork if fork_detected?
|
|
110
110
|
end
|
|
111
111
|
|
|
112
112
|
def close
|
|
@@ -212,20 +212,18 @@ module Dalli
|
|
|
212
212
|
end
|
|
213
213
|
|
|
214
214
|
def log_warn_message(err_or_string)
|
|
215
|
-
detail = err_or_string.is_a?(String) ? err_or_string : "#{err_or_string.class}: #{err_or_string.message}"
|
|
216
215
|
Dalli.logger.warn do
|
|
217
216
|
detail = err_or_string.is_a?(String) ? err_or_string : "#{err_or_string.class}: #{err_or_string.message}"
|
|
218
217
|
"#{name} failed (count: #{@fail_count}) #{detail}"
|
|
219
218
|
end
|
|
220
219
|
end
|
|
221
220
|
|
|
222
|
-
def
|
|
221
|
+
def reconnect_on_fork
|
|
223
222
|
message = 'Fork detected, re-connecting child process...'
|
|
224
223
|
Dalli.logger.info { message }
|
|
225
|
-
# Close socket on a fork
|
|
226
|
-
# on next request.
|
|
224
|
+
# Close socket on a fork and reconnect immediately
|
|
227
225
|
close
|
|
228
|
-
|
|
226
|
+
establish_connection
|
|
229
227
|
end
|
|
230
228
|
|
|
231
229
|
def fork_detected?
|
|
@@ -10,7 +10,7 @@ module Dalli
|
|
|
10
10
|
# memcached supports the use of base64 hashes for keys containing
|
|
11
11
|
# whitespace or non-ASCII characters, provided the 'b' flag is included in the request.
|
|
12
12
|
class KeyRegularizer
|
|
13
|
-
WHITESPACE = /\s
|
|
13
|
+
WHITESPACE = /\s/
|
|
14
14
|
|
|
15
15
|
def self.encode(key)
|
|
16
16
|
return [key, false] if key.ascii_only? && !WHITESPACE.match(key)
|
|
@@ -13,7 +13,6 @@ module Dalli
|
|
|
13
13
|
# and introducing an intermediate object seems like overkill.
|
|
14
14
|
#
|
|
15
15
|
# rubocop:disable Metrics/CyclomaticComplexity
|
|
16
|
-
# rubocop:disable Metrics/MethodLength
|
|
17
16
|
# rubocop:disable Metrics/ParameterLists
|
|
18
17
|
# rubocop:disable Metrics/PerceivedComplexity
|
|
19
18
|
def self.meta_get(key:, value: true, return_cas: false, ttl: nil, base64: false, quiet: false)
|
|
@@ -36,8 +35,6 @@ module Dalli
|
|
|
36
35
|
cmd << " M#{mode_to_token(mode)}"
|
|
37
36
|
cmd << ' q' if quiet
|
|
38
37
|
cmd << TERMINATOR
|
|
39
|
-
cmd << value
|
|
40
|
-
cmd + TERMINATOR
|
|
41
38
|
end
|
|
42
39
|
|
|
43
40
|
def self.meta_delete(key:, cas: nil, ttl: nil, base64: false, quiet: false)
|
|
@@ -62,7 +59,6 @@ module Dalli
|
|
|
62
59
|
cmd + TERMINATOR
|
|
63
60
|
end
|
|
64
61
|
# rubocop:enable Metrics/CyclomaticComplexity
|
|
65
|
-
# rubocop:enable Metrics/MethodLength
|
|
66
62
|
# rubocop:enable Metrics/ParameterLists
|
|
67
63
|
# rubocop:enable Metrics/PerceivedComplexity
|
|
68
64
|
|
|
@@ -81,13 +77,16 @@ module Dalli
|
|
|
81
77
|
cmd + TERMINATOR
|
|
82
78
|
end
|
|
83
79
|
|
|
80
|
+
ALLOWED_STATS_ARGS = [nil, '', 'items', 'slabs', 'settings', 'reset'].freeze
|
|
81
|
+
|
|
84
82
|
def self.stats(arg = nil)
|
|
83
|
+
raise ArgumentError, "Invalid stats argument: #{arg.inspect}" unless ALLOWED_STATS_ARGS.include?(arg)
|
|
84
|
+
|
|
85
85
|
cmd = +'stats'
|
|
86
|
-
cmd << " #{arg}" if arg
|
|
86
|
+
cmd << " #{arg}" if arg && !arg.empty?
|
|
87
87
|
cmd + TERMINATOR
|
|
88
88
|
end
|
|
89
89
|
|
|
90
|
-
# rubocop:disable Metrics/MethodLength
|
|
91
90
|
def self.mode_to_token(mode)
|
|
92
91
|
case mode
|
|
93
92
|
when :add
|
|
@@ -102,7 +101,6 @@ module Dalli
|
|
|
102
101
|
'S'
|
|
103
102
|
end
|
|
104
103
|
end
|
|
105
|
-
# rubocop:enable Metrics/MethodLength
|
|
106
104
|
|
|
107
105
|
def self.cas_string(cas)
|
|
108
106
|
cas = parse_to_64_bit_int(cas, nil)
|
|
@@ -21,6 +21,7 @@ module Dalli
|
|
|
21
21
|
STAT = 'STAT'
|
|
22
22
|
VA = 'VA'
|
|
23
23
|
VERSION = 'VERSION'
|
|
24
|
+
SERVER_ERROR = 'SERVER_ERROR'
|
|
24
25
|
|
|
25
26
|
def initialize(io_source, value_marshaller)
|
|
26
27
|
@io_source = io_source
|
|
@@ -32,7 +33,7 @@ module Dalli
|
|
|
32
33
|
return cache_nils ? ::Dalli::NOT_FOUND : nil if tokens.first == EN
|
|
33
34
|
return true unless tokens.first == VA
|
|
34
35
|
|
|
35
|
-
@value_marshaller.retrieve(
|
|
36
|
+
@value_marshaller.retrieve(read_data(tokens[1].to_i), bitflags_from_tokens(tokens))
|
|
36
37
|
end
|
|
37
38
|
|
|
38
39
|
def meta_get_with_value_and_cas
|
|
@@ -42,7 +43,7 @@ module Dalli
|
|
|
42
43
|
cas = cas_from_tokens(tokens)
|
|
43
44
|
return [nil, cas] unless tokens.first == VA
|
|
44
45
|
|
|
45
|
-
[@value_marshaller.retrieve(
|
|
46
|
+
[@value_marshaller.retrieve(read_data(tokens[1].to_i), bitflags_from_tokens(tokens)), cas]
|
|
46
47
|
end
|
|
47
48
|
|
|
48
49
|
def meta_get_without_value
|
|
@@ -167,9 +168,12 @@ module Dalli
|
|
|
167
168
|
|
|
168
169
|
def error_on_unexpected!(expected_codes)
|
|
169
170
|
tokens = next_line_to_tokens
|
|
170
|
-
raise Dalli::DalliError, "Response error: #{tokens.first}" unless expected_codes.include?(tokens.first)
|
|
171
171
|
|
|
172
|
-
tokens
|
|
172
|
+
return tokens if expected_codes.include?(tokens.first)
|
|
173
|
+
|
|
174
|
+
raise Dalli::ServerError, tokens.join(' ').to_s if tokens.first == SERVER_ERROR
|
|
175
|
+
|
|
176
|
+
raise Dalli::DalliError, "Response error: #{tokens.first}"
|
|
173
177
|
end
|
|
174
178
|
|
|
175
179
|
def bitflags_from_tokens(tokens)
|
|
@@ -205,6 +209,10 @@ module Dalli
|
|
|
205
209
|
line = read_line
|
|
206
210
|
line&.split || []
|
|
207
211
|
end
|
|
212
|
+
|
|
213
|
+
def read_data(data_size)
|
|
214
|
+
@io_source.read(data_size + TERMINATOR.bytesize)&.chomp!(TERMINATOR)
|
|
215
|
+
end
|
|
208
216
|
end
|
|
209
217
|
end
|
|
210
218
|
end
|
data/lib/dalli/protocol/meta.rb
CHANGED
|
@@ -85,6 +85,8 @@ module Dalli
|
|
|
85
85
|
bitflags: bitflags, cas: cas,
|
|
86
86
|
ttl: ttl, mode: mode, quiet: quiet?, base64: base64)
|
|
87
87
|
write(req)
|
|
88
|
+
write(value)
|
|
89
|
+
write(TERMINATOR)
|
|
88
90
|
end
|
|
89
91
|
# rubocop:enable Metrics/ParameterLists
|
|
90
92
|
|
|
@@ -105,6 +107,8 @@ module Dalli
|
|
|
105
107
|
req = RequestFormatter.meta_set(key: encoded_key, value: value, base64: base64,
|
|
106
108
|
cas: cas, ttl: ttl, mode: mode, quiet: quiet?)
|
|
107
109
|
write(req)
|
|
110
|
+
write(value)
|
|
111
|
+
write(TERMINATOR)
|
|
108
112
|
end
|
|
109
113
|
# rubocop:enable Metrics/ParameterLists
|
|
110
114
|
|
|
@@ -16,7 +16,7 @@ module Dalli
|
|
|
16
16
|
# can limit character set to LDH + '.'. Hex digit section
|
|
17
17
|
# is there to support IPv6 addresses, which need to be specified with
|
|
18
18
|
# a bounding []
|
|
19
|
-
SERVER_CONFIG_REGEXP = /\A(\[([\h:]+)\]|[^:]+)(?::(\d+))?(?::(\d+))?\z
|
|
19
|
+
SERVER_CONFIG_REGEXP = /\A(\[([\h:]+)\]|[^:]+)(?::(\d+))?(?::(\d+))?\z/
|
|
20
20
|
|
|
21
21
|
DEFAULT_PORT = 11_211
|
|
22
22
|
DEFAULT_WEIGHT = 1
|
|
@@ -31,7 +31,7 @@ module Dalli
|
|
|
31
31
|
return ttl_as_i if ttl_as_i > now # Already a timestamp
|
|
32
32
|
|
|
33
33
|
Dalli.logger.debug "Expiration interval (#{ttl_as_i}) too long for Memcached " \
|
|
34
|
-
'and too short to be a future timestamp,' \
|
|
34
|
+
'and too short to be a future timestamp, ' \
|
|
35
35
|
'converting to an expiration timestamp'
|
|
36
36
|
now + ttl_as_i
|
|
37
37
|
end
|
|
@@ -25,17 +25,8 @@ module Dalli
|
|
|
25
25
|
FLAG_COMPRESSED = 0x2
|
|
26
26
|
|
|
27
27
|
def initialize(client_options)
|
|
28
|
-
# Support the deprecated compression option, but don't allow it to override
|
|
29
|
-
# an explicit compress
|
|
30
|
-
# Remove this with 4.0
|
|
31
|
-
if client_options.key?(:compression) && !client_options.key?(:compress)
|
|
32
|
-
Dalli.logger.warn "DEPRECATED: Dalli's :compression option is now just 'compress: true'. " \
|
|
33
|
-
'Please update your configuration.'
|
|
34
|
-
client_options[:compress] = client_options.delete(:compression)
|
|
35
|
-
end
|
|
36
|
-
|
|
37
28
|
@compression_options =
|
|
38
|
-
DEFAULTS.merge(client_options.
|
|
29
|
+
DEFAULTS.merge(client_options.slice(*OPTIONS))
|
|
39
30
|
end
|
|
40
31
|
|
|
41
32
|
def store(value, req_options, bitflags)
|
|
@@ -47,7 +38,7 @@ module Dalli
|
|
|
47
38
|
end
|
|
48
39
|
|
|
49
40
|
def retrieve(value, bitflags)
|
|
50
|
-
compressed = (
|
|
41
|
+
compressed = bitflags.anybits?(FLAG_COMPRESSED)
|
|
51
42
|
compressed ? compressor.decompress(value) : value
|
|
52
43
|
|
|
53
44
|
# TODO: We likely want to move this rescue into the Dalli::Compressor / Dalli::GzipCompressor
|
|
@@ -27,7 +27,7 @@ module Dalli
|
|
|
27
27
|
@value_compressor = ValueCompressor.new(client_options)
|
|
28
28
|
|
|
29
29
|
@marshal_options =
|
|
30
|
-
DEFAULTS.merge(client_options.
|
|
30
|
+
DEFAULTS.merge(client_options.slice(*OPTIONS))
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def store(key, value, options = nil)
|
|
@@ -18,57 +18,53 @@ module Dalli
|
|
|
18
18
|
# https://www.hjp.at/zettel/m/memcached_flags.rxml
|
|
19
19
|
# Looks like most clients use bit 0 to indicate native language serialization
|
|
20
20
|
FLAG_SERIALIZED = 0x1
|
|
21
|
+
FLAG_UTF8 = 0x2
|
|
22
|
+
|
|
23
|
+
# Class variable to track whether the Marshal warning has been logged
|
|
24
|
+
@@marshal_warning_logged = false # rubocop:disable Style/ClassVars
|
|
21
25
|
|
|
22
26
|
attr_accessor :serialization_options
|
|
23
27
|
|
|
24
28
|
def initialize(protocol_options)
|
|
25
29
|
@serialization_options =
|
|
26
|
-
DEFAULTS.merge(protocol_options.
|
|
30
|
+
DEFAULTS.merge(protocol_options.slice(*OPTIONS))
|
|
31
|
+
warn_if_marshal_default(protocol_options) unless protocol_options[:silence_marshal_warning]
|
|
27
32
|
end
|
|
28
33
|
|
|
29
34
|
def store(value, req_options, bitflags)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
if req_options
|
|
36
|
+
return [value.to_s, bitflags] if req_options[:raw]
|
|
37
|
+
|
|
38
|
+
# If the value is a simple string, going through serialization is costly
|
|
39
|
+
# for no benefit other than preserving encoding.
|
|
40
|
+
# Assuming most strings are either UTF-8 or BINARY we can just store
|
|
41
|
+
# that information in the bitflags.
|
|
42
|
+
if req_options[:string_fastpath] && value.instance_of?(String)
|
|
43
|
+
case value.encoding
|
|
44
|
+
when Encoding::BINARY
|
|
45
|
+
return [value, bitflags]
|
|
46
|
+
when Encoding::UTF_8
|
|
47
|
+
return [value, bitflags | FLAG_UTF8]
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
[serialize_value(value), bitflags | FLAG_SERIALIZED]
|
|
34
53
|
end
|
|
35
54
|
|
|
36
|
-
# TODO: Some of these error messages need to be validated. It's not obvious
|
|
37
|
-
# that all of them are actually generated by the invoked code
|
|
38
|
-
# in current systems
|
|
39
|
-
# rubocop:disable Layout/LineLength
|
|
40
|
-
TYPE_ERR_REGEXP = %r{needs to have method `_load'|exception class/object expected|instance of IO needed|incompatible marshal file format}.freeze
|
|
41
|
-
ARGUMENT_ERR_REGEXP = /undefined class|marshal data too short/.freeze
|
|
42
|
-
NAME_ERR_STR = 'uninitialized constant'
|
|
43
|
-
# rubocop:enable Layout/LineLength
|
|
44
|
-
|
|
45
55
|
def retrieve(value, bitflags)
|
|
46
|
-
serialized = (
|
|
47
|
-
serialized
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
raise UnmarshalError, "Unable to unmarshal value: #{err.message}"
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def filter_argument_error(err)
|
|
63
|
-
raise err unless ARGUMENT_ERR_REGEXP.match?(err.message)
|
|
64
|
-
|
|
65
|
-
raise UnmarshalError, "Unable to unmarshal value: #{err.message}"
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def filter_name_error(err)
|
|
69
|
-
raise err unless err.message.include?(NAME_ERR_STR)
|
|
70
|
-
|
|
71
|
-
raise UnmarshalError, "Unable to unmarshal value: #{err.message}"
|
|
56
|
+
serialized = bitflags.anybits?(FLAG_SERIALIZED)
|
|
57
|
+
if serialized
|
|
58
|
+
begin
|
|
59
|
+
serializer.load(value)
|
|
60
|
+
rescue StandardError
|
|
61
|
+
raise UnmarshalError, 'Unable to unmarshal value'
|
|
62
|
+
end
|
|
63
|
+
elsif bitflags.anybits?(FLAG_UTF8)
|
|
64
|
+
value.force_encoding(Encoding::UTF_8)
|
|
65
|
+
else
|
|
66
|
+
value
|
|
67
|
+
end
|
|
72
68
|
end
|
|
73
69
|
|
|
74
70
|
def serializer
|
|
@@ -86,6 +82,19 @@ module Dalli
|
|
|
86
82
|
exc.set_backtrace e.backtrace
|
|
87
83
|
raise exc
|
|
88
84
|
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def warn_if_marshal_default(protocol_options)
|
|
89
|
+
return if protocol_options.key?(:serializer)
|
|
90
|
+
return if @@marshal_warning_logged
|
|
91
|
+
|
|
92
|
+
Dalli.logger.warn 'SECURITY WARNING: Dalli is using Marshal for serialization. ' \
|
|
93
|
+
'Marshal can execute arbitrary code during deserialization. ' \
|
|
94
|
+
'If your memcached server could be compromised, consider using ' \
|
|
95
|
+
'a safer serializer like JSON: Dalli::Client.new(servers, serializer: JSON)'
|
|
96
|
+
@@marshal_warning_logged = true # rubocop:disable Style/ClassVars
|
|
97
|
+
end
|
|
89
98
|
end
|
|
90
99
|
end
|
|
91
100
|
end
|
data/lib/dalli/socket.rb
CHANGED
|
@@ -52,7 +52,7 @@ module Dalli
|
|
|
52
52
|
|
|
53
53
|
FILTERED_OUT_OPTIONS = %i[username password].freeze
|
|
54
54
|
def logged_options
|
|
55
|
-
options.
|
|
55
|
+
options.except(*FILTERED_OUT_OPTIONS)
|
|
56
56
|
end
|
|
57
57
|
end
|
|
58
58
|
|
|
@@ -63,6 +63,7 @@ module Dalli
|
|
|
63
63
|
##
|
|
64
64
|
class SSLSocket < ::OpenSSL::SSL::SSLSocket
|
|
65
65
|
include Dalli::Socket::InstanceMethods
|
|
66
|
+
|
|
66
67
|
def options
|
|
67
68
|
io.options
|
|
68
69
|
end
|
|
@@ -85,6 +86,7 @@ module Dalli
|
|
|
85
86
|
##
|
|
86
87
|
class TCP < TCPSocket
|
|
87
88
|
include Dalli::Socket::InstanceMethods
|
|
89
|
+
|
|
88
90
|
# options - supports enhanced logging in the case of a timeout
|
|
89
91
|
attr_accessor :options
|
|
90
92
|
|
|
@@ -117,19 +119,33 @@ module Dalli
|
|
|
117
119
|
end
|
|
118
120
|
|
|
119
121
|
def self.init_socket_options(sock, options)
|
|
122
|
+
configure_tcp_options(sock, options)
|
|
123
|
+
configure_socket_buffers(sock, options)
|
|
124
|
+
configure_timeout(sock, options)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def self.configure_tcp_options(sock, options)
|
|
120
128
|
sock.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, true)
|
|
121
129
|
sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_KEEPALIVE, true) if options[:keepalive]
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def self.configure_socket_buffers(sock, options)
|
|
122
133
|
sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_RCVBUF, options[:rcvbuf]) if options[:rcvbuf]
|
|
123
134
|
sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_SNDBUF, options[:sndbuf]) if options[:sndbuf]
|
|
135
|
+
end
|
|
124
136
|
|
|
137
|
+
def self.configure_timeout(sock, options)
|
|
125
138
|
return unless options[:socket_timeout]
|
|
126
139
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
140
|
+
if sock.respond_to?(:timeout=)
|
|
141
|
+
sock.timeout = options[:socket_timeout]
|
|
142
|
+
else
|
|
143
|
+
seconds, fractional = options[:socket_timeout].divmod(1)
|
|
144
|
+
timeval = [seconds, fractional * 1_000_000].pack('l_2')
|
|
130
145
|
|
|
131
|
-
|
|
132
|
-
|
|
146
|
+
sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_RCVTIMEO, timeval)
|
|
147
|
+
sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_SNDTIMEO, timeval)
|
|
148
|
+
end
|
|
133
149
|
end
|
|
134
150
|
|
|
135
151
|
def self.wrapping_ssl_socket(tcp_socket, host, ssl_context)
|
|
@@ -168,9 +184,16 @@ module Dalli
|
|
|
168
184
|
Timeout.timeout(options[:socket_timeout]) do
|
|
169
185
|
sock = new(path)
|
|
170
186
|
sock.options = { path: path }.merge(options)
|
|
187
|
+
init_socket_options(sock, options)
|
|
171
188
|
sock
|
|
172
189
|
end
|
|
173
190
|
end
|
|
191
|
+
|
|
192
|
+
def self.init_socket_options(sock, options)
|
|
193
|
+
# https://man7.org/linux/man-pages/man7/unix.7.html
|
|
194
|
+
sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_SNDBUF, options[:sndbuf]) if options[:sndbuf]
|
|
195
|
+
sock.timeout = options[:socket_timeout] if options[:socket_timeout] && sock.respond_to?(:timeout=)
|
|
196
|
+
end
|
|
174
197
|
end
|
|
175
198
|
end
|
|
176
199
|
end
|
data/lib/dalli/version.rb
CHANGED
data/lib/dalli.rb
CHANGED
|
@@ -4,8 +4,6 @@
|
|
|
4
4
|
# Namespace for all Dalli code.
|
|
5
5
|
##
|
|
6
6
|
module Dalli
|
|
7
|
-
autoload :Server, 'dalli/server'
|
|
8
|
-
|
|
9
7
|
# generic error
|
|
10
8
|
class DalliError < RuntimeError; end
|
|
11
9
|
|
|
@@ -27,6 +25,9 @@ module Dalli
|
|
|
27
25
|
# operation is not permitted in a multi block
|
|
28
26
|
class NotPermittedMultiOpError < DalliError; end
|
|
29
27
|
|
|
28
|
+
# raised when Memcached response with a SERVER_ERROR
|
|
29
|
+
class ServerError < DalliError; end
|
|
30
|
+
|
|
30
31
|
# Implements the NullObject pattern to store an application-defined value for 'Key not found' responses.
|
|
31
32
|
class NilObject; end # rubocop:disable Lint/EmptyClass
|
|
32
33
|
NOT_FOUND = NilObject.new
|
data/lib/rack/session/dalli.rb
CHANGED
|
@@ -9,6 +9,10 @@ module Rack
|
|
|
9
9
|
module Session
|
|
10
10
|
# Rack::Session::Dalli provides memcached based session management.
|
|
11
11
|
class Dalli < Abstract::PersistedSecure
|
|
12
|
+
class MissingSessionError < StandardError; end
|
|
13
|
+
|
|
14
|
+
RACK_SESSION_PERSISTED = 'rack.session.persisted'
|
|
15
|
+
|
|
12
16
|
attr_reader :data
|
|
13
17
|
|
|
14
18
|
# Don't freeze this until we fix the specs/implementation
|
|
@@ -70,23 +74,37 @@ module Rack
|
|
|
70
74
|
@data = build_data_source(options)
|
|
71
75
|
end
|
|
72
76
|
|
|
73
|
-
def
|
|
77
|
+
def call(*_args)
|
|
78
|
+
super
|
|
79
|
+
rescue MissingSessionError
|
|
80
|
+
[401, {}, ['Wrong session ID']]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def find_session(req, sid)
|
|
74
84
|
with_dalli_client([nil, {}]) do |dc|
|
|
75
85
|
existing_session = existing_session_for_sid(dc, sid)
|
|
76
|
-
|
|
86
|
+
if existing_session.nil?
|
|
87
|
+
sid = create_sid_with_empty_session(dc)
|
|
88
|
+
existing_session = {}
|
|
89
|
+
end
|
|
77
90
|
|
|
78
|
-
|
|
91
|
+
update_session_persisted_data(req, { id: sid })
|
|
92
|
+
return [sid, existing_session]
|
|
79
93
|
end
|
|
80
94
|
end
|
|
81
95
|
|
|
82
|
-
def write_session(
|
|
96
|
+
def write_session(req, sid, session, options)
|
|
83
97
|
return false unless sid
|
|
84
98
|
|
|
85
99
|
key = memcached_key_from_sid(sid)
|
|
86
100
|
return false unless key
|
|
87
101
|
|
|
88
102
|
with_dalli_client(false) do |dc|
|
|
89
|
-
|
|
103
|
+
write_session_safely!(
|
|
104
|
+
dc, sid, session_persisted_data(req),
|
|
105
|
+
write_args: [memcached_key_from_sid(sid), session, ttl(options[:expire_after])]
|
|
106
|
+
)
|
|
107
|
+
|
|
90
108
|
sid
|
|
91
109
|
end
|
|
92
110
|
end
|
|
@@ -139,12 +157,21 @@ module Rack
|
|
|
139
157
|
::Dalli::Client.new(server_configurations, client_options)
|
|
140
158
|
else
|
|
141
159
|
ensure_connection_pool_added!
|
|
142
|
-
ConnectionPool.new(pool_options) do
|
|
160
|
+
ConnectionPool.new(**pool_options) do
|
|
143
161
|
::Dalli::Client.new(server_configurations, client_options.merge(threadsafe: false))
|
|
144
162
|
end
|
|
145
163
|
end
|
|
146
164
|
end
|
|
147
165
|
|
|
166
|
+
def write_session_safely!(dalli_client, sid, persisted_data, write_args:)
|
|
167
|
+
if persisted_data && persisted_data[:id] == sid # That means that we update the existing session
|
|
168
|
+
# Override the session only if it still exists in the store!
|
|
169
|
+
raise MissingSessionError unless dalli_client.replace(*write_args)
|
|
170
|
+
else
|
|
171
|
+
dalli_client.set(*write_args)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
148
175
|
def extract_dalli_options(options)
|
|
149
176
|
raise 'Rack::Session::Dalli no longer supports the :cache option.' if options[:cache]
|
|
150
177
|
|
|
@@ -175,8 +202,8 @@ module Rack
|
|
|
175
202
|
raise e
|
|
176
203
|
end
|
|
177
204
|
|
|
178
|
-
def with_dalli_client(result_on_error = nil, &
|
|
179
|
-
@data.with(&
|
|
205
|
+
def with_dalli_client(result_on_error = nil, &)
|
|
206
|
+
@data.with(&)
|
|
180
207
|
rescue ::Dalli::DalliError, Errno::ECONNREFUSED
|
|
181
208
|
raise if $ERROR_INFO.message.include?('undefined class')
|
|
182
209
|
|
|
@@ -190,6 +217,14 @@ module Rack
|
|
|
190
217
|
def ttl(expire_after)
|
|
191
218
|
expire_after.nil? ? 0 : expire_after + 1
|
|
192
219
|
end
|
|
220
|
+
|
|
221
|
+
def session_persisted_data(req)
|
|
222
|
+
req.get_header RACK_SESSION_PERSISTED
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def update_session_persisted_data(req, data)
|
|
226
|
+
req.set_header RACK_SESSION_PERSISTED, data
|
|
227
|
+
end
|
|
193
228
|
end
|
|
194
229
|
end
|
|
195
230
|
end
|
metadata
CHANGED
|
@@ -1,16 +1,29 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dalli
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 4.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Peter M. Goldstein
|
|
8
8
|
- Mike Perham
|
|
9
|
-
autorequire:
|
|
10
9
|
bindir: bin
|
|
11
10
|
cert_chain: []
|
|
12
|
-
date:
|
|
13
|
-
dependencies:
|
|
11
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: logger
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0'
|
|
14
27
|
description: High performance memcached client for Ruby
|
|
15
28
|
email:
|
|
16
29
|
- peter.m.goldstein@gmail.com
|
|
@@ -50,7 +63,6 @@ files:
|
|
|
50
63
|
- lib/dalli/protocol/value_marshaller.rb
|
|
51
64
|
- lib/dalli/protocol/value_serializer.rb
|
|
52
65
|
- lib/dalli/ring.rb
|
|
53
|
-
- lib/dalli/server.rb
|
|
54
66
|
- lib/dalli/servers_arg_normalizer.rb
|
|
55
67
|
- lib/dalli/socket.rb
|
|
56
68
|
- lib/dalli/version.rb
|
|
@@ -62,7 +74,6 @@ metadata:
|
|
|
62
74
|
bug_tracker_uri: https://github.com/petergoldstein/dalli/issues
|
|
63
75
|
changelog_uri: https://github.com/petergoldstein/dalli/blob/main/CHANGELOG.md
|
|
64
76
|
rubygems_mfa_required: 'true'
|
|
65
|
-
post_install_message:
|
|
66
77
|
rdoc_options: []
|
|
67
78
|
require_paths:
|
|
68
79
|
- lib
|
|
@@ -70,15 +81,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
70
81
|
requirements:
|
|
71
82
|
- - ">="
|
|
72
83
|
- !ruby/object:Gem::Version
|
|
73
|
-
version: '
|
|
84
|
+
version: '3.1'
|
|
74
85
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
75
86
|
requirements:
|
|
76
87
|
- - ">="
|
|
77
88
|
- !ruby/object:Gem::Version
|
|
78
89
|
version: '0'
|
|
79
90
|
requirements: []
|
|
80
|
-
rubygems_version:
|
|
81
|
-
signing_key:
|
|
91
|
+
rubygems_version: 4.0.3
|
|
82
92
|
specification_version: 4
|
|
83
93
|
summary: High performance memcached client for Ruby
|
|
84
94
|
test_files: []
|