dalli 3.2.14 → 3.2.15
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 +17 -0
- data/README.md +19 -0
- data/lib/dalli/client.rb +5 -0
- data/lib/dalli/protocol/base.rb +16 -0
- data/lib/dalli/protocol/binary.rb +4 -1
- data/lib/dalli/protocol/connection_manager.rb +26 -3
- data/lib/dalli/protocol/meta/response_processor.rb +11 -2
- data/lib/dalli/protocol/meta.rb +4 -1
- data/lib/dalli/protocol/value_compressor.rb +6 -4
- data/lib/dalli/protocol.rb +7 -0
- data/lib/dalli/socket.rb +30 -0
- data/lib/dalli/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1282508d83c234919dfb2c8a05c08d7069186663c83d4e3aaf040e6f503a2e9f
|
|
4
|
+
data.tar.gz: cb10108ea1d22c25630b5cfa36b6e038515e3f053e2354e3921718050138535b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 614a4eebdaa9cee1d9dc6afdfd45c96ff1ec7eda12f471a12c6c1eca7866af971d907a7c288aeb5a72734b3f4f72bfb685ba58337da2ac50146ed74a018c0d0a
|
|
7
|
+
data.tar.gz: 8f577fef557f593d9cb6327a54ec1b5d3df8e554a5935ba7a7faadf86259248a60f29204b3ad5a295024b676ebc481e925f81f330095a73e6649e11d182b1127
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@ Dalli Changelog
|
|
|
4
4
|
Unreleased
|
|
5
5
|
==========
|
|
6
6
|
|
|
7
|
+
3.2.15
|
|
8
|
+
==========
|
|
9
|
+
|
|
10
|
+
- Keep a connection shared by several threads or fibers in step during a `quiet` block (GHSA-5xvx-r8rg-5qm2)
|
|
11
|
+
- A `quiet` (or `multi`) block is quiet only for the thread or fiber that runs it. memcached still replies to a quiet request that fails (for example an `add` of a key that exists), and another thread or fiber using the same client while the block was open read that reply as its own, so its later requests could return another key's value. A non-quiet request now reads any pending quiet replies first
|
|
12
|
+
- Affects every supported line, with both protocols; with the binary protocol the connection stayed out of step after the block ended. Fixed in 5.2.4, 5.1.6, 5.0.12, 4.3.9 and 3.2.15
|
|
13
|
+
- With `protocol: :meta`, read values by their size instead of up to the first CRLF (GHSA-5xvx-r8rg-5qm2)
|
|
14
|
+
- `get`, `gat` and `cas` read a value as one line, so a value containing CR/LF was cut short and the rest was parsed as the next reply. A stored value containing a crafted reply was then returned for a later read of a different key, with flags that could cause it to be deserialized
|
|
15
|
+
- Affects 3.2.0 to 3.2.14 with `protocol: :meta`; fixed in 3.2.15. The binary protocol (3.2's default) and 4.0 and later aren't affected
|
|
16
|
+
- A per-request `raw: true` write no longer compresses the value (backport of #1213)
|
|
17
|
+
- Values at or above the compression threshold (4 KB by default) were compressed and flagged even with `raw: true`. Raw reads ignore flags, so reading them back with `raw: true` returned the compressed bytes
|
|
18
|
+
- Raw values are now stored exactly as given. Callers that pass `raw: true` without `compress: false` store large values uncompressed; Rails' `MemCacheStore` passes `compress: false` and isn't affected
|
|
19
|
+
- Handle failed TLS handshakes like other connection failures, and warn when TLS verification is off (backport of #1215)
|
|
20
|
+
- A failed or timed-out handshake left the TCP socket open until GC and raised `OpenSSL::SSL::SSLError` directly, so the server was never marked down. The socket is now closed, and the error goes through the usual retry and `socket_max_failures` handling, raising `Dalli::NetworkError` (or `Dalli::RingError` once no server is available)
|
|
21
|
+
- Certificate and hostname checks come only from the `ssl_context` you pass, and a bare `OpenSSL::SSL::SSLContext.new` checks neither. Dalli now logs a warning once per process when `verify_mode` doesn't include `VERIFY_PEER` or `verify_hostname` is false; see the new TLS section in the README
|
|
22
|
+
- A reply line longer than 8 KiB without a CRLF, or one cut short by the connection closing, is now a network error that closes the connection. Before, the line had no length limit (backport of #1216)
|
|
23
|
+
|
|
7
24
|
3.2.14
|
|
8
25
|
==========
|
|
9
26
|
|
data/README.md
CHANGED
|
@@ -41,6 +41,25 @@ Per-request `raw: true` returns the stored bytes without deserializing or decomp
|
|
|
41
41
|
|
|
42
42
|
memcached names an item made by auto-create for a base64-encoded key (one with whitespace, control or non-ASCII characters) by its raw key bytes in later replies (with `protocol: :meta`). Since 3.2.14, Dalli creates counters for such keys with an add rather than auto-create. Items created before then, or by other clients, can still be named that way until memcached releases a fix (1.6.45 and earlier behave this way). If keys passed to `incr` or `decr` with an initial value can contain untrusted input, set `verify_reply_keys: true` until then: multi-gets then ignore replies for keys they didn't request, at a cost of about 15% per multi-get.
|
|
43
43
|
|
|
44
|
+
## TLS
|
|
45
|
+
|
|
46
|
+
Pass an `OpenSSL::SSL::SSLContext` as `ssl_context:` to connect over TLS. Certificate and hostname verification come only from that context: Dalli adds no checks of its own. A bare `OpenSSL::SSL::SSLContext.new` verifies nothing, so it accepts any certificate from any server. Configure the context to verify both:
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
ssl_context = OpenSSL::SSL::SSLContext.new
|
|
50
|
+
ssl_context.set_params(
|
|
51
|
+
verify_mode: OpenSSL::SSL::VERIFY_PEER,
|
|
52
|
+
verify_hostname: true,
|
|
53
|
+
ca_file: '/path/to/ca.crt' # omit to use the system's trusted certificates
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
Dalli::Client.new('memcached.example.com:11211', ssl_context: ssl_context)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Dalli logs a warning once per process when the context's `verify_mode` is `VERIFY_NONE`, or when it is `VERIFY_PEER` but `verify_hostname` is false.
|
|
60
|
+
|
|
61
|
+
A failed TLS handshake counts as a failed connection attempt, like a refused connection: once `socket_max_failures` attempts have failed, the server is marked down and requests fail over to the other servers.
|
|
62
|
+
|
|
44
63
|
## Development
|
|
45
64
|
|
|
46
65
|
After checking out the repo, run `bin/setup` to install dependencies. You can run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/lib/dalli/client.rb
CHANGED
|
@@ -192,6 +192,11 @@ module Dalli
|
|
|
192
192
|
# Any error replies will be discarded at the end of the block, and
|
|
193
193
|
# Dalli client methods invoked inside the block will not
|
|
194
194
|
# have return values
|
|
195
|
+
#
|
|
196
|
+
# The block is quiet only for the thread or fiber that runs it. Another
|
|
197
|
+
# thread or fiber sharing this client can make requests meanwhile; a
|
|
198
|
+
# non-quiet one first reads the block's pending replies with a noop and
|
|
199
|
+
# discards them, so it reads only its own.
|
|
195
200
|
def quiet
|
|
196
201
|
old = Thread.current[::Dalli::QUIET]
|
|
197
202
|
Thread.current[::Dalli::QUIET] = true
|
data/lib/dalli/protocol/base.rb
CHANGED
|
@@ -41,7 +41,11 @@ module Dalli
|
|
|
41
41
|
|
|
42
42
|
begin
|
|
43
43
|
@connection_manager.start_request!
|
|
44
|
+
drain_pending_quiet_responses(opkey)
|
|
44
45
|
response = send(opkey, *args)
|
|
46
|
+
# Quiet requests can leave replies on the socket; remember that so a
|
|
47
|
+
# later non-quiet request knows to read them first
|
|
48
|
+
@connection_manager.quiet_responses_pending = true if quiet? && opkey != :noop
|
|
45
49
|
|
|
46
50
|
# pipelined_get emit query but doesn't read the response(s)
|
|
47
51
|
@connection_manager.finish_request! unless opkey == :pipelined_get
|
|
@@ -165,6 +169,18 @@ module Dalli
|
|
|
165
169
|
private
|
|
166
170
|
|
|
167
171
|
ALLOWED_QUIET_OPS = %i[add replace set delete incr decr append prepend flush noop].freeze
|
|
172
|
+
|
|
173
|
+
# Reads the replies still pending from quiet requests before any
|
|
174
|
+
# non-quiet request, which reads a reply of its own. A quiet block reads
|
|
175
|
+
# them at its end, but the connection is shared: another thread or fiber
|
|
176
|
+
# (quiet? is fiber-local) can make a request while the block is still
|
|
177
|
+
# open, and would otherwise read the block's replies as its own.
|
|
178
|
+
def drain_pending_quiet_responses(opkey)
|
|
179
|
+
return unless @connection_manager.quiet_responses_pending? && opkey != :noop && !quiet?
|
|
180
|
+
|
|
181
|
+
noop
|
|
182
|
+
end
|
|
183
|
+
|
|
168
184
|
def verify_allowed_quiet!(opkey)
|
|
169
185
|
return if ALLOWED_QUIET_OPS.include?(opkey)
|
|
170
186
|
|
|
@@ -139,7 +139,10 @@ module Dalli
|
|
|
139
139
|
# We need to read all the responses at once.
|
|
140
140
|
def noop
|
|
141
141
|
write_noop
|
|
142
|
-
response_processor.consume_all_responses_until_noop
|
|
142
|
+
result = response_processor.consume_all_responses_until_noop
|
|
143
|
+
# Everything up to and including the noop reply has been read
|
|
144
|
+
@connection_manager.quiet_responses_pending = false
|
|
145
|
+
result
|
|
143
146
|
end
|
|
144
147
|
|
|
145
148
|
def stats(info = '')
|
|
@@ -28,6 +28,7 @@ module Dalli
|
|
|
28
28
|
|
|
29
29
|
attr_accessor :hostname, :port, :socket_type, :options
|
|
30
30
|
attr_reader :sock
|
|
31
|
+
attr_writer :quiet_responses_pending
|
|
31
32
|
|
|
32
33
|
def initialize(hostname, port, socket_type, client_options)
|
|
33
34
|
@hostname = hostname
|
|
@@ -35,6 +36,7 @@ module Dalli
|
|
|
35
36
|
@socket_type = socket_type
|
|
36
37
|
@options = DEFAULTS.merge(client_options)
|
|
37
38
|
@request_in_progress = false
|
|
39
|
+
@quiet_responses_pending = false
|
|
38
40
|
@sock = nil
|
|
39
41
|
@pid = nil
|
|
40
42
|
|
|
@@ -56,8 +58,9 @@ module Dalli
|
|
|
56
58
|
@sock = memcached_socket
|
|
57
59
|
@pid = Process.pid
|
|
58
60
|
@request_in_progress = false
|
|
59
|
-
|
|
60
|
-
|
|
61
|
+
@quiet_responses_pending = false
|
|
62
|
+
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, EOFError, SocketError => e
|
|
63
|
+
# SocketError = DNS resolution failure; SSL_ERRORS = failed TLS handshake
|
|
61
64
|
error_on_request!(e)
|
|
62
65
|
end
|
|
63
66
|
|
|
@@ -122,6 +125,7 @@ module Dalli
|
|
|
122
125
|
end
|
|
123
126
|
@sock = nil
|
|
124
127
|
@pid = nil
|
|
128
|
+
@quiet_responses_pending = false
|
|
125
129
|
abort_request!
|
|
126
130
|
end
|
|
127
131
|
|
|
@@ -136,6 +140,13 @@ module Dalli
|
|
|
136
140
|
!@sock.nil?
|
|
137
141
|
end
|
|
138
142
|
|
|
143
|
+
# True when a quiet request was written on this connection and its
|
|
144
|
+
# replies (memcached only suppresses some of them) haven't been
|
|
145
|
+
# consumed by a noop yet.
|
|
146
|
+
def quiet_responses_pending?
|
|
147
|
+
@quiet_responses_pending
|
|
148
|
+
end
|
|
149
|
+
|
|
139
150
|
def request_in_progress?
|
|
140
151
|
@request_in_progress
|
|
141
152
|
end
|
|
@@ -161,9 +172,21 @@ module Dalli
|
|
|
161
172
|
@request_in_progress = false
|
|
162
173
|
end
|
|
163
174
|
|
|
175
|
+
# The longest reply line Dalli reads, in bytes, CRLF included. A meta
|
|
176
|
+
# reply header is at most about 500 bytes: "VA <size>" plus its flags,
|
|
177
|
+
# the largest being a 250-byte key base64-encoded to 336 bytes, a
|
|
178
|
+
# 20-digit CAS, a 32-byte opaque and a few numeric fields. The longest
|
|
179
|
+
# stats line (stats settings) carries a file path such as ext_path, up
|
|
180
|
+
# to PATH_MAX (4096). 8 KiB leaves room for both, while a server that
|
|
181
|
+
# never sends CRLF can't grow the read buffer without bound.
|
|
182
|
+
MAX_LINE_BYTES = 8 * 1024
|
|
183
|
+
|
|
164
184
|
def read_line
|
|
165
|
-
data = @sock.gets("\r\n")
|
|
185
|
+
data = @sock.gets("\r\n", MAX_LINE_BYTES)
|
|
166
186
|
error_on_request!('EOF in read_line') if data.nil?
|
|
187
|
+
# Without its CRLF the line either ran past MAX_LINE_BYTES or was cut
|
|
188
|
+
# short by EOF; either way the connection is out of step
|
|
189
|
+
error_on_request!("Reply line without CRLF in the first #{MAX_LINE_BYTES} bytes") unless data.end_with?("\r\n")
|
|
167
190
|
data
|
|
168
191
|
rescue SystemCallError, *TIMEOUT_ERRORS, EOFError => e
|
|
169
192
|
error_on_request!(e)
|
|
@@ -34,7 +34,7 @@ module Dalli
|
|
|
34
34
|
return cache_nils ? ::Dalli::NOT_FOUND : nil if tokens.first == EN
|
|
35
35
|
return true unless tokens.first == VA
|
|
36
36
|
|
|
37
|
-
value =
|
|
37
|
+
value = read_data(tokens[1].to_i)
|
|
38
38
|
raw ? value : @value_marshaller.retrieve(value, bitflags_from_tokens(tokens))
|
|
39
39
|
end
|
|
40
40
|
|
|
@@ -46,7 +46,7 @@ module Dalli
|
|
|
46
46
|
cas = cas_from_tokens(tokens)
|
|
47
47
|
return [nil, cas] unless tokens.first == VA
|
|
48
48
|
|
|
49
|
-
value =
|
|
49
|
+
value = read_data(tokens[1].to_i)
|
|
50
50
|
[raw ? value : @value_marshaller.retrieve(value, bitflags_from_tokens(tokens)), cas]
|
|
51
51
|
end
|
|
52
52
|
|
|
@@ -242,6 +242,15 @@ module Dalli
|
|
|
242
242
|
line = read_line
|
|
243
243
|
line&.split || []
|
|
244
244
|
end
|
|
245
|
+
|
|
246
|
+
# Values are read by the size in the VA line, not up to the next CRLF:
|
|
247
|
+
# a value can contain CRLF and can be longer than MAX_LINE_BYTES
|
|
248
|
+
def read_data(data_size)
|
|
249
|
+
# Checked before the terminator is added: -1 or -2 would otherwise
|
|
250
|
+
# read 1 or 0 bytes and leave the connection out of step
|
|
251
|
+
check_value_size!(data_size)
|
|
252
|
+
@io_source.read(data_size + TERMINATOR.bytesize)&.chomp!(TERMINATOR)
|
|
253
|
+
end
|
|
245
254
|
end
|
|
246
255
|
end
|
|
247
256
|
end
|
data/lib/dalli/protocol/meta.rb
CHANGED
|
@@ -176,7 +176,10 @@ module Dalli
|
|
|
176
176
|
# We need to read all the responses at once.
|
|
177
177
|
def noop
|
|
178
178
|
write_noop
|
|
179
|
-
response_processor.consume_all_responses_until_mn
|
|
179
|
+
result = response_processor.consume_all_responses_until_mn
|
|
180
|
+
# Everything up to and including the MN has been read
|
|
181
|
+
@connection_manager.quiet_responses_pending = false
|
|
182
|
+
result
|
|
180
183
|
end
|
|
181
184
|
|
|
182
185
|
def stats(info = nil)
|
|
@@ -97,11 +97,13 @@ module Dalli
|
|
|
97
97
|
end
|
|
98
98
|
|
|
99
99
|
# Checks whether we should apply compression when serializing a value
|
|
100
|
-
# based on the specified options. Returns false
|
|
101
|
-
#
|
|
102
|
-
#
|
|
103
|
-
#
|
|
100
|
+
# based on the specified options. Returns false for a raw request
|
|
101
|
+
# (raw reads ignore bitflags, so the value must be stored as given) or
|
|
102
|
+
# unless the value is greater than the minimum compression size.
|
|
103
|
+
# Otherwise returns based on a method-level option if specified,
|
|
104
|
+
# falling back to the server default.
|
|
104
105
|
def compress_value?(value, req_options)
|
|
106
|
+
return false if req_options&.dig(:raw)
|
|
105
107
|
return false unless value.bytesize >= compression_min_size
|
|
106
108
|
return compress_by_default? unless req_options && !req_options[:compress].nil?
|
|
107
109
|
|
data/lib/dalli/protocol.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'openssl'
|
|
3
4
|
require 'timeout'
|
|
4
5
|
|
|
5
6
|
module Dalli
|
|
@@ -15,5 +16,11 @@ module Dalli
|
|
|
15
16
|
else
|
|
16
17
|
[Timeout::Error]
|
|
17
18
|
end
|
|
19
|
+
|
|
20
|
+
# SSL errors during the TLS handshake count as a failed request: they
|
|
21
|
+
# trigger reconnection and, after socket_max_failures, mark the server
|
|
22
|
+
# down. openssl is required above because this file loads before
|
|
23
|
+
# dalli/socket, so OpenSSL may not be defined yet.
|
|
24
|
+
SSL_ERRORS = [OpenSSL::SSL::SSLError].freeze
|
|
18
25
|
end
|
|
19
26
|
end
|
data/lib/dalli/socket.rb
CHANGED
|
@@ -132,12 +132,42 @@ module Dalli
|
|
|
132
132
|
sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_SNDTIMEO, timeval)
|
|
133
133
|
end
|
|
134
134
|
|
|
135
|
+
# Closes the TCP socket if the handshake fails or times out, so a failed
|
|
136
|
+
# attempt doesn't hold a file descriptor until GC.
|
|
135
137
|
def self.wrapping_ssl_socket(tcp_socket, host, ssl_context)
|
|
138
|
+
warn_if_unverified(ssl_context)
|
|
139
|
+
connected = false
|
|
136
140
|
ssl_socket = Dalli::Socket::SSLSocket.new(tcp_socket, ssl_context)
|
|
137
141
|
ssl_socket.hostname = host
|
|
138
142
|
ssl_socket.sync_close = true
|
|
139
143
|
ssl_socket.connect
|
|
144
|
+
connected = true
|
|
140
145
|
ssl_socket
|
|
146
|
+
ensure
|
|
147
|
+
tcp_socket.close unless connected
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Verification comes entirely from the caller's ssl_context, and a bare
|
|
151
|
+
# SSLContext.new verifies nothing, so warn (once per process) when the
|
|
152
|
+
# certificate or the hostname won't be checked.
|
|
153
|
+
def self.warn_if_unverified(ssl_context)
|
|
154
|
+
return if @ssl_verification_warning_logged
|
|
155
|
+
|
|
156
|
+
problem = unverified_problem(ssl_context)
|
|
157
|
+
return unless problem
|
|
158
|
+
|
|
159
|
+
Dalli.logger.warn "SECURITY WARNING: Dalli's ssl_context #{problem}. " \
|
|
160
|
+
'Use ssl_context.set_params(verify_mode: OpenSSL::SSL::VERIFY_PEER, ' \
|
|
161
|
+
'verify_hostname: true) to verify the server.'
|
|
162
|
+
@ssl_verification_warning_logged = true
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def self.unverified_problem(ssl_context)
|
|
166
|
+
if !ssl_context.verify_mode.to_i.anybits?(::OpenSSL::SSL::VERIFY_PEER)
|
|
167
|
+
'verify_mode is VERIFY_NONE, so the server certificate is not checked'
|
|
168
|
+
elsif !ssl_context.verify_hostname
|
|
169
|
+
'verify_hostname is false, so the certificate is not checked against the hostname'
|
|
170
|
+
end
|
|
141
171
|
end
|
|
142
172
|
end
|
|
143
173
|
|
data/lib/dalli/version.rb
CHANGED