omq-rs 0.1.2 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ace7dd4c9d05f6cba6ed58941ed30038c131c3de0a246395baf2b932ac76fa25
4
- data.tar.gz: 4c6fdc8bd3543f98c01979ba2cabe4460a6c435d92c91b969b7a6bc0e6b7fdd0
3
+ metadata.gz: d916c6123be10c4d2689f3ced757ae56a96a762ed91a48331c2cc2ae6f5fcef5
4
+ data.tar.gz: 7e19c87b521e037a0d739486c42ab0f2586377754cb0f7dc087b4d581a81820c
5
5
  SHA512:
6
- metadata.gz: ff6a09ddfa0c2c1cddad4a369509ed59085143cd5abb208e506a216f5d6c6977db2a4c77384db602f0cd193be1ab4b69b17f4f0b25f31b9a88d236093e415918
7
- data.tar.gz: a0e77ee6fc098846c4e8e40035ebed72d059cbcbb249c4f264954407cc5fde6ba7f1b3b0294752ad33069e261235269e2c0a87c889636b5b8b04374e31b73f7e
6
+ metadata.gz: 4eca9973111eaf059d20637412340440cfa856ff21c99174a12dd4e9f802c077b6cc517ecb5475e0f619252a6f29d5df4ec22838c0700a6d0200dd2060d71abb
7
+ data.tar.gz: fede7168c871a78f2449b2ed5f92eafb943bafa72fa5050240c2d642f42f4beeba5acee0eea200f114563acee8f2fab11eda19599c7babd9be55441e23565415
data/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.2.0] - 2026-09-04
6
+
7
+ - Add PLAIN username/password allowlists and authentication callbacks through
8
+ `plain_auth:` and `Socket#set_plain_auth`.
9
+ - Expand authenticator peer metadata with transport address and PLAIN
10
+ credentials.
11
+ - Require explicit PLAIN server policy; bare PLAIN server mode fails closed.
12
+ - Reject CURVE public keys that do not match the configured secret key.
13
+ - Require `omq-proto` 0.27.0 and `omq-tokio` 0.22.0.
14
+
5
15
  ## [0.1.2] - 2026-09-01
6
16
 
7
17
  - Route `Socket#try_recv` directly through the native nonblocking receive path.
data/README.md CHANGED
@@ -94,9 +94,10 @@ an `OMQ::Rust::MechanismPeerInfo`, or `nil` to accept every valid CURVE client.
94
94
  Configure it before the first bind, connect, send, receive, or monitor call.
95
95
  `OMQ::Rust.curve_public(secret_key)` derives a public key.
96
96
 
97
- PLAIN uses `plain_server: true` on the server and `plain_username` plus
98
- `plain_password` on clients. PLAIN authenticates without encryption; use it
99
- only on trusted transports.
97
+ PLAIN servers require an explicit policy. Pass fixed credentials with
98
+ `plain_server: true, plain_auth: [["alice", "secret"]]`, or pass a callable as
99
+ `plain_auth:`. Clients use `plain_username` plus `plain_password`. PLAIN
100
+ authenticates without encryption; use it only on trusted transports.
100
101
 
101
102
  ## Compression
102
103
 
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "omq_rs_native"
3
- version = "0.1.2"
3
+ version = "0.2.0"
4
4
  edition = "2024"
5
5
  rust-version = "1.93"
6
6
  license = "ISC"
@@ -20,8 +20,8 @@ zstd = ["omq-tokio/zstd"]
20
20
  ws = ["omq-tokio/ws"]
21
21
 
22
22
  [dependencies]
23
- omq-proto = { version = ">=0.26.1, <0.28.0", default-features = false }
24
- omq-tokio = { version = ">=0.21.4, <0.23.0", default-features = false }
23
+ omq-proto = { version = ">=0.27.0, <0.28.0", default-features = false }
24
+ omq-tokio = { version = ">=0.22.0, <0.23.0", default-features = false }
25
25
  yring = { version = "=0.3.14", features = ["async"] }
26
26
 
27
27
  bytes = "1.12.0"
@@ -6,6 +6,7 @@ use bytes::Bytes;
6
6
  use rb_sys::VALUE;
7
7
 
8
8
  use crate::notify::PipeNotify;
9
+ #[cfg(feature = "curve")]
9
10
  use crate::options::parse_curve_public_key;
10
11
  use crate::rb::{self, RbResult};
11
12
 
@@ -13,6 +14,9 @@ enum AuthRequest {
13
14
  Check {
14
15
  public_key: [u8; 32],
15
16
  identity: Option<Bytes>,
17
+ peer_address: Option<String>,
18
+ username: Option<String>,
19
+ password: Option<String>,
16
20
  reply: flume::Sender<bool>,
17
21
  },
18
22
  Stop,
@@ -84,32 +88,73 @@ unsafe extern "C" fn auth_worker_main(data: *mut c_void) -> VALUE {
84
88
  while let Some(AuthRequest::Check {
85
89
  public_key,
86
90
  identity,
91
+ peer_address,
92
+ username,
93
+ password,
87
94
  reply,
88
95
  }) = wait_for_request(&data)
89
96
  {
90
- let accepted = invoke_callback(data.callback, public_key, identity.as_ref());
97
+ let accepted = invoke_callback(
98
+ data.callback,
99
+ public_key,
100
+ identity.as_ref(),
101
+ peer_address.as_deref(),
102
+ username.as_deref(),
103
+ password.as_deref(),
104
+ );
91
105
  let _ = reply.send(accepted);
92
106
  }
93
107
  }));
94
108
  rb::qnil()
95
109
  }
96
110
 
97
- fn invoke_callback(callback: VALUE, public_key: [u8; 32], identity: Option<&Bytes>) -> bool {
111
+ fn invoke_callback(
112
+ callback: VALUE,
113
+ public_key: [u8; 32],
114
+ identity: Option<&Bytes>,
115
+ peer_address: Option<&str>,
116
+ username: Option<&str>,
117
+ password: Option<&str>,
118
+ ) -> bool {
98
119
  let result = (|| -> RbResult<VALUE> {
99
120
  let peer = rb::hash_new()?;
100
- let key = omq_proto::CurvePublicKey::from_bytes(public_key)
101
- .to_z85()
102
- .into_bytes();
103
- rb::hash_aset(
104
- peer,
105
- rb::symbol("public_key")?,
106
- rb::new_binary_string(&key)?,
107
- )?;
121
+ let key = if username.is_some() {
122
+ rb::qnil()
123
+ } else {
124
+ #[cfg(feature = "curve")]
125
+ {
126
+ let key = omq_proto::CurvePublicKey::from_bytes(public_key)
127
+ .to_z85()
128
+ .into_bytes();
129
+ rb::new_binary_string(&key)?
130
+ }
131
+ #[cfg(not(feature = "curve"))]
132
+ {
133
+ let _ = public_key;
134
+ rb::qnil()
135
+ }
136
+ };
137
+ rb::hash_aset(peer, rb::symbol("public_key")?, key)?;
108
138
  let identity = match identity {
109
139
  Some(value) => rb::new_binary_string(value)?,
110
140
  None => rb::qnil(),
111
141
  };
112
142
  rb::hash_aset(peer, rb::symbol("identity")?, identity)?;
143
+ let peer_address = match peer_address {
144
+ Some(value) => rb::new_utf8_string(value)?,
145
+ None => rb::qnil(),
146
+ };
147
+ rb::hash_aset(peer, rb::symbol("peer_address")?, peer_address)?;
148
+ let username = match username {
149
+ Some(value) => rb::new_utf8_string(value)?,
150
+ None => rb::qnil(),
151
+ };
152
+ rb::hash_aset(peer, rb::symbol("username")?, username)?;
153
+ let password = match password {
154
+ Some(value) => rb::new_utf8_string(value)?,
155
+ None => rb::qnil(),
156
+ };
157
+ rb::hash_aset(peer, rb::symbol("password")?, password)?;
113
158
  rb::call_method_1(callback, c"call", peer)
114
159
  })();
115
160
 
@@ -121,6 +166,7 @@ fn invoke_callback(callback: VALUE, public_key: [u8; 32], identity: Option<&Byte
121
166
  }
122
167
  }
123
168
 
169
+ #[cfg(feature = "curve")]
124
170
  pub fn allowed_keys(value: VALUE) -> RbResult<omq_proto::Authenticator> {
125
171
  let count = rb::array_len(value)?;
126
172
  let mut keys = std::collections::HashSet::with_capacity(count);
@@ -162,6 +208,9 @@ pub fn callback(callback: VALUE) -> RbResult<(omq_proto::Authenticator, AuthWork
162
208
  let request = AuthRequest::Check {
163
209
  public_key: peer.public_key,
164
210
  identity: peer.identity.clone(),
211
+ peer_address: peer.peer_address.clone(),
212
+ username: peer.username.clone(),
213
+ password: peer.password.clone(),
165
214
  reply,
166
215
  };
167
216
  if auth_sender.send(request).is_err() {
@@ -1,4 +1,4 @@
1
- #[cfg(feature = "curve")]
1
+ #[cfg(any(feature = "curve", feature = "plain"))]
2
2
  mod auth;
3
3
  mod error;
4
4
  mod notify;
@@ -212,7 +212,6 @@ fn apply_mechanism(hash: VALUE, mech_type: &str, opts: &mut omq_tokio::Options)
212
212
  .ok_or_else(|| RubyErr::arg("CURVE server requires curve_secretkey"))?,
213
213
  "curve_secretkey",
214
214
  )?;
215
- validate_curve_keypair(&public, &secret)?;
216
215
  opts.mechanism = omq_proto::MechanismSetup::CurveServer {
217
216
  our_keypair: omq_proto::CurveKeypair { public, secret },
218
217
  options: omq_proto::CurveServerOptions::default(),
@@ -236,7 +235,6 @@ fn apply_mechanism(hash: VALUE, mech_type: &str, opts: &mut omq_tokio::Options)
236
235
  .ok_or_else(|| RubyErr::arg("CURVE client requires curve_secretkey"))?,
237
236
  "curve_secretkey",
238
237
  )?;
239
- validate_curve_keypair(&public, &secret)?;
240
238
  let server_public = parse_curve_public_key(
241
239
  &server_key
242
240
  .ok_or_else(|| RubyErr::arg("CURVE client requires curve_serverkey"))?,
@@ -253,7 +251,7 @@ fn apply_mechanism(hash: VALUE, mech_type: &str, opts: &mut omq_tokio::Options)
253
251
  "plain" => {
254
252
  if get_opt_bool_alias(hash, &["plain_server", "mechanism_server"])?.unwrap_or(false) {
255
253
  opts.mechanism = omq_proto::MechanismSetup::PlainServer {
256
- authenticator: omq_proto::Authenticator::new(|_| true),
254
+ authenticator: omq_proto::Authenticator::new(|_| false),
257
255
  };
258
256
  } else {
259
257
  let username =
@@ -296,18 +294,6 @@ fn curve_secret_key(bytes: &[u8], label: &str) -> RbResult<omq_proto::CurveSecre
296
294
  .map_err(|error| RubyErr::arg(format!("invalid {label}: {error}")))
297
295
  }
298
296
 
299
- #[cfg(feature = "curve")]
300
- fn validate_curve_keypair(
301
- public: &omq_proto::CurvePublicKey,
302
- secret: &omq_proto::CurveSecretKey,
303
- ) -> RbResult<()> {
304
- if secret.derive_public().as_bytes() == public.as_bytes() {
305
- Ok(())
306
- } else {
307
- Err(RubyErr::arg("CURVE public and secret keys do not match"))
308
- }
309
- }
310
-
311
297
  fn option_present(hash: VALUE, keys: &[&str]) -> RbResult<bool> {
312
298
  for key in keys {
313
299
  if rb::hash_get(hash, key)?.is_some() {
@@ -28,8 +28,10 @@ pub struct RustSocket {
28
28
  materialized: RwLock<Option<Materialized>>,
29
29
  closed: AtomicBool,
30
30
  linger: Mutex<Option<std::time::Duration>>,
31
- #[cfg(feature = "curve")]
31
+ #[cfg(any(feature = "curve", feature = "plain"))]
32
32
  auth_worker: Mutex<Option<crate::auth::AuthWorker>>,
33
+ #[cfg(feature = "plain")]
34
+ plain_auth_configured: AtomicBool,
33
35
  }
34
36
 
35
37
  unsafe impl Send for RustSocket {}
@@ -71,7 +73,7 @@ unsafe extern "C" fn rust_socket_mark(ptr: *mut c_void) {
71
73
  }
72
74
  let _ = catch_unwind(AssertUnwindSafe(|| {
73
75
  let socket = unsafe { &*ptr.cast::<RustSocket>() };
74
- #[cfg(feature = "curve")]
76
+ #[cfg(any(feature = "curve", feature = "plain"))]
75
77
  if let Some(worker) = socket.auth_worker.lock().unwrap().as_ref() {
76
78
  unsafe {
77
79
  rb_sys::rb_gc_mark(worker.callback());
@@ -139,8 +141,10 @@ fn rust_socket_new_impl(class: VALUE, type_str: VALUE) -> RbResult<VALUE> {
139
141
  materialized: RwLock::new(None),
140
142
  closed: AtomicBool::new(false),
141
143
  linger: Mutex::new(None),
142
- #[cfg(feature = "curve")]
144
+ #[cfg(any(feature = "curve", feature = "plain"))]
143
145
  auth_worker: Mutex::new(None),
146
+ #[cfg(feature = "plain")]
147
+ plain_auth_configured: AtomicBool::new(false),
144
148
  }),
145
149
  rust_socket_data_type(),
146
150
  )
@@ -206,6 +210,95 @@ fn set_curve_authenticator(
206
210
  Ok(())
207
211
  }
208
212
 
213
+ #[cfg(feature = "plain")]
214
+ fn set_plain_authenticator(
215
+ rb_self: &RustSocket,
216
+ authenticator: omq_proto::Authenticator,
217
+ mut worker: Option<crate::auth::AuthWorker>,
218
+ ) -> RbResult<()> {
219
+ if rb_self.materialized.read().unwrap().is_some() {
220
+ if let Some(worker) = worker.take() {
221
+ worker.stop();
222
+ }
223
+ return Err(RubyErr::runtime(
224
+ "PLAIN authentication must be configured before bind or connect",
225
+ ));
226
+ }
227
+
228
+ let mut options_guard = rb_self.options.lock().unwrap();
229
+ let options = options_guard
230
+ .as_mut()
231
+ .ok_or_else(|| RubyErr::runtime("socket options not configured"))?;
232
+ if let omq_proto::MechanismSetup::PlainServer {
233
+ authenticator: configured,
234
+ } = &mut options.mechanism
235
+ {
236
+ *configured = authenticator;
237
+ } else {
238
+ drop(options_guard);
239
+ if let Some(worker) = worker.take() {
240
+ worker.stop();
241
+ }
242
+ return Err(RubyErr::runtime(
243
+ "PLAIN authentication requires a PLAIN server socket",
244
+ ));
245
+ }
246
+ drop(options_guard);
247
+
248
+ let previous = rb_self.auth_worker.lock().unwrap().take();
249
+ if let Some(previous) = previous {
250
+ previous.stop();
251
+ }
252
+ *rb_self.auth_worker.lock().unwrap() = worker;
253
+ rb_self.plain_auth_configured.store(true, Ordering::Release);
254
+ Ok(())
255
+ }
256
+
257
+ #[cfg(feature = "plain")]
258
+ unsafe extern "C" fn rust_socket_set_plain_auth_credentials(
259
+ rb_self: VALUE,
260
+ credentials: VALUE,
261
+ ) -> VALUE {
262
+ rb::wrap(|| {
263
+ let rb_self = unsafe { rust_socket_ref(rb_self)? };
264
+ let count = rb::array_len(credentials)?;
265
+ let mut allowlist = Vec::with_capacity(count);
266
+ for index in 0..count {
267
+ let pair = rb::array_entry(credentials, index)?;
268
+ if rb::array_len(pair)? != 2 {
269
+ return Err(RubyErr::arg(
270
+ "each PLAIN credential must be a [username, password] pair",
271
+ ));
272
+ }
273
+ let username = rb::value_to_string(rb::array_entry(pair, 0)?)?;
274
+ let password = rb::value_to_string(rb::array_entry(pair, 1)?)?;
275
+ if username.len() > 255
276
+ || password.len() > 255
277
+ || !username.bytes().all(|byte| byte.is_ascii_graphic())
278
+ || !password.bytes().all(|byte| byte.is_ascii_graphic())
279
+ {
280
+ return Err(RubyErr::arg(
281
+ "PLAIN credentials must contain at most 255 ASCII VCHAR bytes",
282
+ ));
283
+ }
284
+ allowlist.push((username, password));
285
+ }
286
+ let authenticator = omq_proto::Authenticator::plain_credentials(allowlist);
287
+ set_plain_authenticator(rb_self, authenticator, None)?;
288
+ Ok(rb::qnil())
289
+ })
290
+ }
291
+
292
+ #[cfg(feature = "plain")]
293
+ unsafe extern "C" fn rust_socket_set_plain_auth_callback(rb_self: VALUE, callback: VALUE) -> VALUE {
294
+ rb::wrap(|| {
295
+ let rb_self = unsafe { rust_socket_ref(rb_self)? };
296
+ let (authenticator, worker) = crate::auth::callback(callback)?;
297
+ set_plain_authenticator(rb_self, authenticator, Some(worker))?;
298
+ Ok(rb::qnil())
299
+ })
300
+ }
301
+
209
302
  #[cfg(feature = "curve")]
210
303
  unsafe extern "C" fn rust_socket_set_curve_auth_keys(rb_self: VALUE, keys: VALUE) -> VALUE {
211
304
  rb::wrap(|| {
@@ -250,6 +343,25 @@ fn rust_socket_materialize_impl(rb_self: &RustSocket) -> RbResult<()> {
250
343
  return Ok(());
251
344
  }
252
345
 
346
+ #[cfg(feature = "plain")]
347
+ if rb_self
348
+ .options
349
+ .lock()
350
+ .unwrap()
351
+ .as_ref()
352
+ .is_some_and(|options| {
353
+ matches!(
354
+ options.mechanism,
355
+ omq_proto::MechanismSetup::PlainServer { .. }
356
+ )
357
+ })
358
+ && !rb_self.plain_auth_configured.load(Ordering::Acquire)
359
+ {
360
+ return Err(RubyErr::runtime(
361
+ "PLAIN server requires explicit authentication via set_plain_auth",
362
+ ));
363
+ }
364
+
253
365
  let opts = rb_self.options.lock().unwrap().take().unwrap_or_default();
254
366
  let send_cap = opts.send_hwm.max(1) as usize;
255
367
  let recv_cap = opts.recv_hwm.max(1) as usize;
@@ -774,13 +886,15 @@ unsafe extern "C" fn rust_socket_leave(rb_self: VALUE, group: VALUE) -> VALUE {
774
886
 
775
887
  fn rust_socket_close_impl(rb_self: &RustSocket, wait_for_auth_worker: bool) {
776
888
  rb_self.closed.store(true, Ordering::Relaxed);
777
- #[cfg(feature = "curve")]
778
- let worker = rb_self.auth_worker.lock().unwrap().take();
779
- if let Some(worker) = worker {
780
- if wait_for_auth_worker {
781
- worker.stop();
782
- } else {
783
- worker.request_stop();
889
+ #[cfg(any(feature = "curve", feature = "plain"))]
890
+ {
891
+ let worker = rb_self.auth_worker.lock().unwrap().take();
892
+ if let Some(worker) = worker {
893
+ if wait_for_auth_worker {
894
+ worker.stop();
895
+ } else {
896
+ worker.request_stop();
897
+ }
784
898
  }
785
899
  }
786
900
  let mat = rb_self.materialized.write().unwrap().take();
@@ -934,6 +1048,19 @@ pub fn register(native: VALUE) -> RbResult<()> {
934
1048
  )?;
935
1049
  rb::define_method_0(class, c"clear_curve_auth", rust_socket_clear_curve_auth)?;
936
1050
  }
1051
+ #[cfg(feature = "plain")]
1052
+ {
1053
+ rb::define_method_1(
1054
+ class,
1055
+ c"set_plain_auth_credentials",
1056
+ rust_socket_set_plain_auth_credentials,
1057
+ )?;
1058
+ rb::define_method_1(
1059
+ class,
1060
+ c"set_plain_auth_callback",
1061
+ rust_socket_set_plain_auth_callback,
1062
+ )?;
1063
+ }
937
1064
  rb::define_method_0(class, c"materialize", rust_socket_materialize)?;
938
1065
  rb::define_method_1(class, c"bind", rust_socket_bind)?;
939
1066
  rb::define_method_1(class, c"connect", rust_socket_connect)?;
data/lib/omq/rs/socket.rb CHANGED
@@ -81,13 +81,25 @@ module OMQ
81
81
  ].freeze
82
82
  private_constant :ROUTED_TYPES, :SINGLE_FRAME_TYPES, :SOCKET_OPTIONS
83
83
 
84
- # CURVE peer metadata passed to callable authenticators.
84
+ # Peer metadata passed to callable CURVE and PLAIN authenticators.
85
85
  #
86
86
  # @!attribute [r] public_key
87
- # @return [String] peer's 40-byte Z85 public key
87
+ # @return [String, nil] peer's 40-byte Z85 public key for CURVE
88
88
  # @!attribute [r] identity
89
89
  # @return [String, nil] peer's ZMTP identity
90
- MechanismPeerInfo = Data.define(:public_key, :identity)
90
+ # @!attribute [r] peer_address
91
+ # @return [String, nil] peer's transport address
92
+ # @!attribute [r] username
93
+ # @return [String, nil] peer's PLAIN username
94
+ # @!attribute [r] password
95
+ # @return [String, nil] peer's PLAIN password
96
+ MechanismPeerInfo = Data.define(
97
+ :public_key,
98
+ :identity,
99
+ :peer_address,
100
+ :username,
101
+ :password,
102
+ )
91
103
 
92
104
  class << self
93
105
  # Returns number of OMQ.rs IO threads.
@@ -150,17 +162,20 @@ module OMQ
150
162
  Native.curve_public(secret_key)
151
163
  end
152
164
 
153
- # Adapts a public CURVE authenticator to native peer metadata.
165
+ # Adapts a public authenticator to native peer metadata.
154
166
  #
155
167
  # @param authenticator [#call] callable receiving {MechanismPeerInfo}
156
168
  # @return [Proc]
157
169
  # @api private
158
- def wrap_curve_authenticator(authenticator)
170
+ def wrap_authenticator(authenticator)
159
171
  proc do |peer|
160
172
  authenticator.call(
161
173
  MechanismPeerInfo.new(
162
174
  public_key: peer.fetch(:public_key),
163
175
  identity: peer[:identity],
176
+ peer_address: peer[:peer_address],
177
+ username: peer[:username],
178
+ password: peer[:password],
164
179
  ),
165
180
  )
166
181
  end
@@ -222,6 +237,8 @@ module OMQ
222
237
  # @param recv_timeout [Numeric, nil] receive timeout in seconds
223
238
  # @param send_timeout [Numeric, nil] send timeout in seconds
224
239
  # @param curve_auth [Array<String>, #call, nil] CURVE allowlist or authenticator
240
+ # @param plain_auth [Array<Array(String, String)>, #call, nil] PLAIN
241
+ # credential allowlist or authenticator
225
242
  # @param options [Hash] native OMQ.rs socket options
226
243
  # @option options [Symbol] :workload_profile +:throughput+ or +:latency+
227
244
  # @option options [Integer] :send_hwm outbound message capacity
@@ -270,7 +287,7 @@ module OMQ
270
287
  # @option options [String] :curve_serverkey CURVE server raw or Z85 public key
271
288
  # @return [Socket]
272
289
  # @raise [ArgumentError] if an option is unknown or invalid
273
- def initialize(recv_timeout: nil, send_timeout: nil, curve_auth: nil, **options)
290
+ def initialize(recv_timeout: nil, send_timeout: nil, curve_auth: nil, plain_auth: nil, **options)
274
291
  socket_type = self.class.const_get(:SOCKET_TYPE, false)
275
292
  @socket_type = socket_type.to_s.downcase.to_sym
276
293
  unless SOCKET_TYPES.include?(@socket_type)
@@ -290,6 +307,7 @@ module OMQ
290
307
  @peer_connected = false
291
308
  @subscriber_joined = false
292
309
  set_curve_auth(curve_auth) unless curve_auth.nil?
310
+ set_plain_auth(plain_auth) unless plain_auth.nil?
293
311
  end
294
312
 
295
313
  # Binds socket to an endpoint.
@@ -363,7 +381,36 @@ module OMQ
363
381
  raise TypeError, "CURVE authenticator must be an Array, callable, or nil"
364
382
  end
365
383
 
366
- @native.set_curve_auth_callback(Rust.wrap_curve_authenticator(authenticator))
384
+ @native.set_curve_auth_callback(Rust.wrap_authenticator(authenticator))
385
+ end
386
+ self
387
+ end
388
+
389
+ # Configures PLAIN client admission on a server before materialization.
390
+ #
391
+ # Pass an array of username/password pairs, or a callable receiving
392
+ # {MechanismPeerInfo}. Pairs match exactly and case-sensitively. Each
393
+ # field must contain at most 255 ASCII VCHAR bytes. An empty array denies
394
+ # every client. PLAIN never has an allow-all mode and does not encrypt
395
+ # traffic.
396
+ #
397
+ # @param credentials [Array<Array(String, String)>, #call] credential
398
+ # allowlist or authenticator
399
+ # @yieldparam peer [MechanismPeerInfo]
400
+ # @return [Socket] self
401
+ # @raise [RuntimeError] if socket is already materialized
402
+ # @raise [TypeError, ArgumentError] if credentials are invalid
403
+ def set_plain_auth(credentials = nil, &block)
404
+ raise RuntimeError, "PLAIN authentication must be configured before bind or connect" if @materialized
405
+
406
+ authenticator = block || (credentials if credentials.respond_to?(:call))
407
+ if authenticator
408
+ @native.set_plain_auth_callback(Rust.wrap_authenticator(authenticator))
409
+ else
410
+ unless credentials.is_a?(Array)
411
+ raise TypeError, "PLAIN authentication requires a credential Array or a callable"
412
+ end
413
+ @native.set_plain_auth_credentials(credentials)
367
414
  end
368
415
  self
369
416
  end
@@ -5,6 +5,6 @@ module OMQ
5
5
  module Rust
6
6
  # Ruby binding version.
7
7
  # @return [String]
8
- VERSION = "0.1.2"
8
+ VERSION = "0.2.0"
9
9
  end
10
10
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omq-rs
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrik Wenger