use std::{ffi::CString, ptr, time::Duration};
use open62541_sys::{
UA_CertificateVerification_AcceptAll, UA_Client_connect, UA_Client_getEndpoints,
UA_ClientConfig,
};
use crate::{DataType as _, Error, Result, ua};
#[derive(Debug)]
pub struct ClientBuilder(ua::ClientConfig);
impl ClientBuilder {
#[must_use]
fn default() -> Self {
Self(ua::ClientConfig::default(ClientContext::default()))
}
#[cfg(feature = "mbedtls")]
pub fn default_encryption(
local_certificate: &crate::Certificate,
private_key: &crate::PrivateKey,
) -> Result<Self> {
Ok(Self(ua::ClientConfig::default_encryption(
ClientContext::default(),
local_certificate,
private_key,
)?))
}
#[must_use]
pub fn timeout(mut self, timeout: Duration) -> Self {
self.config_mut().timeout = u32::try_from(timeout.as_millis())
.expect("timeout (in milliseconds) should be in range of u32");
self
}
#[must_use]
pub fn client_description(mut self, client_description: ua::ApplicationDescription) -> Self {
client_description.move_into_raw(&mut self.config_mut().clientDescription);
self
}
#[must_use]
pub fn user_identity_token(mut self, user_identity_token: &ua::UserIdentityToken) -> Self {
user_identity_token
.to_extension_object()
.move_into_raw(&mut self.config_mut().userIdentityToken);
self
}
#[must_use]
pub fn security_mode(mut self, security_mode: ua::MessageSecurityMode) -> Self {
security_mode.move_into_raw(&mut self.config_mut().securityMode);
self
}
#[must_use]
pub fn security_policy_uri(mut self, security_policy_uri: ua::String) -> Self {
security_policy_uri.move_into_raw(&mut self.config_mut().securityPolicyUri);
self
}
#[must_use]
pub fn secure_channel_life_time(mut self, secure_channel_life_time: Duration) -> Self {
self.config_mut().secureChannelLifeTime =
u32::try_from(secure_channel_life_time.as_millis())
.expect("secure channel life time (in milliseconds) should be in range of u32");
self
}
#[must_use]
pub fn requested_session_timeout(mut self, requested_session_timeout: Duration) -> Self {
self.config_mut().requestedSessionTimeout =
u32::try_from(requested_session_timeout.as_millis())
.expect("secure channel life time (in milliseconds) should be in range of u32");
self
}
#[must_use]
pub fn connectivity_check_interval(
mut self,
connectivity_check_interval: Option<Duration>,
) -> Self {
self.config_mut().connectivityCheckInterval =
u32::try_from(connectivity_check_interval.map_or(0, |interval| interval.as_millis()))
.expect("connectivity check interval (in milliseconds) should be in range of u32");
self
}
#[must_use]
pub fn accept_all(mut self) -> Self {
let config = self.config_mut();
unsafe {
UA_CertificateVerification_AcceptAll(&raw mut config.certificateVerification);
}
self
}
#[must_use]
pub fn certificate_verification(
mut self,
certificate_verification: ua::CertificateVerification,
) -> Self {
let config = self.config_mut();
certificate_verification.move_into_raw(&mut config.certificateVerification);
self
}
#[cfg(feature = "mbedtls")]
#[must_use]
pub fn private_key_password_callback(
mut self,
private_key_password_callback: impl crate::PrivateKeyPasswordCallback + 'static,
) -> Self {
self.context_mut().private_key_password_callback =
Some(Box::new(private_key_password_callback));
self
}
pub fn connect(self, endpoint_url: &str) -> Result<Client> {
let mut client = self.build();
client.connect(endpoint_url)?;
Ok(client)
}
pub fn get_endpoints(self, server_url: &str) -> Result<ua::Array<ua::EndpointDescription>> {
log::info!("Getting endpoints of server {server_url}");
let server_url = CString::new(server_url).expect("server URL does not contain NUL bytes");
let mut client = self.build();
let endpoint_descriptions: Option<ua::Array<ua::EndpointDescription>>;
let status_code = ua::StatusCode::new({
let mut endpoint_descriptions_size = 0;
let mut endpoint_descriptions_ptr = ptr::null_mut();
let result = unsafe {
UA_Client_getEndpoints(
client.0.as_mut_ptr(),
server_url.as_ptr(),
&raw mut endpoint_descriptions_size,
&raw mut endpoint_descriptions_ptr,
)
};
endpoint_descriptions = ua::Array::<ua::EndpointDescription>::from_raw_parts(
endpoint_descriptions_size,
endpoint_descriptions_ptr,
);
result
});
Error::verify_good(&status_code)?;
let Some(endpoint_descriptions) = endpoint_descriptions else {
return Err(Error::internal("expected array of endpoint descriptions"));
};
Ok(endpoint_descriptions)
}
#[must_use]
fn build(self) -> Client {
Client(ua::Client::new_with_config(self.0))
}
#[must_use]
fn config_mut(&mut self) -> &mut UA_ClientConfig {
unsafe { self.0.as_mut() }
}
#[cfg_attr(not(feature = "mbedtls"), expect(dead_code, reason = "unused"))]
#[must_use]
fn context_mut(&mut self) -> &mut ClientContext {
self.0.context_mut()
}
}
impl Default for ClientBuilder {
fn default() -> Self {
Self::default()
}
}
#[derive(Default)]
pub(crate) struct ClientContext {
#[cfg(feature = "mbedtls")]
pub(crate) private_key_password_callback: Option<Box<dyn crate::PrivateKeyPasswordCallback>>,
}
#[derive(Debug)]
pub struct Client(ua::Client);
impl Client {
pub fn new(endpoint_url: &str) -> Result<Self> {
ClientBuilder::default().connect(endpoint_url)
}
#[must_use]
pub fn into_async(self) -> crate::AsyncClient {
crate::AsyncClient::from_sync(self.0)
}
#[must_use]
pub fn state(&self) -> ua::ClientState {
self.0.state()
}
fn connect(&mut self, endpoint_url: &str) -> Result<()> {
log::info!("Connecting to endpoint {endpoint_url}");
let endpoint_url =
CString::new(endpoint_url).expect("endpoint URL does not contain NUL bytes");
let status_code = ua::StatusCode::new(unsafe {
UA_Client_connect(self.0.as_mut_ptr(), endpoint_url.as_ptr())
});
Error::verify_good(&status_code)
}
#[expect(clippy::semicolon_if_nothing_returned, reason = "future fail-safe")]
pub fn disconnect(self) {
self.0.disconnect()
}
}