couchbase 3.8.1 → 3.8.2

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.
@@ -23,12 +23,128 @@
23
23
  #include <asio/error.hpp>
24
24
  #include <asio/ssl.hpp>
25
25
 
26
+ #include <cerrno>
27
+
28
+ #if defined(_WIN32)
29
+ #include <process.h>
30
+ #else
31
+ #include <unistd.h>
32
+ #endif
33
+
26
34
  namespace couchbase::core::io
27
35
  {
36
+ namespace
37
+ {
38
+ auto
39
+ current_process_id() -> long long
40
+ {
41
+ #if defined(_WIN32)
42
+ return static_cast<long long>(::_getpid());
43
+ #else
44
+ return static_cast<long long>(::getpid());
45
+ #endif
46
+ }
47
+
48
+ /**
49
+ * Release a socket this process inherited across fork(2), without disturbing the
50
+ * connection the parent is still using.
51
+ *
52
+ * Two things differ from the ordinary close path, and both matter:
53
+ *
54
+ * * No shutdown(2). It acts on the open file description, which fork(2) shares,
55
+ * so it would send FIN and break the parent's connection. close(2) drops only
56
+ * our own descriptor; the peer sees nothing.
57
+ * * release() before closing, rather than asio's close(). asio's close() skips
58
+ * EPOLL_CTL_DEL on the assumption that closing a descriptor removes its epoll
59
+ * registration. Per epoll(7) that happens only once *every* descriptor
60
+ * referring to the description is closed -- and the parent still holds one --
61
+ * so the registration would outlive the descriptor_state asio recycles, which
62
+ * is precisely the stale-pointer shape the reactor faults on. release() emits
63
+ * the EPOLL_CTL_DEL explicitly and hands back the raw descriptor to close.
64
+ *
65
+ * release() also completes any outstanding asynchronous operations on this socket
66
+ * with operation_aborted, which is what we want: those reads belong to the parent.
67
+ */
68
+ // Takes the basic_socket base rather than tcp::socket so the same code serves both
69
+ // the plain socket and a TLS stream's lowest_layer(), whose type is that base.
70
+ void
71
+ detach_inherited_socket(asio::basic_socket<asio::ip::tcp>& socket, asio::error_code& ec)
72
+ {
73
+ if (!socket.is_open()) {
74
+ ec = asio::error::bad_descriptor;
75
+ return;
76
+ }
77
+
78
+ const auto handle = socket.release(ec);
79
+ if (ec) {
80
+ // Report it. Do not reach for asio's close() here: that is the very call
81
+ // whose epoll behaviour makes it unsafe on an inherited descriptor, and the
82
+ // descriptor goes away when this process exits anyway.
83
+ return;
84
+ }
85
+
86
+ #if defined(_WIN32)
87
+ // Unreachable: without fork(2) no socket is ever inherited, so the owner pid
88
+ // always matches and close() never takes this path.
89
+ (void)handle;
90
+ ec = asio::error::operation_not_supported;
91
+ #else
92
+ if (::close(handle) != 0) {
93
+ ec.assign(errno, asio::error::get_system_category());
94
+ }
95
+ #endif
96
+ }
97
+ } // namespace
98
+
99
+ auto
100
+ configure_tls_handshake(asio::ssl::stream<asio::ip::tcp::socket>& stream,
101
+ const std::string& hostname) -> std::error_code
102
+ {
103
+ if (hostname.empty()) {
104
+ // Fail closed: a TLS client connection must have a hostname to verify the
105
+ // server's identity against. Returning success here would silently fall back
106
+ // to CA-only validation -- the CWE-297 problem this function exists to prevent.
107
+ return asio::error::make_error_code(asio::error::invalid_argument);
108
+ }
109
+
110
+ // Send SNI so that name-based / multi-tenant TLS endpoints present the correct
111
+ // certificate for this hostname.
112
+ {
113
+ // SSL_set_tlsext_host_name() is a macro that expands to an internal C-style
114
+ // cast to void*, which trips -Wold-style-cast; suppress it locally.
115
+ #if defined(__GNUC__) || defined(__clang__)
116
+ #pragma GCC diagnostic push
117
+ #pragma GCC diagnostic ignored "-Wold-style-cast"
118
+ #endif
119
+ ERR_clear_error();
120
+ const auto sni_result = SSL_set_tlsext_host_name(stream.native_handle(), hostname.c_str());
121
+ #if defined(__GNUC__) || defined(__clang__)
122
+ #pragma GCC diagnostic pop
123
+ #endif
124
+ if (sni_result != 1) {
125
+ // Prefer the underlying SSL reason (if any) so failures are diagnosable,
126
+ // and fall back to a generic error when the SSL error queue is empty.
127
+ if (const auto reason = ERR_get_error(); reason != 0) {
128
+ return { static_cast<int>(reason), asio::error::get_ssl_category() };
129
+ }
130
+ return asio::error::make_error_code(asio::error::invalid_argument);
131
+ }
132
+ }
133
+
134
+ // Verify that the presented certificate actually identifies `hostname`
135
+ // (SAN/CN). asio::ssl::verify_peer on its own only validates the certificate
136
+ // chain, not the name. In verify_none mode OpenSSL ignores the callback
137
+ // result, so this has no effect there.
138
+ std::error_code ec{};
139
+ stream.set_verify_callback(asio::ssl::host_name_verification(hostname), ec);
140
+ return ec;
141
+ }
142
+
28
143
  stream_impl::stream_impl(asio::io_context& ctx, bool is_tls)
29
144
  : strand_(asio::make_strand(ctx))
30
145
  , tls_(is_tls)
31
146
  , id_(uuid::to_string(uuid::random()))
147
+ , owner_pid_(current_process_id())
32
148
  {
33
149
  }
34
150
 
@@ -79,12 +195,20 @@ plain_stream_impl::close(utils::movable_function<void(std::error_code)>&& handle
79
195
  if (!stream_) {
80
196
  return handler(asio::error::bad_descriptor);
81
197
  }
82
- return asio::post(strand_, [stream = std::move(stream_), handler = std::move(handler)]() {
83
- asio::error_code ec{};
84
- stream->shutdown(asio::socket_base::shutdown_both, ec);
85
- stream->close(ec);
86
- handler(ec);
87
- });
198
+ return asio::post(
199
+ strand_, [stream = std::move(stream_), owner_pid = owner_pid_, handler = std::move(handler)]() {
200
+ asio::error_code ec{};
201
+ // Decide here rather than when this was posted: io_.stop() at fork_prepare
202
+ // does not drain the queue, so a close queued before the fork runs in the
203
+ // child once the io_context is restarted, and there the answer differs.
204
+ if (owner_pid != current_process_id()) {
205
+ detach_inherited_socket(*stream, ec);
206
+ } else {
207
+ stream->shutdown(asio::socket_base::shutdown_both, ec);
208
+ stream->close(ec);
209
+ }
210
+ handler(ec);
211
+ });
88
212
  }
89
213
 
90
214
  void
@@ -178,12 +302,19 @@ tls_stream_impl::close(utils::movable_function<void(std::error_code)>&& handler)
178
302
  if (!stream_) {
179
303
  return handler(asio::error::bad_descriptor);
180
304
  }
181
- return asio::post(strand_, [stream = std::move(stream_), handler = std::move(handler)]() {
182
- asio::error_code ec{};
183
- stream->lowest_layer().shutdown(asio::socket_base::shutdown_both, ec);
184
- stream->lowest_layer().close(ec);
185
- handler(ec);
186
- });
305
+ return asio::post(
306
+ strand_, [stream = std::move(stream_), owner_pid = owner_pid_, handler = std::move(handler)]() {
307
+ asio::error_code ec{};
308
+ // See plain_stream_impl::close(): the inheritance test belongs here, not at
309
+ // post time.
310
+ if (owner_pid != current_process_id()) {
311
+ detach_inherited_socket(stream->lowest_layer(), ec);
312
+ } else {
313
+ stream->lowest_layer().shutdown(asio::socket_base::shutdown_both, ec);
314
+ stream->lowest_layer().close(ec);
315
+ }
316
+ handler(ec);
317
+ });
187
318
  }
188
319
 
189
320
  void
@@ -65,6 +65,12 @@ protected:
65
65
  asio::strand<asio::io_context::executor_type> strand_;
66
66
  bool tls_;
67
67
  std::string id_{};
68
+ // Identifies the process that opened this socket. close() compares it against
69
+ // the process running at that moment to tell a socket inherited across fork(2)
70
+ // apart from one this process opened itself, because an inherited socket must not
71
+ // be shut down: shutdown(2) acts on the open file description, which fork(2)
72
+ // shares, so it would tear down a connection the parent is still using.
73
+ long long owner_pid_;
68
74
 
69
75
  public:
70
76
  stream_impl(asio::io_context& ctx, bool is_tls);
@@ -30,104 +30,101 @@ namespace couchbase::core::operations::management
30
30
  {
31
31
 
32
32
  auto
33
- bucket_create_request::encode_to(encoded_request_type& encoded,
34
- http_context& /* context */) const -> std::error_code
33
+ bucket_create_request::encode_to(encoded_request_type& encoded, http_context& /* context */) const
34
+ -> std::error_code
35
35
  {
36
36
  encoded.method = "POST";
37
37
  encoded.path = "/pools/default/buckets";
38
38
 
39
39
  encoded.headers["content-type"] = "application/x-www-form-urlencoded";
40
- encoded.body.append(fmt::format("name={}", utils::string_codec::form_encode(bucket.name)));
40
+
41
+ std::map<std::string, std::string> values{};
42
+ values["name"] = bucket.name;
41
43
  switch (bucket.bucket_type) {
42
44
  case couchbase::core::management::cluster::bucket_type::couchbase:
43
- encoded.body.append("&bucketType=couchbase");
45
+ values["bucketType"] = "couchbase";
44
46
  break;
45
47
  case couchbase::core::management::cluster::bucket_type::memcached:
46
- encoded.body.append("&bucketType=memcached");
48
+ values["bucketType"] = "memcached";
47
49
  break;
48
50
  case couchbase::core::management::cluster::bucket_type::ephemeral:
49
- encoded.body.append("&bucketType=ephemeral");
51
+ values["bucketType"] = "ephemeral";
50
52
  break;
51
53
  case couchbase::core::management::cluster::bucket_type::unknown:
52
54
  break;
53
55
  }
54
56
  if (bucket.ram_quota_mb == 0) {
55
- encoded.body.append(fmt::format(
56
- "&ramQuotaMB={}", 100)); // If not explicitly set, set to prior default value of 100
57
+ values["ramQuotaMB"] = "100"; // If not explicitly set, set to prior default value of 100
57
58
  } else {
58
- encoded.body.append(fmt::format("&ramQuotaMB={}", bucket.ram_quota_mb));
59
+ values["ramQuotaMB"] = std::to_string(bucket.ram_quota_mb);
59
60
  }
60
61
 
61
62
  if (bucket.bucket_type != couchbase::core::management::cluster::bucket_type::memcached &&
62
63
  bucket.num_replicas.has_value()) {
63
- encoded.body.append(fmt::format("&replicaNumber={}", bucket.num_replicas.value()));
64
+ values["replicaNumber"] = std::to_string(bucket.num_replicas.value());
64
65
  }
65
66
  if (bucket.max_expiry.has_value()) {
66
- encoded.body.append(fmt::format("&maxTTL={}", bucket.max_expiry.value()));
67
+ values["maxTTL"] = std::to_string(bucket.max_expiry.value());
67
68
  }
68
69
  if (bucket.bucket_type != couchbase::core::management::cluster::bucket_type::ephemeral &&
69
70
  bucket.replica_indexes.has_value()) {
70
- encoded.body.append(
71
- fmt::format("&replicaIndex={}", bucket.replica_indexes.value() ? "1" : "0"));
71
+ values["replicaIndex"] = bucket.replica_indexes.value() ? "1" : "0";
72
72
  }
73
73
  if (bucket.history_retention_collection_default.has_value()) {
74
- encoded.body.append(
75
- fmt::format("&historyRetentionCollectionDefault={}",
76
- bucket.history_retention_collection_default.value() ? "true" : "false"));
74
+ values["historyRetentionCollectionDefault"] =
75
+ bucket.history_retention_collection_default.value() ? "true" : "false";
77
76
  }
78
77
  if (bucket.history_retention_bytes.has_value()) {
79
- encoded.body.append(
80
- fmt::format("&historyRetentionBytes={}", bucket.history_retention_bytes.value()));
78
+ values["historyRetentionBytes"] = std::to_string(bucket.history_retention_bytes.value());
81
79
  }
82
80
  if (bucket.history_retention_duration.has_value()) {
83
- encoded.body.append(
84
- fmt::format("&historyRetentionSeconds={}", bucket.history_retention_duration.value()));
81
+ values["historyRetentionSeconds"] = std::to_string(bucket.history_retention_duration.value());
85
82
  }
86
83
  if (bucket.flush_enabled.has_value()) {
87
- encoded.body.append(fmt::format("&flushEnabled={}", bucket.flush_enabled.value() ? "1" : "0"));
84
+ values["flushEnabled"] = bucket.flush_enabled.value() ? "1" : "0";
88
85
  }
89
86
  if (bucket.num_vbuckets.has_value()) {
90
- encoded.body.append(fmt::format("&numVBuckets={}", bucket.num_vbuckets.value()));
87
+ values["numVBuckets"] = std::to_string(bucket.num_vbuckets.value());
91
88
  }
92
89
 
93
90
  switch (bucket.eviction_policy) {
94
91
  case couchbase::core::management::cluster::bucket_eviction_policy::full:
95
- encoded.body.append("&evictionPolicy=fullEviction");
92
+ values["evictionPolicy"] = "fullEviction";
96
93
  break;
97
94
  case couchbase::core::management::cluster::bucket_eviction_policy::value_only:
98
- encoded.body.append("&evictionPolicy=valueOnly");
95
+ values["evictionPolicy"] = "valueOnly";
99
96
  break;
100
97
  case couchbase::core::management::cluster::bucket_eviction_policy::no_eviction:
101
- encoded.body.append("&evictionPolicy=noEviction");
98
+ values["evictionPolicy"] = "noEviction";
102
99
  break;
103
100
  case couchbase::core::management::cluster::bucket_eviction_policy::not_recently_used:
104
- encoded.body.append("&evictionPolicy=nruEviction");
101
+ values["evictionPolicy"] = "nruEviction";
105
102
  break;
106
103
  case couchbase::core::management::cluster::bucket_eviction_policy::unknown:
107
104
  break;
108
105
  }
109
106
  switch (bucket.compression_mode) {
110
107
  case couchbase::core::management::cluster::bucket_compression::off:
111
- encoded.body.append("&compressionMode=off");
108
+ values["compressionMode"] = "off";
112
109
  break;
113
110
  case couchbase::core::management::cluster::bucket_compression::active:
114
- encoded.body.append("&compressionMode=active");
111
+ values["compressionMode"] = "active";
115
112
  break;
116
113
  case couchbase::core::management::cluster::bucket_compression::passive:
117
- encoded.body.append("&compressionMode=passive");
114
+ values["compressionMode"] = "passive";
118
115
  break;
119
116
  case couchbase::core::management::cluster::bucket_compression::unknown:
120
117
  break;
121
118
  }
122
119
  switch (bucket.conflict_resolution_type) {
123
120
  case couchbase::core::management::cluster::bucket_conflict_resolution::timestamp:
124
- encoded.body.append("&conflictResolutionType=lww");
121
+ values["conflictResolutionType"] = "lww";
125
122
  break;
126
123
  case couchbase::core::management::cluster::bucket_conflict_resolution::sequence_number:
127
- encoded.body.append("&conflictResolutionType=seqno");
124
+ values["conflictResolutionType"] = "seqno";
128
125
  break;
129
126
  case couchbase::core::management::cluster::bucket_conflict_resolution::custom:
130
- encoded.body.append("&conflictResolutionType=custom");
127
+ values["conflictResolutionType"] = "custom";
131
128
  break;
132
129
  case couchbase::core::management::cluster::bucket_conflict_resolution::unknown:
133
130
  break;
@@ -135,16 +132,16 @@ bucket_create_request::encode_to(encoded_request_type& encoded,
135
132
  if (bucket.minimum_durability_level.has_value()) {
136
133
  switch (bucket.minimum_durability_level.value()) {
137
134
  case durability_level::none:
138
- encoded.body.append("&durabilityMinLevel=none");
135
+ values["durabilityMinLevel"] = "none";
139
136
  break;
140
137
  case durability_level::majority:
141
- encoded.body.append("&durabilityMinLevel=majority");
138
+ values["durabilityMinLevel"] = "majority";
142
139
  break;
143
140
  case durability_level::majority_and_persist_to_active:
144
- encoded.body.append("&durabilityMinLevel=majorityAndPersistActive");
141
+ values["durabilityMinLevel"] = "majorityAndPersistActive";
145
142
  break;
146
143
  case durability_level::persist_to_majority:
147
- encoded.body.append("&durabilityMinLevel=persistToMajority");
144
+ values["durabilityMinLevel"] = "persistToMajority";
148
145
  break;
149
146
  }
150
147
  }
@@ -152,12 +149,14 @@ bucket_create_request::encode_to(encoded_request_type& encoded,
152
149
  case couchbase::core::management::cluster::bucket_storage_backend::unknown:
153
150
  break;
154
151
  case couchbase::core::management::cluster::bucket_storage_backend::couchstore:
155
- encoded.body.append("&storageBackend=couchstore");
152
+ values["storageBackend"] = "couchstore";
156
153
  break;
157
154
  case couchbase::core::management::cluster::bucket_storage_backend::magma:
158
- encoded.body.append("&storageBackend=magma");
155
+ values["storageBackend"] = "magma";
159
156
  break;
160
157
  }
158
+
159
+ encoded.body = utils::string_codec::v2::form_encode(values);
161
160
  return {};
162
161
  }
163
162
  auto
@@ -30,8 +30,8 @@
30
30
  namespace couchbase::core::operations::management
31
31
  {
32
32
  auto
33
- bucket_update_request::encode_to(encoded_request_type& encoded,
34
- http_context& /* context */) const -> std::error_code
33
+ bucket_update_request::encode_to(encoded_request_type& encoded, http_context& /* context */) const
34
+ -> std::error_code
35
35
  {
36
36
  encoded.method = "POST";
37
37
  encoded.path =
@@ -39,65 +39,61 @@ bucket_update_request::encode_to(encoded_request_type& encoded,
39
39
 
40
40
  encoded.headers["content-type"] = "application/x-www-form-urlencoded";
41
41
 
42
+ std::map<std::string, std::string> values{};
42
43
  if (bucket.ram_quota_mb > 0) {
43
- encoded.body.append(fmt::format("&ramQuotaMB={}", bucket.ram_quota_mb));
44
+ values["ramQuotaMB"] = std::to_string(bucket.ram_quota_mb);
44
45
  }
45
46
  if (bucket.num_replicas.has_value()) {
46
- encoded.body.append(fmt::format("&replicaNumber={}", bucket.num_replicas.value()));
47
+ values["replicaNumber"] = std::to_string(bucket.num_replicas.value());
47
48
  }
48
-
49
49
  if (bucket.max_expiry.has_value()) {
50
- encoded.body.append(fmt::format("&maxTTL={}", bucket.max_expiry.value()));
50
+ values["maxTTL"] = std::to_string(bucket.max_expiry.value());
51
51
  }
52
52
  if (bucket.history_retention_collection_default.has_value()) {
53
- encoded.body.append(
54
- fmt::format("&historyRetentionCollectionDefault={}",
55
- bucket.history_retention_collection_default.value() ? "true" : "false"));
53
+ values["historyRetentionCollectionDefault"] =
54
+ bucket.history_retention_collection_default.value() ? "true" : "false";
56
55
  }
57
56
  if (bucket.history_retention_bytes.has_value()) {
58
- encoded.body.append(
59
- fmt::format("&historyRetentionBytes={}", bucket.history_retention_bytes.value()));
57
+ values["historyRetentionBytes"] = std::to_string(bucket.history_retention_bytes.value());
60
58
  }
61
59
  if (bucket.history_retention_duration.has_value()) {
62
- encoded.body.append(
63
- fmt::format("&historyRetentionSeconds={}", bucket.history_retention_duration.value()));
60
+ values["historyRetentionSeconds"] = std::to_string(bucket.history_retention_duration.value());
64
61
  }
65
62
  if (bucket.replica_indexes.has_value()) {
66
- encoded.body.append(
67
- fmt::format("&replicaIndex={}", bucket.replica_indexes.value() ? "1" : "0"));
63
+ values["replicaIndex"] = bucket.replica_indexes.value() ? "1" : "0";
68
64
  }
69
65
  if (bucket.flush_enabled.has_value()) {
70
- encoded.body.append(fmt::format("&flushEnabled={}", bucket.flush_enabled.value() ? "1" : "0"));
66
+ values["flushEnabled"] = bucket.flush_enabled.value() ? "1" : "0";
71
67
  }
72
68
  if (bucket.num_vbuckets.has_value()) {
73
- encoded.body.append(fmt::format("&numVBuckets={}", bucket.num_vbuckets.value()));
69
+ values["numVBuckets"] = std::to_string(bucket.num_vbuckets.value());
74
70
  }
75
71
 
76
72
  switch (bucket.eviction_policy) {
77
73
  case couchbase::core::management::cluster::bucket_eviction_policy::full:
78
- encoded.body.append("&evictionPolicy=fullEviction");
74
+ values["evictionPolicy"] = "fullEviction";
79
75
  break;
80
76
  case couchbase::core::management::cluster::bucket_eviction_policy::value_only:
81
- encoded.body.append("&evictionPolicy=valueOnly");
77
+ values["evictionPolicy"] = "valueOnly";
82
78
  break;
83
79
  case couchbase::core::management::cluster::bucket_eviction_policy::no_eviction:
84
- encoded.body.append("&evictionPolicy=noEviction");
80
+ values["evictionPolicy"] = "noEviction";
85
81
  break;
86
82
  case couchbase::core::management::cluster::bucket_eviction_policy::not_recently_used:
87
- encoded.body.append("&evictionPolicy=nruEviction");
83
+ values["evictionPolicy"] = "nruEviction";
88
84
  break;
89
85
  case couchbase::core::management::cluster::bucket_eviction_policy::unknown:
90
86
  break;
91
87
  }
92
88
  switch (bucket.compression_mode) {
93
89
  case couchbase::core::management::cluster::bucket_compression::off:
94
- encoded.body.append("&compressionMode=off");
90
+ values["compressionMode"] = "off";
95
91
  break;
96
92
  case couchbase::core::management::cluster::bucket_compression::active:
97
- encoded.body.append("&compressionMode=active");
93
+ values["compressionMode"] = "active";
98
94
  break;
99
95
  case couchbase::core::management::cluster::bucket_compression::passive:
100
- encoded.body.append("&compressionMode=passive");
96
+ values["compressionMode"] = "passive";
101
97
  break;
102
98
  case couchbase::core::management::cluster::bucket_compression::unknown:
103
99
  break;
@@ -105,19 +101,21 @@ bucket_update_request::encode_to(encoded_request_type& encoded,
105
101
  if (bucket.minimum_durability_level) {
106
102
  switch (bucket.minimum_durability_level.value()) {
107
103
  case durability_level::none:
108
- encoded.body.append("&durabilityMinLevel=none");
104
+ values["durabilityMinLevel"] = "none";
109
105
  break;
110
106
  case durability_level::majority:
111
- encoded.body.append("&durabilityMinLevel=majority");
107
+ values["durabilityMinLevel"] = "majority";
112
108
  break;
113
109
  case durability_level::majority_and_persist_to_active:
114
- encoded.body.append("&durabilityMinLevel=majorityAndPersistActive");
110
+ values["durabilityMinLevel"] = "majorityAndPersistActive";
115
111
  break;
116
112
  case durability_level::persist_to_majority:
117
- encoded.body.append("&durabilityMinLevel=persistToMajority");
113
+ values["durabilityMinLevel"] = "persistToMajority";
118
114
  break;
119
115
  }
120
116
  }
117
+
118
+ encoded.body = utils::string_codec::v2::form_encode(values);
121
119
  return {};
122
120
  }
123
121