From 4438579e68b5a534dad909be0579a8d391c47120 Mon Sep 17 00:00:00 2001 From: Gustav Palmqvist Date: Mon, 31 Oct 2022 23:18:27 +0100 Subject: [PATCH 1/5] Secure capability --- hislip/src/server/mod.rs | 13 ++++++++++++- hislip/src/server/session/synchronous.rs | 13 +++++++++---- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/hislip/src/server/mod.rs b/hislip/src/server/mod.rs index e86db47..c2716f5 100644 --- a/hislip/src/server/mod.rs +++ b/hislip/src/server/mod.rs @@ -237,6 +237,16 @@ where from_utf8(&payload).unwrap_or("") ); } + Message { + message_type: MessageType::StartTLS, + control_code, + message_parameter, + payload, + } => { + // Uppgrade connection + + // Start session + } Message { message_type: MessageType::Initialize, message_parameter, @@ -301,8 +311,9 @@ where shared, RemoteLockHandle::new(device), receiver, + protocol ) - .handle_session(stream, peer.clone(), protocol) + .handle_session(stream, peer.clone()) .await; log::debug!(peer=peer.to_string(), session_id=id; "Sync session closed: {res:?}"); return res; diff --git a/hislip/src/server/session/synchronous.rs b/hislip/src/server/session/synchronous.rs index a368cae..691b2de 100644 --- a/hislip/src/server/session/synchronous.rs +++ b/hislip/src/server/session/synchronous.rs @@ -33,6 +33,8 @@ where shared: Arc>, clear: Receiver<()>, + + protocol: Protocol } impl SyncSession @@ -45,6 +47,7 @@ where shared: Arc>, handle: RemoteLockHandle, clear: Receiver<()>, + protocol: Protocol ) -> Self { Self { id, @@ -52,6 +55,7 @@ where shared, handle, clear, + protocol } } @@ -134,7 +138,6 @@ where self, mut stream: S, peer: String, - protocol: Protocol, ) -> Result<(), io::Error> where S: AsyncRead + AsyncWrite + Unpin, @@ -327,11 +330,13 @@ where Message { message_type: MessageType::GetDescriptors, .. - } => {} + } if self.protocol >= PROTOCOL_2_0 => { + todo!() + } Message { message_type: MessageType::StartTLS | MessageType::EndTLS, .. - } if protocol >= PROTOCOL_2_0 => { + } if self.protocol >= PROTOCOL_2_0 => { log::debug!(peer=peer.to_string(), session_id=self.id; "Start/end TLS"); send_fatal!( @@ -347,7 +352,7 @@ where | MessageType::AuthenticationExchange, payload: _data, .. - } if protocol >= PROTOCOL_2_0 => { + } if self.protocol >= PROTOCOL_2_0 => { log::debug!(peer=peer.to_string(), session_id=self.id; "Authentication Start/Exchange"); send_fatal!( From 2da185fd7d0c77cc377e2683803e05a82f8bb9f8 Mon Sep 17 00:00:00 2001 From: Gustav Palmqvist Date: Tue, 1 Nov 2022 17:59:34 +0100 Subject: [PATCH 2/5] Some stuff --- .gitignore | 5 +- .vscode/launch.json | 16 +++ Cargo.toml | 15 +-- hislip/Cargo.toml | 9 +- hislip/src/common/descriptors.rs | 74 +++++++++++++ hislip/src/common/mod.rs | 2 + hislip/src/common/stream.rs | 108 +++++++++++++++++++ hislip/src/server/mod.rs | 5 +- hislip/src/server/session/asynchronous.rs | 102 +++++++++--------- hislip/src/server/session/mod.rs | 19 +++- raw/Cargo.toml | 8 +- raw/examples/{raw.rs => scpi-raw.rs} | 0 raw/examples/scpi-tls.rs | 123 ++++++++++++++++++++++ raw/src/lib.rs | 5 + raw/src/server/mod.rs | 54 ++++++++++ raw/src/server/tls.rs | 13 +++ 16 files changed, 493 insertions(+), 65 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 hislip/src/common/descriptors.rs create mode 100644 hislip/src/common/stream.rs rename raw/examples/{raw.rs => scpi-raw.rs} (100%) create mode 100644 raw/examples/scpi-tls.rs create mode 100644 raw/src/server/tls.rs diff --git a/.gitignore b/.gitignore index 21b6580..76dc915 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,7 @@ __pycache__/ .pytest_cache/ # Coverage -lcov.info \ No newline at end of file +lcov.info + +# Certificates +/.certificates \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..10efcb2 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug", + "program": "${workspaceFolder}/", + "args": [], + "cwd": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 34ebecd..acf7ef5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,5 @@ [workspace] -members = [ - "device", - "hislip", - "raw", - "telnet", - "vxi11" -] +members = ["device", "hislip", "raw", "telnet", "vxi11"] [workspace.package] version = "0.1.0" @@ -15,12 +9,13 @@ edition = "2021" [workspace.dependencies] # Common dependencies -async-std = {version = "1.11", features = ["attributes"]} +async-std = { version = "1.11", features = ["attributes"] } async-listen = "0.2.1" -futures = {version = "0.3" } +futures = { version = "0.3" } log = { version = "0.4.17" } byteorder = { version = "1.4" } +async-rustls = { version = "0.2" } # Dev dependencies femme = "2.2" -clap = { version = "4.0", features = ["derive"] } \ No newline at end of file +clap = { version = "4.0", features = ["derive"] } diff --git a/hislip/Cargo.toml b/hislip/Cargo.toml index 67c7379..b0de5cf 100644 --- a/hislip/Cargo.toml +++ b/hislip/Cargo.toml @@ -15,11 +15,16 @@ futures = { workspace = true } byteorder = { workspace = true } log = { workspace = true, features = ["kv_unstable_std"] } bitfield = "0.14" +async-rustls = { workspace = true, optional = true } +cfg-if = "1.0.0" [dependencies.lxi-device] path = "../device" version = "0.1.0" [dev-dependencies] -femme = { workspace = true } -clap = { workspace = true } \ No newline at end of file +femme = { workspace = true } +clap = { workspace = true } + +[features] +secure-capability = ["dep:async-rustls"] diff --git a/hislip/src/common/descriptors.rs b/hislip/src/common/descriptors.rs new file mode 100644 index 0000000..39b2c2b --- /dev/null +++ b/hislip/src/common/descriptors.rs @@ -0,0 +1,74 @@ +use std::io; +use byteorder::{WriteBytesExt, ReadBytesExt}; + +pub enum Descriptor { + SupportedTlsVersions(Vec), + TlsInformation(Vec), + TlsLastError(Vec), + Reserved(u8, Vec), + VendorSpecific(u8, Vec), +} + +impl Descriptor { + pub fn read_descriptor(reader: &mut R) -> io::Result { + let len = reader.read_u16::()?; + let typ = reader.read_u8()?; + match typ { + 0 => { + let mut buf = Vec::with_capacity(len as usize); + for _ in 0..len { + buf.push(reader.read_u16::()?) + } + Ok(Self::SupportedTlsVersions(buf)) + }, + 1 => { + let mut buf = Vec::with_capacity(len as usize); + reader.read_exact(&mut buf)?; + Ok(Self::TlsInformation(buf)) + }, + 2 => { + let mut buf = Vec::with_capacity(len as usize); + reader.read_exact(&mut buf)?; + Ok(Self::TlsLastError(buf)) + }, + 3..=127 => { + let mut buf = Vec::with_capacity(len as usize); + reader.read_exact(&mut buf)?; + Ok(Self::Reserved(typ, buf)) + } + 128..=255 => { + let mut buf = Vec::with_capacity(len as usize); + reader.read_exact(&mut buf)?; + Ok(Self::VendorSpecific(typ, buf)) + } + } + } + + pub fn write_descriptor(&self, writer: &mut W) -> io::Result<()> { + match self { + Descriptor::SupportedTlsVersions(versions) => { + writer.write_u16::(versions.len() as u16)?; + writer.write_u8(0)?; + for v in versions { + writer.write_u16::(*v)?; + } + } + Descriptor::TlsInformation(info) => { + writer.write_u16::(info.len() as u16)?; + writer.write_u8(1)?; + writer.write(info)?; + } + Descriptor::TlsLastError(err) => { + writer.write_u16::(err.len() as u16)?; + writer.write_u8(2)?; + writer.write(err)?; + } + Descriptor::Reserved(t, dat) | Descriptor::VendorSpecific(t, dat) => { + writer.write_u16::(dat.len() as u16)?; + writer.write_u8(t.clone())?; + writer.write(dat)?; + } + } + Ok(()) + } +} diff --git a/hislip/src/common/mod.rs b/hislip/src/common/mod.rs index a206521..491b152 100644 --- a/hislip/src/common/mod.rs +++ b/hislip/src/common/mod.rs @@ -2,6 +2,8 @@ use bitfield::bitfield; pub mod errors; pub mod messages; +pub mod descriptors; +pub(crate) mod stream; /// Protocol version 1.0 pub const PROTOCOL_1_0: Protocol = Protocol(0x0100); diff --git a/hislip/src/common/stream.rs b/hislip/src/common/stream.rs new file mode 100644 index 0000000..c9a8ccf --- /dev/null +++ b/hislip/src/common/stream.rs @@ -0,0 +1,108 @@ +use std::pin::Pin; + +use futures::io::{AsyncRead, AsyncWrite}; + +pub(crate) enum HislipStream { + Insecure(IO), + #[cfg(feature = "secure-capability")] + Secure(async_rustls::server::TlsStream), +} + +impl HislipStream { + pub(crate) fn new(io: IO) -> Self { + Self::Insecure(io) + } +} + +impl HislipStream +where + IO: AsyncRead + AsyncWrite + Unpin, +{ + #[cfg(feature = "secure-capability")] + pub(crate) async fn start_tls( + self, + acceptor: &mut async_rustls::TlsAcceptor, + ) -> Result { + match self { + HislipStream::Insecure(io) => { + match acceptor.accept(io).into_failable().await { + // Success + Ok(tls) => Ok(Self::Secure(tls)), + // Failed to switch to TLS + Err((err, io)) => Err((err, Self::Insecure(io))), + } + }, + HislipStream::Secure(_) => Err((std::io::ErrorKind::Other.into(), self)), + } + } + + #[cfg(feature = "secure-capability")] + pub(crate) async fn end_tls(self) -> std::io::Result { + match self { + HislipStream::Insecure(_) => Err(std::io::ErrorKind::Other.into()), + HislipStream::Secure(mut _tls) => { + let (_io, _session) = _tls.get_mut(); + todo!("Implement end_tls when async-rustls is updated") + } + } + } +} + +impl AsyncRead for HislipStream +where + IO: AsyncRead + AsyncWrite + Unpin, +{ + fn poll_read( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &mut [u8], + ) -> std::task::Poll> { + match self.get_mut() { + HislipStream::Insecure(io) => Pin::new(io).poll_read(cx, buf), + #[cfg(feature = "secure-capability")] + HislipStream::Secure(tls) => Pin::new(tls).poll_read(cx, buf), + } + } +} + +impl AsyncWrite for HislipStream +where + IO: AsyncRead + AsyncWrite + Unpin, +{ + #[inline] + fn poll_write( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &[u8], + ) -> std::task::Poll> { + match self.get_mut() { + HislipStream::Insecure(io) => Pin::new(io).poll_write(cx, buf), + #[cfg(feature = "secure-capability")] + HislipStream::Secure(tls) => Pin::new(tls).poll_write(cx, buf), + } + } + + #[inline] + fn poll_flush( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + match self.get_mut() { + HislipStream::Insecure(io) => Pin::new(io).poll_flush(cx), + #[cfg(feature = "secure-capability")] + HislipStream::Secure(tls) => Pin::new(tls).poll_flush(cx), + } + } + + #[inline] + fn poll_close( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + match self.get_mut() { + HislipStream::Insecure(io) => Pin::new(io).poll_close(cx), + #[cfg(feature = "secure-capability")] + HislipStream::Secure(tls) => Pin::new(tls).poll_close(cx), + } + } +} diff --git a/hislip/src/server/mod.rs b/hislip/src/server/mod.rs index c2716f5..c287aa0 100644 --- a/hislip/src/server/mod.rs +++ b/hislip/src/server/mod.rs @@ -15,6 +15,7 @@ use lxi_device::Device; use crate::common::errors::{Error, FatalErrorCode, NonFatalErrorCode}; use crate::common::messages::{prelude::*, send_fatal, send_nonfatal}; +use crate::common::stream::HislipStream; use crate::common::{Protocol, SUPPORTED_PROTOCOL}; use crate::server::session::{SessionState, SharedSession}; use crate::DEFAULT_DEVICE_SUBADRESS; @@ -199,6 +200,7 @@ where S: AsyncRead + AsyncWrite + Unpin, SRQ: Stream + Unpin, { + let mut stream = HislipStream::new(stream); loop { match Message::read_from(&mut stream, self.config.max_message_size).await? { Ok(msg) => { @@ -244,8 +246,7 @@ where payload, } => { // Uppgrade connection - - // Start session + //stream = stream.start_tls(acceptor).await?; } Message { message_type: MessageType::Initialize, diff --git a/hislip/src/server/session/asynchronous.rs b/hislip/src/server/session/asynchronous.rs index 3b23d4e..69b0611 100644 --- a/hislip/src/server/session/asynchronous.rs +++ b/hislip/src/server/session/asynchronous.rs @@ -7,7 +7,7 @@ use async_std::future; use async_std::prelude::StreamExt; use async_std::sync::Arc; use byteorder::{ByteOrder, NetworkEndian}; -use futures::future::Either; +use futures::future::{select, Either}; use futures::lock::Mutex; use futures::{pin_mut, AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, FutureExt, Stream}; use lxi_device::lock::{LockHandle, SharedLockError, SharedLockMode, SpinMutex}; @@ -15,6 +15,7 @@ use lxi_device::{Device, DeviceError}; use crate::common::errors::{Error, FatalErrorCode, NonFatalErrorCode}; use crate::common::messages::{prelude::*, send_fatal, send_nonfatal}; +use crate::common::stream::HislipStream; use crate::common::{Protocol, PROTOCOL_2_0}; use super::{ServerConfig, SharedSession}; @@ -60,7 +61,7 @@ where pub(crate) async fn handle_session( self, - stream: S, + mut stream: HislipStream, peer: String, mut srq: SRQ, protocol: Protocol, @@ -69,38 +70,44 @@ where S: AsyncRead + AsyncWrite + Unpin, SRQ: Stream + Unpin, { - let (mut rd, mut wr) = stream.split(); + //let (mut rd, mut wr) = stream.split(); let mut srq_bit = false; loop { - let read_msg = Message::read_from(&mut rd, self.config.max_message_size).fuse(); - pin_mut!(read_msg); - - let t = match futures::future::select(read_msg, srq.next()).await { - // Message was received - Either::Left((msg, _)) => msg, - // Status changed - Either::Right((stb, read_msg)) => { - // Send SRQ - match stb { - Some(val) if !srq_bit => { - srq_bit = true; - MessageType::AsyncServiceRequest - .message_params(val, 0) - .write_to(&mut wr) - .await? - } - _ => { - send_fatal!(peer=peer.to_string(), session_id=self.id; - &mut wr, FatalErrorCode::UnidentifiedError, - "Server shutdown", - ); + // Read a message + let t = { + let (mut rd, mut wr) = stream.split(); + let read_msg = Box::pin(Message::read_from(&mut rd, self.config.max_message_size)); + let msg = match select(read_msg, srq.next()).await { + Either::Left((msg, _)) => msg, + Either::Right((stb, msg)) => { + match stb { + // Statusbyte has changed + Some(stb) => { + if !srq_bit { + MessageType::AsyncServiceRequest + .message_params(stb as u8, 0) + .no_payload() + .write_to(&mut wr) + .await?; + srq_bit = true; + } + }, + // Srq is closed, server is shutting down + None => { + log::info!(peer=peer.to_string(), session_id=self.id; "Server shutting down..."); + return Ok(()) + }, } + // Finish receiving message + // This is important as dropping the future mid-message can corrupt the datastream + msg.await } - // Finish receiving message - read_msg.await - } - }?; + }; + stream = rd.reunite(wr).unwrap(); + + msg? + }; match t { Ok(msg) => { @@ -110,7 +117,7 @@ where .. } => { send_nonfatal!(peer=peer.to_string(), session_id=self.id; - &mut wr, NonFatalErrorCode::UnrecognizedVendorDefinedMessage, + &mut stream, NonFatalErrorCode::UnrecognizedVendorDefinedMessage, "Unrecognized Vendor Defined Message ({})", code ); } @@ -158,7 +165,7 @@ where MessageType::AsyncLockResponse .message_params(control as u8, 0) .no_payload() - .write_to(&mut wr) + .write_to(&mut stream) .await?; } else { // Lock @@ -205,7 +212,7 @@ where MessageType::AsyncLockResponse .message_params(control as u8, 0) .no_payload() - .write_to(&mut wr) + .write_to(&mut stream) .await?; } } @@ -273,17 +280,17 @@ where MessageType::AsyncRemoteLocalResponse .message_params(0, 0) .no_payload() - .write_to(&mut wr) + .write_to(&mut stream) .await? } Err(DeviceError::NotSupported) => { - send_nonfatal!(peer=peer.to_string(), session_id=self.id; &mut wr, + send_nonfatal!(peer=peer.to_string(), session_id=self.id; &mut stream, NonFatalErrorCode::UnrecognizedControlCode, "Unrecognized control code", ); } Err(_) => { - send_nonfatal!(peer=peer.to_string(), session_id=self.id; &mut wr, + send_nonfatal!(peer=peer.to_string(), session_id=self.id; &mut stream, NonFatalErrorCode::UnidentifiedError, "Internal error", ); @@ -297,7 +304,7 @@ where } => { if payload.len() != 8 { send_fatal!(peer=peer.to_string(), session_id=self.id; - &mut wr, FatalErrorCode::PoorlyFormattedMessageHeader, + &mut stream, FatalErrorCode::PoorlyFormattedMessageHeader, "Expected 8 bytes in AsyncMaximumMessageSize payload" ) } @@ -316,7 +323,7 @@ where MessageType::AsyncMaximumMessageSizeResponse .message_params(0, 0) .with_payload(buf.to_vec()) - .write_to(&mut wr) + .write_to(&mut stream) .await?; } Message { @@ -338,7 +345,7 @@ where MessageType::AsyncDeviceClearAcknowledge .message_params(features.0, 0) .no_payload() - .write_to(&mut wr) + .write_to(&mut stream) .await?; } Message { @@ -372,7 +379,7 @@ where MessageType::AsyncStatusResponse .message_params(stb, 0) .no_payload() - .write_to(&mut wr) + .write_to(&mut stream) .await?; } Message { @@ -389,7 +396,7 @@ where MessageType::AsyncLockInfoResponse .message_params(exclusive.into(), num_shared) .no_payload() - .write_to(&mut wr) + .write_to(&mut stream) .await?; } Message { @@ -397,10 +404,10 @@ where control_code, message_parameter, payload, - } if protocol >= PROTOCOL_2_0 => { + } if protocol >= PROTOCOL_2_0 && cfg!(feature = "secure-capability") => { if payload.len() != 4 { send_fatal!(peer=peer.to_string(), session_id=self.id; - &mut wr, FatalErrorCode::PoorlyFormattedMessageHeader, + &mut stream, FatalErrorCode::PoorlyFormattedMessageHeader, "Expected 4 bytes in AsyncStartTLS payload" ) } @@ -412,8 +419,9 @@ where log::debug!(session_id=self.id, message_id_sent=message_id_sent, message_id_read=message_id_read; "Start async TLS"); // TODO: Encryption support + //stream = stream.start_tls(acceptor)?; send_fatal!( - &mut wr, + &mut stream, FatalErrorCode::SecureConnectionFailed, "Secure connection not supported" ) @@ -434,13 +442,13 @@ where // TODO: Encryption support send_fatal!( - &mut wr, + &mut stream, FatalErrorCode::SecureConnectionFailed, "Secure connection not supported" ) } _ => { - send_nonfatal!(peer=peer.to_string(), session_id=self.id; &mut wr, + send_nonfatal!(peer=peer.to_string(), session_id=self.id; &mut stream, NonFatalErrorCode::UnrecognizedMessageType, "Unexpected message type in asynchronous channel", ); @@ -450,10 +458,10 @@ where Err(err) => { // Send error to client and close if fatal if err.is_fatal() { - Message::from(err).write_to(&mut wr).await?; + Message::from(err).write_to(&mut stream).await?; break Err(io::ErrorKind::Other.into()); } else { - Message::from(err).write_to(&mut wr).await?; + Message::from(err).write_to(&mut stream).await?; } } } diff --git a/hislip/src/server/session/mod.rs b/hislip/src/server/session/mod.rs index afba926..d4c4438 100644 --- a/hislip/src/server/session/mod.rs +++ b/hislip/src/server/session/mod.rs @@ -1,4 +1,7 @@ use async_std::channel::{self, Receiver, Sender}; +use lxi_device::Device; + +use self::{asynchronous::AsyncSession, synchronous::SyncSession}; use super::ServerConfig; use crate::common::Protocol; @@ -6,6 +9,14 @@ use crate::common::Protocol; pub(crate) mod asynchronous; pub(crate) mod synchronous; +macro_rules! assert_session_state { + ($state:expr, $expected:pat) => { + if !matches!($state, $expected) { + + } + } +} + #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum SessionMode { Synchronized, @@ -22,7 +33,7 @@ pub(crate) struct SharedSession { /// Negotiated rpc protocol: Protocol, - /// Current tate of session + /// Current state of session state: SessionState, /// Negotiated session mode @@ -88,3 +99,9 @@ impl SharedSession { self.clear.0.clone() } } + +enum Session { + Async(AsyncSession), + Sync(SyncSession), + Uninitialized +} \ No newline at end of file diff --git a/raw/Cargo.toml b/raw/Cargo.toml index ce18d68..a3266bc 100644 --- a/raw/Cargo.toml +++ b/raw/Cargo.toml @@ -14,13 +14,17 @@ async-std = { workspace = true } async-listen = { workspace = true } futures = { workspace = true } log = { workspace = true, features = ["kv_unstable_std"] } +async-rustls = { workspace = true, optional = true } [dependencies.lxi-device] path = "../device" version = "0.1.0" [dev-dependencies] -femme = { workspace = true } +femme = { workspace = true } clap = { workspace = true } mio-serial = "5.0" -async-io = "1.9.0" \ No newline at end of file +async-io = "1.9.0" + +[features] +tls = ["dep:async-rustls"] diff --git a/raw/examples/raw.rs b/raw/examples/scpi-raw.rs similarity index 100% rename from raw/examples/raw.rs rename to raw/examples/scpi-raw.rs diff --git a/raw/examples/scpi-tls.rs b/raw/examples/scpi-tls.rs new file mode 100644 index 0000000..0116f2a --- /dev/null +++ b/raw/examples/scpi-tls.rs @@ -0,0 +1,123 @@ +use std::{ + fs::File, + io::{self, BufReader}, + sync::Arc, + time::Duration, +}; + +use async_std::io::timeout; +use lxi_device::{lock::SharedLock, util::SimpleDevice}; +use lxi_socket::{server::ServerConfig, SOCKET_STANDARD_PORT}; + +use clap::Parser; + +use async_rustls::{ + rustls::{ + internal::pemfile::{certs, rsa_private_keys, pkcs8_private_keys}, + Certificate, NoClientAuth, AllowAnyAuthenticatedClient, PrivateKey, ServerConfig as TlsConfig, + RootCertStore, AllowAnyAnonymousOrAuthenticatedClient + }, + TlsAcceptor, +}; + +/// Simple program to greet a person +#[derive(Parser, Debug)] +#[clap(author, version, about, long_about = None)] +struct Args { + #[clap(default_value = "0.0.0.0")] + ip: String, + + /// Number of times to greet + #[clap(short, long, default_value_t = SOCKET_STANDARD_PORT)] + port: u16, + + /// Kill server after timeout (useful for coverage testing) + #[clap(short, long)] + timeout: Option, + + /// TLS certificate + #[clap(short, long, default_value = ".certificates/cert.pem")] + cert: String, + + /// TLS key + #[clap(short, long, default_value = ".certificates/key.pem")] + key: String, + + #[clap(long)] + client_cert: Vec, + + #[clap(long)] + require_authentication: bool, +} + +/// Load the passed certificates file +fn load_certs(path: &str) -> io::Result> { + certs(&mut BufReader::new(File::open(path)?)) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid cert")) +} + +/// Load the passed keys file +fn load_keys(path: &str) -> io::Result> { + pkcs8_private_keys(&mut BufReader::new(File::open(path)?)) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid key")) +} + +/// Configure the server using rusttls +/// See https://docs.rs/rustls/0.16.0/rustls/struct.ServerConfig.html for details +/// +/// A TLS server needs a certificate and a fitting private key +fn load_config(options: &Args) -> io::Result { + let certs = load_certs(&options.cert)?; + let mut keys = load_keys(&options.key)?; + + let mut config = if !options.client_cert.is_empty() { + let mut store = RootCertStore::empty(); + for path in &options.client_cert { + let mut reader = BufReader::new(File::open(path)?); + store.add_pem_file(&mut reader).expect("Failed to load client certificate"); + } + if options.require_authentication { + TlsConfig::new(AllowAnyAuthenticatedClient::new(store)) + } else { + TlsConfig::new(AllowAnyAnonymousOrAuthenticatedClient::new(store)) + + } + } else { + if options.require_authentication { + log::error!("Client authentication required but no certificates were provided") + } + TlsConfig::new(NoClientAuth::new()) + }; + + config + // set this server to use one cert together with the loaded private key + .set_single_cert(certs, keys.remove(0)) + .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?; + + Ok(config) +} + +#[async_std::main] +async fn main() -> std::io::Result<()> { + femme::with_level(log::LevelFilter::Debug); + let args = Args::parse(); + + let device = SimpleDevice::new_arc(); + let shared_lock = SharedLock::new(); + + // TLS + let config = load_config(&args)?; + let acceptor = TlsAcceptor::from(Arc::new(config)); + + let ipv4_server = ServerConfig::default() + .read_buffer(16 * 1024) + .build() + .accept_tls((&args.ip[..], args.port), shared_lock, device, acceptor); + + log::info!("Running server on port {}:{}...", args.ip, args.port); + if let Some(t) = args.timeout { + timeout(Duration::from_millis(t), ipv4_server).await + } else { + ipv4_server.await + } +} diff --git a/raw/src/lib.rs b/raw/src/lib.rs index 1e805d4..f609c82 100644 --- a/raw/src/lib.rs +++ b/raw/src/lib.rs @@ -2,4 +2,9 @@ pub mod server; pub mod common {} +/// Standard port for raw SCPI socket communication pub const SOCKET_STANDARD_PORT: u16 = 5025; +/// Our standard port for secure raw communication. +/// **This is not a LXI standard port, just ours!** +pub const TLS_PORT: u16 = 6025; + diff --git a/raw/src/server/mod.rs b/raw/src/server/mod.rs index da13cc6..4c15ed6 100644 --- a/raw/src/server/mod.rs +++ b/raw/src/server/mod.rs @@ -21,6 +21,9 @@ use lxi_device::{ #[cfg(unix)] use async_std::os::unix::net::UnixListener; +#[cfg(feature = "tls")] +pub mod tls; + pub struct Server(ServerConfig); impl Server { @@ -65,6 +68,57 @@ impl Server { Ok(()) } + /// Listen to a socket for clients with a TLS acceptor + #[cfg(feature = "tls")] + pub async fn accept_tls( + self: Arc, + addr: impl ToSocketAddrs, + shared_lock: Arc>, + device: Arc>, + acceptor: async_rustls::TlsAcceptor, + ) -> io::Result<()> + where + DEV: Device + Send + 'static, + { + let listener = TcpListener::bind(addr).await?; + let mut incoming = listener + .incoming() + .log_warnings(|warn| log::warn!("Listening error: {}", warn)) + .handle_errors(Duration::from_millis(100)) + .backpressure(self.0.limit); + + while let Some((token, stream)) = incoming.next().await { + let s = self.clone(); + let peer = stream.peer_addr()?; + log::error!("Accepted from: {}", peer); + + let shared_lock = shared_lock.clone(); + let device = device.clone(); + let acceptor = acceptor.clone(); + + stream.set_nodelay(true)?; + + task::spawn(async move { + match acceptor.accept(stream).await { + Ok(stream) => { + let (reader, writer) = stream.split(); + if let Err(err) = s + .process_client(reader, writer, shared_lock, device, peer) + .await + { + log::warn!("Error processing client: {}", err) + } + } + Err(err) => { + log::warn!("TLS handshake failed: {err}") + }, + } + drop(token); + }); + } + Ok(()) + } + /// Listen to a unix socket for client #[cfg(unix)] pub async fn accept_unix( diff --git a/raw/src/server/tls.rs b/raw/src/server/tls.rs new file mode 100644 index 0000000..482f914 --- /dev/null +++ b/raw/src/server/tls.rs @@ -0,0 +1,13 @@ + + + +struct TlsServerConfig { + +} + + + + + + + From c2b967891cd2407c3ad99b94eebb0f0a75f977f7 Mon Sep 17 00:00:00 2001 From: Gustav Palmqvist Date: Wed, 2 Nov 2022 00:17:18 +0100 Subject: [PATCH 3/5] Refactor --- hislip/examples/hislip.rs | 6 +- hislip/src/server/config.rs | 48 ++++++++++ hislip/src/server/mod.rs | 109 ++-------------------- hislip/src/server/session/asynchronous.rs | 8 +- hislip/src/server/session/mod.rs | 49 +++++++--- hislip/src/server/session/synchronous.rs | 17 +--- hislip/tests/test_hislip_v2.py | 23 +++++ raw/examples/scpi-tls.rs | 24 +++-- 8 files changed, 140 insertions(+), 144 deletions(-) create mode 100644 hislip/src/server/config.rs create mode 100644 hislip/tests/test_hislip_v2.py diff --git a/hislip/examples/hislip.rs b/hislip/examples/hislip.rs index da9ef8d..14581b0 100644 --- a/hislip/examples/hislip.rs +++ b/hislip/examples/hislip.rs @@ -12,7 +12,7 @@ use lxi_device::{ Device, }; use lxi_hislip::{ - server::{ServerBuilder, ServerConfig}, + server::{config::ServerConfig, ServerBuilder}, STANDARD_PORT, }; @@ -69,9 +69,7 @@ async fn main() -> Result<(), io::Error> { let shared_lock1 = SharedLock::new(); let device1: Arc>> = Arc::new(Mutex::new(Box::new(EchoDevice))); - let config = ServerConfig::default() - .vendor_id(0x1234) - .short_idn(b"Vendor,Model,Serial,Version"); + let config = ServerConfig::default().vendor_id(0x1234); let server = ServerBuilder::new(config) .device("hislip0".to_string(), device0, shared_lock0) .device("hislip1".to_string(), device1, shared_lock1) diff --git a/hislip/src/server/config.rs b/hislip/src/server/config.rs new file mode 100644 index 0000000..79418c3 --- /dev/null +++ b/hislip/src/server/config.rs @@ -0,0 +1,48 @@ +#[derive(Debug, Clone)] +pub struct ServerConfig { + pub vendor_id: u16, + /// Maximum server message size + pub max_message_size: u64, + /// Prefer overlapped data + pub prefer_overlap: bool, + /// Maximum allowed number of sessions + pub max_num_sessions: usize, +} + +impl ServerConfig { + pub fn vendor_id(mut self, vendor_id: u16) -> Self { + self.vendor_id = vendor_id; + self + } + + pub fn max_message_size(mut self, max_message_size: u64) -> Self { + self.max_message_size = max_message_size; + self + } + + pub fn max_num_sessions(mut self, max_num_sessions: usize) -> Self { + self.max_num_sessions = max_num_sessions; + self + } + + pub fn prefer_overlap(mut self) -> Self { + self.prefer_overlap = true; + self + } + + pub fn prefer_synchronized(mut self) -> Self { + self.prefer_overlap = false; + self + } +} + +impl Default for ServerConfig { + fn default() -> Self { + Self { + vendor_id: 0xBEEF, + max_message_size: 1024 * 1024, + prefer_overlap: true, + max_num_sessions: 64, + } + } +} \ No newline at end of file diff --git a/hislip/src/server/mod.rs b/hislip/src/server/mod.rs index c287aa0..bd0c0f9 100644 --- a/hislip/src/server/mod.rs +++ b/hislip/src/server/mod.rs @@ -2,7 +2,6 @@ use std::cmp::min; use std::collections::HashMap; use std::io; use std::str::from_utf8; -use std::sync::Weak; use async_std::net::{TcpListener, ToSocketAddrs}; use async_std::sync::Arc; @@ -20,65 +19,11 @@ use crate::common::{Protocol, SUPPORTED_PROTOCOL}; use crate::server::session::{SessionState, SharedSession}; use crate::DEFAULT_DEVICE_SUBADRESS; -pub mod session; - -#[derive(Debug, Clone)] -pub struct ServerConfig { - pub vendor_id: u16, - /// Maximum server message size - pub max_message_size: u64, - /// Prefer overlapped data - pub prefer_overlap: bool, - /// Maximum allowed number of sessions - pub max_num_sessions: usize, - /// Short circuited "*IDN?" response. - /// This should be set identical to what a real "*IDN?" command would return. - pub short_idn: Option>, -} +pub use self::config::ServerConfig; +use self::session::SessionHandle; -impl ServerConfig { - pub fn vendor_id(mut self, vendor_id: u16) -> Self { - self.vendor_id = vendor_id; - self - } - - pub fn max_message_size(mut self, max_message_size: u64) -> Self { - self.max_message_size = max_message_size; - self - } - - pub fn short_idn(mut self, short_idn: &[u8]) -> Self { - self.short_idn = Some(short_idn.to_vec()); - self - } - - pub fn max_num_sessions(mut self, max_num_sessions: usize) -> Self { - self.max_num_sessions = max_num_sessions; - self - } - - pub fn prefer_overlap(mut self) -> Self { - self.prefer_overlap = true; - self - } - - pub fn prefer_synchronized(mut self) -> Self { - self.prefer_overlap = false; - self - } -} - -impl Default for ServerConfig { - fn default() -> Self { - Self { - vendor_id: 0xBEEF, - max_message_size: 1024 * 1024, - prefer_overlap: true, - max_num_sessions: 64, - short_idn: None, - } - } -} +pub mod session; +pub mod config; type DeviceMap = HashMap>, Arc>)>; @@ -193,7 +138,7 @@ where async fn handle_session( &self, peer: String, - mut stream: S, + stream: S, srq: SRQ, ) -> Result<(), io::Error> where @@ -239,15 +184,6 @@ where from_utf8(&payload).unwrap_or("") ); } - Message { - message_type: MessageType::StartTLS, - control_code, - message_parameter, - payload, - } => { - // Uppgrade connection - //stream = stream.start_tls(acceptor).await?; - } Message { message_type: MessageType::Initialize, message_parameter, @@ -373,7 +309,7 @@ where MessageType::AsyncInitializeResponse .message_params( - AsyncInitializeResponseControl::new(false).0, + AsyncInitializeResponseControl::new(true).0, AsyncInitializeResponseParameter::new( self.config.vendor_id, ) @@ -419,39 +355,6 @@ where } } -/// A handle to a created active season -#[derive(Clone)] -pub(crate) struct SessionHandle -where - DEV: Device, -{ - _id: u16, - shared: Weak>, - device: Weak>>, -} - -impl SessionHandle -where - DEV: Device, -{ - fn new( - id: u16, - session: Weak>, - handle: Weak>>, - ) -> Self { - Self { - _id: id, - shared: session, - device: handle, - } - } - - /// Return false if the assosciated object have been closed - fn active(&self) -> bool { - self.shared.strong_count() > 0 && self.device.strong_count() > 0 - } -} - struct InnerServer where DEV: Device, diff --git a/hislip/src/server/session/asynchronous.rs b/hislip/src/server/session/asynchronous.rs index 69b0611..ae66458 100644 --- a/hislip/src/server/session/asynchronous.rs +++ b/hislip/src/server/session/asynchronous.rs @@ -9,7 +9,7 @@ use async_std::sync::Arc; use byteorder::{ByteOrder, NetworkEndian}; use futures::future::{select, Either}; use futures::lock::Mutex; -use futures::{pin_mut, AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, FutureExt, Stream}; +use futures::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, Stream}; use lxi_device::lock::{LockHandle, SharedLockError, SharedLockMode, SpinMutex}; use lxi_device::{Device, DeviceError}; @@ -447,10 +447,12 @@ where "Secure connection not supported" ) } - _ => { + Message { + message_type, .. + } => { send_nonfatal!(peer=peer.to_string(), session_id=self.id; &mut stream, NonFatalErrorCode::UnrecognizedMessageType, - "Unexpected message type in asynchronous channel", + "Unexpected {message_type:?} in asynchronous channel", ); } } diff --git a/hislip/src/server/session/mod.rs b/hislip/src/server/session/mod.rs index d4c4438..911aa75 100644 --- a/hislip/src/server/session/mod.rs +++ b/hislip/src/server/session/mod.rs @@ -1,7 +1,7 @@ -use async_std::channel::{self, Receiver, Sender}; -use lxi_device::Device; +use std::sync::Weak; -use self::{asynchronous::AsyncSession, synchronous::SyncSession}; +use async_std::{channel::{self, Receiver, Sender}}; +use lxi_device::{Device, lock::{Mutex, SpinMutex, LockHandle}}; use super::ServerConfig; use crate::common::Protocol; @@ -9,14 +9,6 @@ use crate::common::Protocol; pub(crate) mod asynchronous; pub(crate) mod synchronous; -macro_rules! assert_session_state { - ($state:expr, $expected:pat) => { - if !matches!($state, $expected) { - - } - } -} - #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum SessionMode { Synchronized, @@ -100,8 +92,35 @@ impl SharedSession { } } -enum Session { - Async(AsyncSession), - Sync(SyncSession), - Uninitialized +/// A handle to a created active season +#[derive(Clone)] +pub(crate) struct SessionHandle +where + DEV: Device, +{ + _id: u16, + pub shared: Weak>, + pub device: Weak>>, +} + +impl SessionHandle +where + DEV: Device, +{ + pub(crate) fn new( + id: u16, + session: Weak>, + handle: Weak>>, + ) -> Self { + Self { + _id: id, + shared: session, + device: handle, + } + } + + /// Return false if the assosciated object have been closed + pub(crate) fn active(&self) -> bool { + self.shared.strong_count() > 0 && self.device.strong_count() > 0 + } } \ No newline at end of file diff --git a/hislip/src/server/session/synchronous.rs b/hislip/src/server/session/synchronous.rs index 691b2de..a45857a 100644 --- a/hislip/src/server/session/synchronous.rs +++ b/hislip/src/server/session/synchronous.rs @@ -34,7 +34,7 @@ where clear: Receiver<()>, - protocol: Protocol + protocol: Protocol, } impl SyncSession @@ -47,7 +47,7 @@ where shared: Arc>, handle: RemoteLockHandle, clear: Receiver<()>, - protocol: Protocol + protocol: Protocol, ) -> Self { Self { id, @@ -55,7 +55,7 @@ where shared, handle, clear, - protocol + protocol, } } @@ -234,15 +234,8 @@ where if is_end { log::debug!(peer=peer.to_string(), session_id=self.id, message_id=message_id; "Data END, {}", control); - let data = if buffer.eq_ignore_ascii_case(b"*idn?") - && self.config.short_idn.is_some() - { - self.config.short_idn.clone() - } else { - let data = dev.execute(&buffer); - buffer.clear(); - data - }; + let data = dev.execute(&buffer); + buffer.clear(); // Send back response if let Some(data) = data { diff --git a/hislip/tests/test_hislip_v2.py b/hislip/tests/test_hislip_v2.py new file mode 100644 index 0000000..0344b71 --- /dev/null +++ b/hislip/tests/test_hislip_v2.py @@ -0,0 +1,23 @@ +from pyvisa import highlevel + + +for backend in highlevel.list_backends(): + if backend.startswith("pyvisa-"): + backend = backend[7:] + + try: + cls = highlevel.get_wrapper_class(backend) + except Exception as e: + backend_details[backend] = [ + "Could not instantiate backend", + "-> %s" % str(e), + ] + continue + + try: + backend_details[backend] = cls.get_debug_info() + except Exception as e: + backend_details[backend] = [ + "Could not obtain debug info", + "-> %s" % str(e), + ] \ No newline at end of file diff --git a/raw/examples/scpi-tls.rs b/raw/examples/scpi-tls.rs index 0116f2a..37539ba 100644 --- a/raw/examples/scpi-tls.rs +++ b/raw/examples/scpi-tls.rs @@ -13,9 +13,9 @@ use clap::Parser; use async_rustls::{ rustls::{ - internal::pemfile::{certs, rsa_private_keys, pkcs8_private_keys}, - Certificate, NoClientAuth, AllowAnyAuthenticatedClient, PrivateKey, ServerConfig as TlsConfig, - RootCertStore, AllowAnyAnonymousOrAuthenticatedClient + internal::pemfile::{certs, pkcs8_private_keys, rsa_private_keys}, + AllowAnyAnonymousOrAuthenticatedClient, AllowAnyAuthenticatedClient, Certificate, + NoClientAuth, PrivateKey, RootCertStore, ServerConfig as TlsConfig, }, TlsAcceptor, }; @@ -58,8 +58,17 @@ fn load_certs(path: &str) -> io::Result> { /// Load the passed keys file fn load_keys(path: &str) -> io::Result> { - pkcs8_private_keys(&mut BufReader::new(File::open(path)?)) - .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid key")) + // Try to load RSA key + match rsa_private_keys(&mut BufReader::new(File::open(path)?)) { + Ok(keys) => Ok(keys), + // Try PKCS#8 if not RSA + Err(_) => match pkcs8_private_keys(&mut BufReader::new(File::open(path)?)) { + Ok(keys) => Ok(keys), + Err(_) => { + return Err(io::Error::new(io::ErrorKind::InvalidInput, "Invalid key, expected RSA or PKCS#8 in PEM format")) + }, + }, + } } /// Configure the server using rusttls @@ -74,13 +83,14 @@ fn load_config(options: &Args) -> io::Result { let mut store = RootCertStore::empty(); for path in &options.client_cert { let mut reader = BufReader::new(File::open(path)?); - store.add_pem_file(&mut reader).expect("Failed to load client certificate"); + store + .add_pem_file(&mut reader) + .expect("Failed to load client certificate"); } if options.require_authentication { TlsConfig::new(AllowAnyAuthenticatedClient::new(store)) } else { TlsConfig::new(AllowAnyAnonymousOrAuthenticatedClient::new(store)) - } } else { if options.require_authentication { From 83ce4095300d236ade19bdff44ba29af159427c3 Mon Sep 17 00:00:00 2001 From: Gustav Palmqvist Date: Wed, 2 Nov 2022 00:43:42 +0100 Subject: [PATCH 4/5] Squashed commit of the following: commit 447fa343e22b90d44a6893e4b7645275dc6b5421 Merge: c2b9678 654ccf5 Author: Gustav Palmqvist Date: Wed Nov 2 00:30:33 2022 +0100 Merge remote-tracking branch 'origin/webserver' into security commit c2b967891cd2407c3ad99b94eebb0f0a75f977f7 Author: Gustav Palmqvist Date: Wed Nov 2 00:17:18 2022 +0100 Refactor commit 2da185fd7d0c77cc377e2683803e05a82f8bb9f8 Author: Gustav Palmqvist Date: Tue Nov 1 17:59:34 2022 +0100 Some stuff commit 4438579e68b5a534dad909be0579a8d391c47120 Author: Gustav Palmqvist Date: Mon Oct 31 23:18:27 2022 +0100 Secure capability commit 654ccf5646b80560a4622e3e8908d80b849e561d Author: Gustav Palmqvist Date: Mon Oct 31 23:19:28 2022 +0100 Certificate stuff commit 02aaebf8bd5d6ca93e4c087ec7ea67faccf2b6c8 Author: Gustav Palmqvist Date: Mon Oct 31 23:18:27 2022 +0100 Secure capability commit 5780567434197f470de81839f49b3f2d9085666e Author: Gustav Palmqvist Date: Fri Oct 28 21:53:23 2022 +0200 Http(s) server work commit 6290cf6da2f7bdab74fd0288a7caf2af0b2ee314 Author: Gustav Palmqvist Date: Sun Oct 23 16:58:11 2022 +0200 Stuff commit f96fe2b2e8b51ed95eb66ca30c17762f82598900 Author: Gustav Palmqvist Date: Sun Oct 23 16:46:47 2022 +0200 Stuff commit 684989c3e4a25e3b3c64543b8d9c859f997439d4 Author: Gustav Palmqvist Date: Sun Oct 23 00:00:10 2022 +0200 Started work on a http server commit 25a2404978e70fe2fd9c722ad5a89e54bd014baa Author: Gustav Palmqvist Date: Sat Oct 22 23:53:52 2022 +0200 Removed IDN short-circuit from HiSLIP protocol commit 4f1bf5a0d7ba2303d6dd391ae215b040049fdfc8 Author: Gustav Palmqvist Date: Sat Oct 22 02:38:46 2022 +0200 Cache pytest cargo invocations seperate from /target. commit 3acb4c71c8dbea89f8d9ef0b48afeb1174e251ce Author: Gustav Palmqvist Date: Fri Oct 21 00:13:49 2022 +0200 Idk commit 8e5e10938f883a5fe4886b04502c4d67de963965 Merge: 672d529 eba0f6d Author: Gustav Palmqvist Date: Thu Oct 20 22:12:52 2022 +0200 Merge remote-tracking branch 'refs/remotes/origin/master' commit 672d5295b6768e75749ad8f05addb73224c5be34 Author: Gustav Palmqvist Date: Thu Oct 20 22:11:35 2022 +0200 Refactor abort/clear code and a little cleanup commit eba0f6d9c691472c229af4ee3265dd647fa43867 Author: Gustav Palmqvist Date: Thu Oct 20 21:48:02 2022 +0200 Short-circuit *idn? commit ab2bddab966f20ca4aabcdd8737934e9685cd234 Author: Gustav Palmqvist Date: Thu Oct 20 21:47:40 2022 +0200 Added serialport example commit 091eab5ebb7c10e8eec28399c5da798c90055596 Author: Gustav Palmqvist Date: Fri Oct 14 00:47:27 2022 +0200 Try to reserve memory before extending buffers commit 3b9310fc29d655447c6a690a20019a386a5414ad Author: Gustav Palmqvist Date: Mon Oct 10 23:57:31 2022 +0200 Support multiple devices for vxi-11 commit 54fc029009abf64628e3c22472377303dad65fad Author: Gustav Palmqvist Date: Mon Oct 10 23:07:35 2022 +0200 Add serialport example commit 6cafcd2f3c7205e97675de1d986423c8e213df1e Author: Gustav Palmqvist Date: Mon Oct 10 23:07:20 2022 +0200 Cleanup commit afd678f50958bc001855e2b081af3446c7c30f06 Author: Gustav Palmqvist Date: Mon Oct 10 20:02:19 2022 +0200 Return an option instead to signify that no data should be sent back. commit 44e9d90e68d4764986b3e06d845a41660f079a25 Author: Gustav Palmqvist Date: Sat Oct 8 00:39:12 2022 +0200 Cleanup --- .github/workflows/python.yml | 3 +- .gitignore | 5 +- .vscode/launch.json | 16 ++ Cargo.toml | 15 +- README.md | 8 + conftest.py | 10 + device/src/lib.rs | 16 +- device/src/lock.rs | 22 +- device/src/status.rs | 14 +- device/src/trigger.rs | 4 +- device/src/util.rs | 28 +- hislip/Cargo.toml | 9 +- hislip/examples/hislip.rs | 18 +- hislip/src/common/descriptors.rs | 74 +++++ hislip/src/common/messages.rs | 16 +- hislip/src/common/mod.rs | 2 + hislip/src/common/stream.rs | 108 ++++++++ hislip/src/server/config.rs | 48 ++++ hislip/src/server/mod.rs | 153 +++-------- hislip/src/server/session/asynchronous.rs | 156 +++++------ hislip/src/server/session/mod.rs | 40 ++- hislip/src/server/session/synchronous.rs | 268 +++++++++--------- hislip/tests/conftest.py | 11 +- hislip/tests/test_hislip.py | 31 ++- hislip/tests/test_hislip_v2.py | 23 ++ raw/Cargo.toml | 13 +- raw/README.md | 5 +- raw/examples/{raw.rs => scpi-raw.rs} | 7 +- raw/examples/scpi-tls.rs | 133 +++++++++ raw/examples/serial.rs | 53 ++++ raw/src/lib.rs | 5 + raw/src/server/mod.rs | 67 ++++- raw/src/server/tls.rs | 13 + raw/tests/conftest.py | 11 +- requirements.txt | 7 +- telnet/Cargo.toml | 6 +- telnet/examples/telnet.rs | 6 +- telnet/src/server/mod.rs | 7 +- telnet/tests/conftest.py | 11 +- vxi11/examples/vxi11.rs | 22 +- vxi11/src/client/portmapper.rs | 1 - vxi11/src/common/onc_rpc/mod.rs | 4 +- vxi11/src/common/onc_rpc/xdr.rs | 2 +- vxi11/src/lib.rs | 2 +- vxi11/src/server/vxi11/abort_service.rs | 6 +- vxi11/src/server/vxi11/core_service.rs | 320 ++++++++++++---------- vxi11/src/server/vxi11/mod.rs | 62 +++-- vxi11/tests/conftest.py | 13 +- vxi11/tests/test_vxi11.py | 18 +- 49 files changed, 1258 insertions(+), 634 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 hislip/src/common/descriptors.rs create mode 100644 hislip/src/common/stream.rs create mode 100644 hislip/src/server/config.rs create mode 100644 hislip/tests/test_hislip_v2.py rename raw/examples/{raw.rs => scpi-raw.rs} (91%) create mode 100644 raw/examples/scpi-tls.rs create mode 100644 raw/examples/serial.rs create mode 100644 raw/src/server/tls.rs diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index a82c01b..92e000b 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -47,9 +47,10 @@ jobs: flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Build - run: cargo build --verbose + run: cargo build --verbose --examples env: RUSTFLAGS: -Awarnings + CARGO_TARGET_DIR: .pytest_cache/d/target # HiSLIP isn't supported by pyvisa-py yet - name: Test with pytest diff --git a/.gitignore b/.gitignore index 21b6580..acb32ed 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,7 @@ __pycache__/ .pytest_cache/ # Coverage -lcov.info \ No newline at end of file +lcov.info + +# Certificates +/.certificates diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..10efcb2 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug", + "program": "${workspaceFolder}/", + "args": [], + "cwd": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 34ebecd..acf7ef5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,5 @@ [workspace] -members = [ - "device", - "hislip", - "raw", - "telnet", - "vxi11" -] +members = ["device", "hislip", "raw", "telnet", "vxi11"] [workspace.package] version = "0.1.0" @@ -15,12 +9,13 @@ edition = "2021" [workspace.dependencies] # Common dependencies -async-std = {version = "1.11", features = ["attributes"]} +async-std = { version = "1.11", features = ["attributes"] } async-listen = "0.2.1" -futures = {version = "0.3" } +futures = { version = "0.3" } log = { version = "0.4.17" } byteorder = { version = "1.4" } +async-rustls = { version = "0.2" } # Dev dependencies femme = "2.2" -clap = { version = "4.0", features = ["derive"] } \ No newline at end of file +clap = { version = "4.0", features = ["derive"] } diff --git a/README.md b/README.md index 5efc426..e0c51d3 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,16 @@ Currently the focus is on implementing HiSLIP/VXI-11/Socket protocols for Unix-l # Scope This crate does not handle command parsing and/or execution, look at [scpi-rs](https://github.com/Atmelfan/scpi-rs)(:crab:) or [libscpi](https://github.com/j123b567/scpi-parser)(C) for that. +# Certificates +Secure extensions and https server requires a certificate and key. + +The simplest method is to use [`mkcert`](https://github.com/FiloSottile/mkcert) to generate one in `.certificates` directory: + +```mkcert -key-file .certificates/key.pem -cert-file .certificates/cert.pem localhost 127.0.0.1 ::1``` + # Examples Each protocol includes an example service, you can try them out with `cargo run --example ` where protocol is either `hislip`,`vxi11`,`raw`, or `telnet`. + Run `cargo run --example -- --help` for help and specific arguments for each protocol. # Testing diff --git a/conftest.py b/conftest.py index 2a7b412..a248aa6 100644 --- a/conftest.py +++ b/conftest.py @@ -1,8 +1,18 @@ +import os +import subprocess import pytest from pyvisa import ResourceManager import socket from contextlib import closing +pytest.fixture(scope='session', autouse=True) +def prep_cargo(db, data): + print("Building...") + return_code = subprocess.call("cargo build --examples", shell=True) + # yield, to let all tests within the scope run + yield + + @pytest.fixture def free_port(request): with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s: diff --git a/device/src/lib.rs b/device/src/lib.rs index e46bfa5..dd6e78d 100644 --- a/device/src/lib.rs +++ b/device/src/lib.rs @@ -1,10 +1,10 @@ //! This crate contains an abstract device trait and common infrastructure used to create //! a LXI device. -//! +//! //! The [Device] trait implements an abstract LXI device capable of receiving and excuting a command and some other common LXI tasks. -//! -//! -//! +//! +//! +//! #![cfg_attr(not(feature = "std"), no_std)] extern crate alloc; @@ -14,10 +14,10 @@ use trigger::Source; #[cfg(feature = "experimental")] pub mod frontpanel; -/// Internal device status/SRQ messaging channel -pub mod status; /// Instrument locking infrastructure pub mod lock; +/// Internal device status/SRQ messaging channel +pub mod status; /// Standard trigger sources pub mod trigger; /// Example/debugging devices @@ -33,7 +33,7 @@ pub enum DeviceError { pub trait Device { /// Execute a arbitrary command - fn execute(&mut self, cmd: &Vec) -> Vec; + fn execute(&mut self, cmd: &[u8]) -> Option>; /// Return a current device status (STB) byte /// Some flags (such as MAV) will be ignored. @@ -62,7 +62,7 @@ pub trait Device { // Blanket proxy implementation for boxed devices impl Device for Box { - fn execute(&mut self, cmd: &Vec) -> Vec { + fn execute(&mut self, cmd: &[u8]) -> Option> { (**self).execute(cmd) } diff --git a/device/src/lock.rs b/device/src/lock.rs index d0c3113..6595319 100644 --- a/device/src/lock.rs +++ b/device/src/lock.rs @@ -90,7 +90,7 @@ impl SharedLock { } /// A handle to a locked resource. -/// +/// /// This will check if the shared lock is available for this handle before locking. pub struct LockHandle { id: u32, @@ -262,7 +262,7 @@ impl LockHandle { } // Current state: Shared lock or both locks (_, Some(key)) => { - if key == &lockstr { + if key == lockstr { shared.num_shared_locks += 1; self.has_shared = true; @@ -342,7 +342,7 @@ impl LockHandle { } /// Check if the shared lock is available and then lock - pub fn try_lock<'a>(&'a self) -> Result, SharedLockError> { + pub fn try_lock(&self) -> Result, SharedLockError> { // Check any active locks self.can_lock()?; // Lock device and return a guard @@ -351,7 +351,7 @@ impl LockHandle { /// Lock device if allowed /// - pub async fn async_lock<'a>(&'a self) -> Result, SharedLockError> { + pub async fn async_lock(&self) -> Result, SharedLockError> { let mut listener = None; loop { @@ -399,7 +399,7 @@ impl LockHandle { /// Lock device without checking shared/exclusive lock /// NOTE: This shuld ony be used for quick actions like reading status etc to avoid locking /// the device for handles holding a legitimate lock. - pub async fn inner_lock<'a>(&'a self) -> MutexGuard<'a, DEV> { + pub async fn inner_lock(&self) -> MutexGuard { self.device.lock().await } @@ -438,13 +438,12 @@ pub struct RemoteLockHandle { impl RemoteLockHandle { pub fn new(handle: Arc>>) -> Self { - let handle = handle.clone(); let device = handle.lock().device.clone(); Self { handle, device } } /// Check if the shared lock is available and then lock - pub async fn try_lock<'a>(&'a self) -> Result, SharedLockError> { + pub async fn try_lock(&self) -> Result, SharedLockError> { // Check any active locks self.can_lock()?; // Lock device and return a guard @@ -453,7 +452,7 @@ impl RemoteLockHandle { /// Wait for device becoming onlocked (or handle acquiring a lock) and available /// - pub async fn async_lock<'a>(&'a self) -> Result, SharedLockError> { + pub async fn async_lock(&self) -> Result, SharedLockError> { let mut listener = None; loop { @@ -512,7 +511,7 @@ impl RemoteLockHandle { /// Lock device without checking shared/exclusive lock /// NOTE: This shuld ony be used for quick actions like reading status etc to avoid locking /// the device for handles holding a legitimate lock. - pub async fn inner_lock<'a>(&'a self) -> MutexGuard<'a, DEV> { + pub async fn inner_lock(&self) -> MutexGuard { self.device.lock().await } @@ -556,10 +555,7 @@ mod tests { use super::{LockHandle, SharedLock, SpinMutex}; use crate::{lock::RemoteLockHandle, util::EchoDevice}; - use async_std::{ - sync::{Arc}, - task::yield_now, - }; + use async_std::{sync::Arc, task::yield_now}; use futures::{join, lock::Mutex}; #[test] diff --git a/device/src/status.rs b/device/src/status.rs index 2ea99bc..da56575 100644 --- a/device/src/status.rs +++ b/device/src/status.rs @@ -1,9 +1,8 @@ -use alloc::{vec::Vec, sync::Arc}; +use alloc::{sync::Arc, vec::Vec}; use futures::channel::mpsc; use spin::Mutex; - -/// A mpmc channel where **ALL** receiver receives the sent message (i.e. a broadcast channel). +/// A mpmc channel where **ALL** receiver receives the sent message (i.e. a broadcast channel). #[derive(Clone)] pub struct Sender { senders: Arc>>>, @@ -11,6 +10,12 @@ pub struct Sender { pub type Receiver = mpsc::Receiver; +impl Default for Sender { + fn default() -> Self { + Self::new() + } +} + impl Sender { pub fn new() -> Self { Self { @@ -54,6 +59,5 @@ mod tests { assert_eq!(receiver2.try_next().unwrap(), Some(1)); assert!(receiver2.try_next().is_err()); - } -} \ No newline at end of file +} diff --git a/device/src/trigger.rs b/device/src/trigger.rs index c2f006f..2d013ff 100644 --- a/device/src/trigger.rs +++ b/device/src/trigger.rs @@ -1,5 +1,3 @@ - - /// Source of a trigger signal #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Source { @@ -34,4 +32,4 @@ pub enum Source { Lan5, Lan6, Lan7, -} \ No newline at end of file +} diff --git a/device/src/util.rs b/device/src/util.rs index 949299f..3d33b07 100644 --- a/device/src/util.rs +++ b/device/src/util.rs @@ -1,7 +1,7 @@ use alloc::{sync::Arc, vec::Vec}; use futures::lock::Mutex; -use crate::{Device, DeviceError, trigger::Source}; +use crate::{trigger::Source, Device, DeviceError}; /// A device that echoes any command sent to it. #[derive(Clone)] @@ -14,8 +14,8 @@ impl EchoDevice { } impl Device for EchoDevice { - fn execute(&mut self, cmd: &Vec) -> Vec { - cmd.clone() + fn execute(&mut self, cmd: &[u8]) -> Option> { + Some(cmd.to_vec()) } fn get_status(&mut self) -> Result { @@ -44,6 +44,12 @@ pub struct SimpleDevice { rmt: bool, } +impl Default for SimpleDevice { + fn default() -> Self { + Self::new() + } +} + impl SimpleDevice { pub fn new() -> Self { Self { @@ -59,22 +65,20 @@ impl SimpleDevice { } impl Device for SimpleDevice { - fn execute(&mut self, cmd: &Vec) -> Vec { + fn execute(&mut self, cmd: &[u8]) -> Option> { log::debug!(">>> {:?}", cmd); - let r = match cmd.as_slice() { + let r = match cmd { x if x.eq_ignore_ascii_case(b"*IDN?") || x.eq_ignore_ascii_case(b"*IDN?\n") => { - b"Cyberdyne systems,T800 Model 101,A9012.C,V2.4".to_vec() - } - x if x.eq_ignore_ascii_case(b"EVENT") || x.eq_ignore_ascii_case(b"EVENT\n") => { - b"".to_vec() + Some(b"Cyberdyne systems,T800 Model 101,A9012.C,V2.4".to_vec()) } + x if x.eq_ignore_ascii_case(b"EVENT") || x.eq_ignore_ascii_case(b"EVENT\n") => None, x if x.eq_ignore_ascii_case(b"QUERY?") || x.eq_ignore_ascii_case(b"QUERY?\n") => { - b"RESPONSE".to_vec() + Some(b"RESPONSE".to_vec()) } _ => { - let mut rev = cmd.clone(); + let mut rev = cmd.to_vec(); rev.reverse(); - rev + Some(rev) } }; log::debug!("<<< {:?}", r); diff --git a/hislip/Cargo.toml b/hislip/Cargo.toml index 67c7379..b0de5cf 100644 --- a/hislip/Cargo.toml +++ b/hislip/Cargo.toml @@ -15,11 +15,16 @@ futures = { workspace = true } byteorder = { workspace = true } log = { workspace = true, features = ["kv_unstable_std"] } bitfield = "0.14" +async-rustls = { workspace = true, optional = true } +cfg-if = "1.0.0" [dependencies.lxi-device] path = "../device" version = "0.1.0" [dev-dependencies] -femme = { workspace = true } -clap = { workspace = true } \ No newline at end of file +femme = { workspace = true } +clap = { workspace = true } + +[features] +secure-capability = ["dep:async-rustls"] diff --git a/hislip/examples/hislip.rs b/hislip/examples/hislip.rs index c1903a1..14581b0 100644 --- a/hislip/examples/hislip.rs +++ b/hislip/examples/hislip.rs @@ -1,9 +1,9 @@ -use std::{ - sync::Arc, - time::Duration, -}; +use std::{sync::Arc, time::Duration}; -use async_std::{io::{self, timeout}, task}; +use async_std::{ + io::{self, timeout}, + task, +}; use futures::{lock::Mutex, task::Spawn}; use lxi_device::{ lock::SharedLock, @@ -11,7 +11,10 @@ use lxi_device::{ util::{EchoDevice, SimpleDevice}, Device, }; -use lxi_hislip::{server::ServerBuilder, STANDARD_PORT}; +use lxi_hislip::{ + server::{config::ServerConfig, ServerBuilder}, + STANDARD_PORT, +}; use clap::Parser; @@ -66,7 +69,8 @@ async fn main() -> Result<(), io::Error> { let shared_lock1 = SharedLock::new(); let device1: Arc>> = Arc::new(Mutex::new(Box::new(EchoDevice))); - let server = ServerBuilder::default() + let config = ServerConfig::default().vendor_id(0x1234); + let server = ServerBuilder::new(config) .device("hislip0".to_string(), device0, shared_lock0) .device("hislip1".to_string(), device1, shared_lock1) .build(); diff --git a/hislip/src/common/descriptors.rs b/hislip/src/common/descriptors.rs new file mode 100644 index 0000000..39b2c2b --- /dev/null +++ b/hislip/src/common/descriptors.rs @@ -0,0 +1,74 @@ +use std::io; +use byteorder::{WriteBytesExt, ReadBytesExt}; + +pub enum Descriptor { + SupportedTlsVersions(Vec), + TlsInformation(Vec), + TlsLastError(Vec), + Reserved(u8, Vec), + VendorSpecific(u8, Vec), +} + +impl Descriptor { + pub fn read_descriptor(reader: &mut R) -> io::Result { + let len = reader.read_u16::()?; + let typ = reader.read_u8()?; + match typ { + 0 => { + let mut buf = Vec::with_capacity(len as usize); + for _ in 0..len { + buf.push(reader.read_u16::()?) + } + Ok(Self::SupportedTlsVersions(buf)) + }, + 1 => { + let mut buf = Vec::with_capacity(len as usize); + reader.read_exact(&mut buf)?; + Ok(Self::TlsInformation(buf)) + }, + 2 => { + let mut buf = Vec::with_capacity(len as usize); + reader.read_exact(&mut buf)?; + Ok(Self::TlsLastError(buf)) + }, + 3..=127 => { + let mut buf = Vec::with_capacity(len as usize); + reader.read_exact(&mut buf)?; + Ok(Self::Reserved(typ, buf)) + } + 128..=255 => { + let mut buf = Vec::with_capacity(len as usize); + reader.read_exact(&mut buf)?; + Ok(Self::VendorSpecific(typ, buf)) + } + } + } + + pub fn write_descriptor(&self, writer: &mut W) -> io::Result<()> { + match self { + Descriptor::SupportedTlsVersions(versions) => { + writer.write_u16::(versions.len() as u16)?; + writer.write_u8(0)?; + for v in versions { + writer.write_u16::(*v)?; + } + } + Descriptor::TlsInformation(info) => { + writer.write_u16::(info.len() as u16)?; + writer.write_u8(1)?; + writer.write(info)?; + } + Descriptor::TlsLastError(err) => { + writer.write_u16::(err.len() as u16)?; + writer.write_u8(2)?; + writer.write(err)?; + } + Descriptor::Reserved(t, dat) | Descriptor::VendorSpecific(t, dat) => { + writer.write_u16::(dat.len() as u16)?; + writer.write_u8(t.clone())?; + writer.write(dat)?; + } + } + Ok(()) + } +} diff --git a/hislip/src/common/messages.rs b/hislip/src/common/messages.rs index 3fee4ef..960c427 100644 --- a/hislip/src/common/messages.rs +++ b/hislip/src/common/messages.rs @@ -70,11 +70,19 @@ impl Message { ))) } else { let mut payload = Vec::with_capacity(len as usize); + if payload.try_reserve_exact(len as usize).is_err() { + return Ok(Err(Error::Fatal( + FatalErrorCode::UnidentifiedError, + "Out of memory".to_string(), + ))); + } reader.take(len).read_to_end(&mut payload).await?; - match MessageType::from_message_type(buf[2]).ok_or(Error::NonFatal( - NonFatalErrorCode::UnrecognizedMessageType, - "Unrecognized message type".to_string(), - )) { + match MessageType::from_message_type(buf[2]).ok_or_else(|| { + Error::NonFatal( + NonFatalErrorCode::UnrecognizedMessageType, + "Unrecognized message type".to_string(), + ) + }) { Ok(message_type) => Ok(Ok(Message { message_type, control_code, diff --git a/hislip/src/common/mod.rs b/hislip/src/common/mod.rs index a206521..491b152 100644 --- a/hislip/src/common/mod.rs +++ b/hislip/src/common/mod.rs @@ -2,6 +2,8 @@ use bitfield::bitfield; pub mod errors; pub mod messages; +pub mod descriptors; +pub(crate) mod stream; /// Protocol version 1.0 pub const PROTOCOL_1_0: Protocol = Protocol(0x0100); diff --git a/hislip/src/common/stream.rs b/hislip/src/common/stream.rs new file mode 100644 index 0000000..c9a8ccf --- /dev/null +++ b/hislip/src/common/stream.rs @@ -0,0 +1,108 @@ +use std::pin::Pin; + +use futures::io::{AsyncRead, AsyncWrite}; + +pub(crate) enum HislipStream { + Insecure(IO), + #[cfg(feature = "secure-capability")] + Secure(async_rustls::server::TlsStream), +} + +impl HislipStream { + pub(crate) fn new(io: IO) -> Self { + Self::Insecure(io) + } +} + +impl HislipStream +where + IO: AsyncRead + AsyncWrite + Unpin, +{ + #[cfg(feature = "secure-capability")] + pub(crate) async fn start_tls( + self, + acceptor: &mut async_rustls::TlsAcceptor, + ) -> Result { + match self { + HislipStream::Insecure(io) => { + match acceptor.accept(io).into_failable().await { + // Success + Ok(tls) => Ok(Self::Secure(tls)), + // Failed to switch to TLS + Err((err, io)) => Err((err, Self::Insecure(io))), + } + }, + HislipStream::Secure(_) => Err((std::io::ErrorKind::Other.into(), self)), + } + } + + #[cfg(feature = "secure-capability")] + pub(crate) async fn end_tls(self) -> std::io::Result { + match self { + HislipStream::Insecure(_) => Err(std::io::ErrorKind::Other.into()), + HislipStream::Secure(mut _tls) => { + let (_io, _session) = _tls.get_mut(); + todo!("Implement end_tls when async-rustls is updated") + } + } + } +} + +impl AsyncRead for HislipStream +where + IO: AsyncRead + AsyncWrite + Unpin, +{ + fn poll_read( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &mut [u8], + ) -> std::task::Poll> { + match self.get_mut() { + HislipStream::Insecure(io) => Pin::new(io).poll_read(cx, buf), + #[cfg(feature = "secure-capability")] + HislipStream::Secure(tls) => Pin::new(tls).poll_read(cx, buf), + } + } +} + +impl AsyncWrite for HislipStream +where + IO: AsyncRead + AsyncWrite + Unpin, +{ + #[inline] + fn poll_write( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &[u8], + ) -> std::task::Poll> { + match self.get_mut() { + HislipStream::Insecure(io) => Pin::new(io).poll_write(cx, buf), + #[cfg(feature = "secure-capability")] + HislipStream::Secure(tls) => Pin::new(tls).poll_write(cx, buf), + } + } + + #[inline] + fn poll_flush( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + match self.get_mut() { + HislipStream::Insecure(io) => Pin::new(io).poll_flush(cx), + #[cfg(feature = "secure-capability")] + HislipStream::Secure(tls) => Pin::new(tls).poll_flush(cx), + } + } + + #[inline] + fn poll_close( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + match self.get_mut() { + HislipStream::Insecure(io) => Pin::new(io).poll_close(cx), + #[cfg(feature = "secure-capability")] + HislipStream::Secure(tls) => Pin::new(tls).poll_close(cx), + } + } +} diff --git a/hislip/src/server/config.rs b/hislip/src/server/config.rs new file mode 100644 index 0000000..79418c3 --- /dev/null +++ b/hislip/src/server/config.rs @@ -0,0 +1,48 @@ +#[derive(Debug, Clone)] +pub struct ServerConfig { + pub vendor_id: u16, + /// Maximum server message size + pub max_message_size: u64, + /// Prefer overlapped data + pub prefer_overlap: bool, + /// Maximum allowed number of sessions + pub max_num_sessions: usize, +} + +impl ServerConfig { + pub fn vendor_id(mut self, vendor_id: u16) -> Self { + self.vendor_id = vendor_id; + self + } + + pub fn max_message_size(mut self, max_message_size: u64) -> Self { + self.max_message_size = max_message_size; + self + } + + pub fn max_num_sessions(mut self, max_num_sessions: usize) -> Self { + self.max_num_sessions = max_num_sessions; + self + } + + pub fn prefer_overlap(mut self) -> Self { + self.prefer_overlap = true; + self + } + + pub fn prefer_synchronized(mut self) -> Self { + self.prefer_overlap = false; + self + } +} + +impl Default for ServerConfig { + fn default() -> Self { + Self { + vendor_id: 0xBEEF, + max_message_size: 1024 * 1024, + prefer_overlap: true, + max_num_sessions: 64, + } + } +} \ No newline at end of file diff --git a/hislip/src/server/mod.rs b/hislip/src/server/mod.rs index 3ff75b4..3a79e4c 100644 --- a/hislip/src/server/mod.rs +++ b/hislip/src/server/mod.rs @@ -2,10 +2,9 @@ use std::cmp::min; use std::collections::HashMap; use std::io; use std::str::from_utf8; -use std::sync::Weak; -use async_std::sync::Arc; use async_std::net::{TcpListener, ToSocketAddrs}; +use async_std::sync::Arc; use futures::task::{Spawn, SpawnExt}; use futures::{AsyncRead, AsyncWrite, AsyncWriteExt, Stream, StreamExt}; @@ -15,36 +14,22 @@ use lxi_device::Device; use crate::common::errors::{Error, FatalErrorCode, NonFatalErrorCode}; use crate::common::messages::{prelude::*, send_fatal, send_nonfatal}; +use crate::common::stream::HislipStream; use crate::common::{Protocol, SUPPORTED_PROTOCOL}; use crate::server::session::{SessionState, SharedSession}; use crate::DEFAULT_DEVICE_SUBADRESS; -pub mod session; +pub use self::config::ServerConfig; +use self::session::SessionHandle; -#[derive(Debug, Copy, Clone)] -pub struct ServerConfig { - pub vendor_id: u16, - /// Maximum server message size - pub max_message_size: u64, - /// Prefer overlapped data - pub prefer_overlap: bool, - pub max_num_sessions: usize, -} +pub mod session; +pub mod config; -impl Default for ServerConfig { - fn default() -> Self { - Self { - vendor_id: 0xBEEF, - max_message_size: 1024 * 1024, - prefer_overlap: true, - max_num_sessions: 64, - } - } -} +type DeviceMap = HashMap>, Arc>)>; pub struct ServerBuilder { config: ServerConfig, - devices: HashMap>, Arc>)>, + devices: DeviceMap, } impl Default for ServerBuilder { @@ -88,7 +73,7 @@ where pub fn build(self) -> Arc> { assert!( - self.devices.len() > 0, + !self.devices.is_empty(), "Server must have one or more devices" ); Server::with_config(self.config, self.devices) @@ -100,7 +85,7 @@ where DEV: Device, { inner: Arc>>, - devices: HashMap>, Arc>)>, + devices: DeviceMap, config: ServerConfig, } @@ -108,21 +93,12 @@ impl Server where DEV: Device + Send + 'static, { - pub fn new( - devices: HashMap>, Arc>)>, - ) -> Arc { + pub fn new(devices: DeviceMap) -> Arc { let config = ServerConfig::default(); - Arc::new(Server { - inner: InnerServer::new(config.max_num_sessions), - config, - devices, - }) + Self::with_config(config, devices) } - pub fn with_config( - config: ServerConfig, - devices: HashMap>, Arc>)>, - ) -> Arc { + pub fn with_config(config: ServerConfig, devices: DeviceMap) -> Arc { Arc::new(Server { inner: InnerServer::new(config.max_num_sessions), config, @@ -162,13 +138,14 @@ where async fn handle_session( &self, peer: String, - mut stream: S, + stream: S, srq: SRQ, ) -> Result<(), io::Error> where S: AsyncRead + AsyncWrite + Unpin, SRQ: Stream + Unpin, { + let mut stream = HislipStream::new(stream); loop { match Message::read_from(&mut stream, self.config.max_message_size).await? { Ok(msg) => { @@ -185,27 +162,22 @@ where ) } Message { - message_type: MessageType::FatalError, - control_code, - payload, - .. - } => { - log::error!(peer=format!("{}", peer); - "Client fatal error {:?}: {}", FatalErrorCode::from_error_code(control_code), - from_utf8(&payload).unwrap_or("") - ); - //break; // Let client close connection - } - Message { - message_type: MessageType::Error, + message_type: typ @ MessageType::Error | typ @ MessageType::FatalError, control_code, payload, .. } => { - log::warn!(peer=format!("{}", peer); - "Client error {:?}: {}", NonFatalErrorCode::from_error_code(control_code), - from_utf8(&payload).unwrap_or("") - ); + if typ == MessageType::FatalError { + log::error!(peer=peer.to_string(); + "Client fatal error {:?}: {}", FatalErrorCode::from_error_code(control_code), + from_utf8(&payload).unwrap_or("") + ); + } else { + log::warn!(peer=peer.to_string(); + "Client error {:?}: {}", NonFatalErrorCode::from_error_code(control_code), + from_utf8(&payload).unwrap_or("") + ); + } } Message { message_type: MessageType::Initialize, @@ -267,12 +239,13 @@ where // Continue as sync session let res = session::synchronous::SyncSession::new( id, - self.config, + self.config.clone(), shared, RemoteLockHandle::new(device), - receiver + receiver, + protocol ) - .handle_session(stream, peer.clone(), protocol, ) + .handle_session(stream, peer.clone()) .await; log::debug!(peer=peer.to_string(), session_id=id; "Sync session closed: {res:?}"); return res; @@ -331,7 +304,7 @@ where MessageType::AsyncInitializeResponse .message_params( - AsyncInitializeResponseControl::new(false).0, + AsyncInitializeResponseControl::new(true).0, AsyncInitializeResponseParameter::new( self.config.vendor_id, ) @@ -344,10 +317,10 @@ where // Continue as async session let res = session::asynchronous::AsyncSession::new( id, - self.config, + self.config.clone(), shared, device, - sender + sender, ) .handle_session(stream, peer.clone(), srq, protocol) .await; @@ -377,39 +350,6 @@ where } } -/// A handle to a created active season -#[derive(Clone)] -pub(crate) struct SessionHandle -where - DEV: Device, -{ - _id: u16, - shared: Weak>, - device: Weak>>, -} - -impl SessionHandle -where - DEV: Device, -{ - fn new( - id: u16, - session: Weak>, - handle: Weak>>, - ) -> Self { - Self { - _id: id, - shared: session, - device: handle, - } - } - - /// Return false if the assosciated object have been closed - fn active(&self) -> bool { - self.shared.strong_count() > 0 && self.device.strong_count() > 0 - } -} - struct InnerServer where DEV: Device, @@ -419,6 +359,14 @@ where max_num_sessions: usize, } +type SessionInfo = (Arc>, Arc>>); + +type NewSession = ( + u16, + Arc>, + Arc>>, +); + impl InnerServer where DEV: Device, @@ -444,8 +392,7 @@ where return Err(Error::Fatal( FatalErrorCode::MaximumClientsExceeded, "Out of session ids".to_string(), - ) - .into()); + )); } } @@ -457,14 +404,7 @@ where &mut self, protocol: Protocol, handle: LockHandle, - ) -> Result< - ( - u16, - Arc>, - Arc>>, - ), - Error, - > { + ) -> Result, Error> { self.gc_sessions(); if self.sessions.len() >= self.max_num_sessions { return Err(Error::Fatal( @@ -486,10 +426,7 @@ where /// Get a session /// Note: Returns a strong reference which will keep any locks assosciated with session active until dropped - fn get_session( - &mut self, - session_id: u16, - ) -> Option<(Arc>, Arc>>)> { + fn get_session(&mut self, session_id: u16) -> Option> { let tmp = self.sessions.get(&session_id)?; let shared = tmp.shared.upgrade()?; let dev = tmp.device.upgrade()?; diff --git a/hislip/src/server/session/asynchronous.rs b/hislip/src/server/session/asynchronous.rs index 6e7f022..12b303c 100644 --- a/hislip/src/server/session/asynchronous.rs +++ b/hislip/src/server/session/asynchronous.rs @@ -7,17 +7,16 @@ use async_std::future; use async_std::prelude::StreamExt; use async_std::sync::Arc; use byteorder::{ByteOrder, NetworkEndian}; -use futures::future::Either; +use futures::future::{select, Either}; use futures::lock::Mutex; -use futures::{ - pin_mut, AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, FutureExt, Stream, -}; +use futures::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, Stream}; use lxi_device::lock::{LockHandle, SharedLockError, SharedLockMode, SpinMutex}; use lxi_device::{Device, DeviceError}; use crate::common::errors::{Error, FatalErrorCode, NonFatalErrorCode}; use crate::common::messages::{prelude::*, send_fatal, send_nonfatal}; -use crate::common::{PROTOCOL_2_0, Protocol}; +use crate::common::stream::HislipStream; +use crate::common::{Protocol, PROTOCOL_2_0}; use super::{ServerConfig, SharedSession}; @@ -49,7 +48,7 @@ where config: ServerConfig, shared: Arc>, handle: Arc>>, - clear: Sender<()> + clear: Sender<()>, ) -> Self { Self { id, @@ -62,47 +61,53 @@ where pub(crate) async fn handle_session( self, - stream: S, + mut stream: HislipStream, peer: String, mut srq: SRQ, - protocol: Protocol + protocol: Protocol, ) -> Result<(), io::Error> where S: AsyncRead + AsyncWrite + Unpin, SRQ: Stream + Unpin, { - let (mut rd, mut wr) = stream.split(); + //let (mut rd, mut wr) = stream.split(); let mut srq_bit = false; loop { - let read_msg = Message::read_from(&mut rd, self.config.max_message_size).fuse(); - pin_mut!(read_msg); - - let t = match futures::future::select(read_msg, srq.next()).await { - // Message was received - Either::Left((msg, _)) => msg, - // Status changed - Either::Right((stb, read_msg)) => { - // Send SRQ - match stb { - Some(val) if !srq_bit => { - srq_bit = true; - MessageType::AsyncServiceRequest - .message_params(val, 0) - .write_to(&mut wr) - .await? - } - _ => { - send_fatal!(peer=peer.to_string(), session_id=self.id; - &mut wr, FatalErrorCode::UnidentifiedError, - "Server shutdown", - ); + // Read a message + let t = { + let (mut rd, mut wr) = stream.split(); + let read_msg = Box::pin(Message::read_from(&mut rd, self.config.max_message_size)); + let msg = match select(read_msg, srq.next()).await { + Either::Left((msg, _)) => msg, + Either::Right((stb, msg)) => { + match stb { + // Statusbyte has changed + Some(stb) => { + if !srq_bit { + MessageType::AsyncServiceRequest + .message_params(stb as u8, 0) + .no_payload() + .write_to(&mut wr) + .await?; + srq_bit = true; + } + }, + // Srq is closed, server is shutting down + None => { + log::info!(peer=peer.to_string(), session_id=self.id; "Server shutting down..."); + return Ok(()) + }, } + // Finish receiving message + // This is important as dropping the future mid-message can corrupt the datastream + msg.await } - // Finish receiving message - read_msg.await - } - }?; + }; + stream = rd.reunite(wr).unwrap(); + + msg? + }; match t { Ok(msg) => { @@ -112,32 +117,27 @@ where .. } => { send_nonfatal!(peer=peer.to_string(), session_id=self.id; - &mut wr, NonFatalErrorCode::UnrecognizedVendorDefinedMessage, + &mut stream, NonFatalErrorCode::UnrecognizedVendorDefinedMessage, "Unrecognized Vendor Defined Message ({})", code ); } Message { - message_type: MessageType::FatalError, - control_code, - payload, - .. - } => { - log::error!(peer=peer.to_string(), session_id=self.id; - "Client fatal error {:?}: {}", FatalErrorCode::from_error_code(control_code), - from_utf8(&payload).unwrap_or("") - ); - //break; // Let client close connection - } - Message { - message_type: MessageType::Error, + message_type: typ @ MessageType::Error | typ @ MessageType::FatalError, control_code, payload, .. } => { - log::warn!(peer=peer.to_string(), session_id=self.id; - "Client error {:?}: {}", NonFatalErrorCode::from_error_code(control_code), - from_utf8(&payload).unwrap_or("") - ); + if typ == MessageType::FatalError { + log::error!(peer=peer.to_string(), session_id=self.id; + "Client fatal error {:?}: {}", FatalErrorCode::from_error_code(control_code), + from_utf8(&payload).unwrap_or("") + ); + } else { + log::warn!(peer=peer.to_string(), session_id=self.id; + "Client error {:?}: {}", NonFatalErrorCode::from_error_code(control_code), + from_utf8(&payload).unwrap_or("") + ); + } } Message { message_type: MessageType::AsyncLock, @@ -160,7 +160,7 @@ where MessageType::AsyncLockResponse .message_params(control as u8, 0) .no_payload() - .write_to(&mut wr) + .write_to(&mut stream) .await?; } else { // Lock @@ -207,7 +207,7 @@ where MessageType::AsyncLockResponse .message_params(control as u8, 0) .no_payload() - .write_to(&mut wr) + .write_to(&mut stream) .await?; } } @@ -275,17 +275,17 @@ where MessageType::AsyncRemoteLocalResponse .message_params(0, 0) .no_payload() - .write_to(&mut wr) + .write_to(&mut stream) .await? } Err(DeviceError::NotSupported) => { - send_nonfatal!(peer=peer.to_string(), session_id=self.id; &mut wr, + send_nonfatal!(peer=peer.to_string(), session_id=self.id; &mut stream, NonFatalErrorCode::UnrecognizedControlCode, "Unrecognized control code", ); } Err(_) => { - send_nonfatal!(peer=peer.to_string(), session_id=self.id; &mut wr, + send_nonfatal!(peer=peer.to_string(), session_id=self.id; &mut stream, NonFatalErrorCode::UnidentifiedError, "Internal error", ); @@ -299,7 +299,7 @@ where } => { if payload.len() != 8 { send_fatal!(peer=peer.to_string(), session_id=self.id; - &mut wr, FatalErrorCode::PoorlyFormattedMessageHeader, + &mut stream, FatalErrorCode::PoorlyFormattedMessageHeader, "Expected 8 bytes in AsyncMaximumMessageSize payload" ) } @@ -314,11 +314,11 @@ where let mut buf = [0u8; 8]; - NetworkEndian::write_u64(&mut buf, self.config.max_message_size as u64); + NetworkEndian::write_u64(&mut buf, self.config.max_message_size); MessageType::AsyncMaximumMessageSizeResponse .message_params(0, 0) .with_payload(buf.to_vec()) - .write_to(&mut wr) + .write_to(&mut stream) .await?; } Message { @@ -333,17 +333,14 @@ where let _ = self.clear.try_send(()); // Announce preferred features - let features = FeatureBitmap::new( - self.config.prefer_overlap, - false, - false, - ); + let features = + FeatureBitmap::new(self.config.prefer_overlap, false, false); drop(shared); MessageType::AsyncDeviceClearAcknowledge .message_params(features.0, 0) .no_payload() - .write_to(&mut wr) + .write_to(&mut stream) .await?; } Message { @@ -377,7 +374,7 @@ where MessageType::AsyncStatusResponse .message_params(stb, 0) .no_payload() - .write_to(&mut wr) + .write_to(&mut stream) .await?; } Message { @@ -394,7 +391,7 @@ where MessageType::AsyncLockInfoResponse .message_params(exclusive.into(), num_shared) .no_payload() - .write_to(&mut wr) + .write_to(&mut stream) .await?; } Message { @@ -402,10 +399,10 @@ where control_code, message_parameter, payload, - } if protocol >= PROTOCOL_2_0 => { + } if protocol >= PROTOCOL_2_0 && cfg!(feature = "secure-capability") => { if payload.len() != 4 { send_fatal!(peer=peer.to_string(), session_id=self.id; - &mut wr, FatalErrorCode::PoorlyFormattedMessageHeader, + &mut stream, FatalErrorCode::PoorlyFormattedMessageHeader, "Expected 4 bytes in AsyncStartTLS payload" ) } @@ -417,8 +414,9 @@ where log::debug!(session_id=self.id, message_id_sent=message_id_sent, message_id_read=message_id_read; "Start async TLS"); // TODO: Encryption support + //stream = stream.start_tls(acceptor)?; send_fatal!( - &mut wr, + &mut stream, FatalErrorCode::SecureConnectionFailed, "Secure connection not supported" ) @@ -439,15 +437,17 @@ where // TODO: Encryption support send_fatal!( - &mut wr, + &mut stream, FatalErrorCode::SecureConnectionFailed, "Secure connection not supported" ) } - _ => { - send_nonfatal!(peer=peer.to_string(), session_id=self.id; &mut wr, + Message { + message_type, .. + } => { + send_nonfatal!(peer=peer.to_string(), session_id=self.id; &mut stream, NonFatalErrorCode::UnrecognizedMessageType, - "Unexpected message type in asynchronous channel", + "Unexpected {message_type:?} in asynchronous channel", ); } } @@ -455,10 +455,10 @@ where Err(err) => { // Send error to client and close if fatal if err.is_fatal() { - Message::from(err).write_to(&mut wr).await?; + Message::from(err).write_to(&mut stream).await?; break Err(io::ErrorKind::Other.into()); } else { - Message::from(err).write_to(&mut wr).await?; + Message::from(err).write_to(&mut stream).await?; } } } diff --git a/hislip/src/server/session/mod.rs b/hislip/src/server/session/mod.rs index afba926..911aa75 100644 --- a/hislip/src/server/session/mod.rs +++ b/hislip/src/server/session/mod.rs @@ -1,4 +1,7 @@ -use async_std::channel::{self, Receiver, Sender}; +use std::sync::Weak; + +use async_std::{channel::{self, Receiver, Sender}}; +use lxi_device::{Device, lock::{Mutex, SpinMutex, LockHandle}}; use super::ServerConfig; use crate::common::Protocol; @@ -22,7 +25,7 @@ pub(crate) struct SharedSession { /// Negotiated rpc protocol: Protocol, - /// Current tate of session + /// Current state of session state: SessionState, /// Negotiated session mode @@ -88,3 +91,36 @@ impl SharedSession { self.clear.0.clone() } } + +/// A handle to a created active season +#[derive(Clone)] +pub(crate) struct SessionHandle +where + DEV: Device, +{ + _id: u16, + pub shared: Weak>, + pub device: Weak>>, +} + +impl SessionHandle +where + DEV: Device, +{ + pub(crate) fn new( + id: u16, + session: Weak>, + handle: Weak>>, + ) -> Self { + Self { + _id: id, + shared: session, + device: handle, + } + } + + /// Return false if the assosciated object have been closed + pub(crate) fn active(&self) -> bool { + self.shared.strong_count() > 0 && self.device.strong_count() > 0 + } +} \ No newline at end of file diff --git a/hislip/src/server/session/synchronous.rs b/hislip/src/server/session/synchronous.rs index 4c439ac..5c10c52 100644 --- a/hislip/src/server/session/synchronous.rs +++ b/hislip/src/server/session/synchronous.rs @@ -1,17 +1,19 @@ use std::io; use std::str::from_utf8; +use std::time::Duration; use async_std::channel::Receiver; +use async_std::future::timeout; use async_std::sync::Arc; use futures::lock::Mutex; -use futures::{select, AsyncWriteExt, FutureExt, AsyncRead, AsyncWrite}; +use futures::{select, AsyncRead, AsyncWrite, AsyncWriteExt, FutureExt}; use lxi_device::lock::RemoteLockHandle; -use lxi_device::Device; use lxi_device::trigger::Source; +use lxi_device::Device; use crate::common::errors::{Error, FatalErrorCode, NonFatalErrorCode}; use crate::common::messages::{prelude::*, send_fatal, send_nonfatal}; -use crate::common::{PROTOCOL_2_0, Protocol}; +use crate::common::{Protocol, PROTOCOL_2_0}; use super::{ServerConfig, SharedSession}; use crate::server::session::{SessionMode, SessionState}; @@ -33,6 +35,8 @@ where shared: Arc>, clear: Receiver<()>, + + protocol: Protocol, } impl SyncSession @@ -45,6 +49,7 @@ where shared: Arc>, handle: RemoteLockHandle, clear: Receiver<()>, + protocol: Protocol, ) -> Self { Self { id, @@ -52,6 +57,7 @@ where shared, handle, clear, + protocol, } } @@ -60,7 +66,10 @@ where mut stream: S, peer: String, control_code: u8, - ) -> Result<(), io::Error> where S: AsyncRead + AsyncWrite + Unpin { + ) -> Result<(), io::Error> + where + S: AsyncRead + AsyncWrite + Unpin, + { let mut shared = self.shared.lock().await; let feature_request = FeatureBitmap(control_code); log::debug!(peer=peer.to_string(), session_id = self.id; "Device clear complete, {}", feature_request); @@ -75,11 +84,7 @@ where }; // Agreed features - let feature_setting = FeatureBitmap::new( - feature_request.overlapped(), - false, - false, - ); + let feature_setting = FeatureBitmap::new(feature_request.overlapped(), false, false); let sent_message_id = shared.sent_message_id; drop(shared); @@ -90,76 +95,20 @@ where .await } - async fn clear_buffer( - &self, - mut stream: S, - peer: String, - mut msg: Result, - ) -> Result<(), io::Error> where S: AsyncRead + AsyncWrite + Unpin { - loop { - match msg { - Ok(Message { - message_type: MessageType::DeviceClearComplete, - control_code, - .. - }) => { - if self.handle.can_lock().is_ok() { - let mut dev = self.handle.inner_lock().await; - let _res = dev.clear(); - } - - break self - .acknowledge_device_clear(stream, peer, control_code) - .await; - } - // Ignore other messages - Ok(_) => {} - // Invalid message - Err(err) => { - if err.is_fatal() { - Message::from(err).write_to(&mut stream).await?; - return Err(io::ErrorKind::Other.into()); - } else { - Message::from(err).write_to(&mut stream).await?; - } - } - } - msg = Message::read_from(&mut stream, self.config.max_message_size).await?; - } - } - pub(crate) async fn handle_session( self, mut stream: S, peer: String, - protocol: Protocol, - ) -> Result<(), io::Error> where S: AsyncRead + AsyncWrite + Unpin { + ) -> Result<(), io::Error> + where + S: AsyncRead + AsyncWrite + Unpin, + { // Data buffer let mut buffer: Vec = Vec::new(); loop { let msg = Message::read_from(&mut stream, self.config.max_message_size).await?; - // Check if a clear device is in progress before waiting for a lock - if let Ok(_abort) = self.clear.try_recv() { - // Clear buffer - buffer.clear(); - self.clear_buffer(&mut stream, peer.clone(), msg).await?; - continue; - } - - // Wait for device becoming available or a lock is acquired - // Abort the lock attempt if a clear device is started - let mut dev = select! { - res = self.handle.async_lock().fuse() => res.unwrap(), - _abort = self.clear.recv().fuse() => { - // Clear buffer - buffer.clear(); - self.clear_buffer(&mut stream, peer.clone(), msg).await?; - continue; - } - }; - // Do not read messages unless a loc match msg { // Valid message @@ -175,26 +124,22 @@ where ); } Message { - message_type: MessageType::FatalError, + message_type: typ @ MessageType::Error | typ @ MessageType::FatalError, control_code, payload, .. } => { - log::error!(peer=peer.to_string(), session_id=self.id; - "Client fatal error {:?}: {}", FatalErrorCode::from_error_code(control_code), - from_utf8(&payload).unwrap_or("") - ); - } - Message { - message_type: MessageType::Error, - control_code, - payload, - .. - } => { - log::warn!(peer=peer.to_string(), session_id=self.id; - "Client error {:?}: {}", NonFatalErrorCode::from_error_code(control_code), - from_utf8(&payload).unwrap_or("") - ); + if typ == MessageType::FatalError { + log::error!(peer=peer.to_string(), session_id=self.id; + "Client fatal error {:?}: {}", FatalErrorCode::from_error_code(control_code), + from_utf8(&payload).unwrap_or("") + ); + } else { + log::warn!(peer=peer.to_string(), session_id=self.id; + "Client error {:?}: {}", NonFatalErrorCode::from_error_code(control_code), + from_utf8(&payload).unwrap_or("") + ); + } } Message { message_type: typ @ MessageType::Data | typ @ MessageType::DataEnd, @@ -206,6 +151,26 @@ where let control = RmtDeliveredControl(control_code); let is_end = matches!(typ, MessageType::DataEnd); + // Wait for device becoming available or a lock is acquired + // Abort the lock attempt if a clear device is started + let mut dev = select! { + res = self.handle.async_lock().fuse() => match res{ + Ok(res) => res, + Err(_) => { + send_fatal!(peer=peer.to_string(), session_id=self.id; + &mut stream, + FatalErrorCode::UnidentifiedError, + "Internal locking error" + ); + } + }, + _abort = self.clear.recv().fuse() => { + buffer.clear(); + self.acknowledge_device_clear(&mut stream, peer.clone(), control_code).await?; + continue; + }, + }; + let mut shared = self.shared.lock().await; let state = shared.state(); @@ -213,37 +178,55 @@ where // Normal state SessionState::Normal => { shared.read_message_id = message_id; + drop(shared); // Drop shared data as to not block async session + + if buffer.try_reserve_exact(data.len()).is_err() { + send_fatal!(peer=peer.to_string(), session_id=self.id; + &mut stream, + FatalErrorCode::UnidentifiedError, + "Out of memory" + ); + } buffer.extend_from_slice(&data); + log::info!("Buffer={:?}", data); if is_end { log::debug!(peer=peer.to_string(), session_id=self.id, message_id=message_id; "Data END, {}", control); + let data = dev.execute(&buffer); buffer.clear(); - let mut chunks = data - .chunks(shared.max_message_size as usize) - .peekable(); - drop(shared); - - while let Some(chunk) = chunks.next() { - // Stop sending if a clear has been received on async channel - if self.clear.try_recv().is_ok() { - break; + // Send back response + let shared = self.shared.lock().await; + if let Some(data) = data { + log::info!("Sending back"); + let mut chunks = data + .chunks(shared.max_message_size as usize) + .peekable(); + drop(shared); + + while let Some(chunk) = chunks.next() { + // Stop sending if a clear has been received on async channel + if self.clear.try_recv().is_ok() { + log::info!("Sending back, clear!"); + + break; + } + + // Peek if next chunk exists, if not, mark data as end + let end = chunks.peek().is_none(); + let msg = if end { + MessageType::DataEnd + } else { + MessageType::Data + }; + + // Send message + msg.message_params(0, message_id) + .with_payload(chunk.to_vec()) + .write_to(&mut stream) + .await?; } - - // Peek if next chunk exists, if not, mark data as end - let end = chunks.peek().is_none(); - let msg = if end { - MessageType::DataEnd - } else { - MessageType::Data - }; - - // Send message - msg.message_params(0, message_id) - .with_payload(chunk.to_vec()) - .write_to(&mut stream) - .await?; } } else { log::debug!(peer=peer.to_string(), session_id=self.id, message_id=message_id; "Data, {}", control); @@ -267,6 +250,26 @@ where control_code, .. } => { + // Wait for device becoming available or a lock is acquired + // Abort the lock attempt if a clear device is started + let mut dev = select! { + res = self.handle.async_lock().fuse() => match res{ + Ok(res) => res, + Err(_) => { + send_fatal!(peer=peer.to_string(), session_id=self.id; + &mut stream, + FatalErrorCode::UnidentifiedError, + "Internal locking error" + ); + } + }, + _abort = self.clear.recv().fuse() => { + buffer.clear(); + self.acknowledge_device_clear(&mut stream, peer.clone(), control_code).await?; + continue; + } + }; + let mut inner = self.shared.lock().await; inner.read_message_id = message_id; let state = inner.state(); @@ -291,25 +294,43 @@ where } Message { message_type: MessageType::DeviceClearComplete, + control_code, .. - } => { - // Should've been handled above when AsyncDeviceClear was sent - send_nonfatal!(peer=peer.to_string(), session_id=self.id; - &mut stream, - NonFatalErrorCode::UnidentifiedError, - "Unexpected device clear complete in synchronous channel" - ); - } + } => match timeout(Duration::from_secs(10), self.clear.recv()).await { + Ok(Ok(())) => { + buffer.clear(); + self.acknowledge_device_clear( + &mut stream, + peer.clone(), + control_code, + ) + .await?; + } + Ok(Err(_rerr)) => { + send_fatal!(peer=peer.to_string(), session_id=self.id; + &mut stream, + FatalErrorCode::UnidentifiedError, + "Internal server error" + ); + } + Err(_terr) => { + send_fatal!(peer=peer.to_string(), session_id=self.id; + &mut stream, + FatalErrorCode::UnidentifiedError, + "Received device clear complete without a request" + ); + } + }, Message { message_type: MessageType::GetDescriptors, .. - } => { - + } if self.protocol >= PROTOCOL_2_0 => { + todo!() } Message { message_type: MessageType::StartTLS | MessageType::EndTLS, .. - } if protocol >= PROTOCOL_2_0 => { + } if self.protocol >= PROTOCOL_2_0 => { log::debug!(peer=peer.to_string(), session_id=self.id; "Start/end TLS"); send_fatal!( @@ -319,16 +340,19 @@ where ) } Message { - message_type: MessageType::GetSaslMechanismList | MessageType::AuthenticationStart | MessageType::AuthenticationExchange, + message_type: + MessageType::GetSaslMechanismList + | MessageType::AuthenticationStart + | MessageType::AuthenticationExchange, payload: _data, .. - } if protocol >= PROTOCOL_2_0 => { + } if self.protocol >= PROTOCOL_2_0 => { log::debug!(peer=peer.to_string(), session_id=self.id; "Authentication Start/Exchange"); send_fatal!( &mut stream, FatalErrorCode::SecureConnectionFailed, - "Secure connection not supported" + "Authentication not supported" ) } msg => { diff --git a/hislip/tests/conftest.py b/hislip/tests/conftest.py index 794240d..a44c588 100644 --- a/hislip/tests/conftest.py +++ b/hislip/tests/conftest.py @@ -2,8 +2,9 @@ import pytest from xprocess import ProcessStarter + @pytest.fixture -def hislip_example(xprocess, request, free_port): +def hislip_example(xprocess, request, pytestconfig, free_port): target = os.environ.get("DEBUG_TARGET") if target is not None: port = os.environ.get("HISLIP_PORT") @@ -14,14 +15,18 @@ def hislip_example(xprocess, request, free_port): yield f"TCPIP::{target}::hislip0::INSTR" else: - port = free_port + port = os.environ.get("HISLIP_PORT", default=str(free_port)) class Starter(ProcessStarter): # startup pattern pattern = "Running server" # Hide warnings - env = {"RUSTFLAGS": "-Awarnings", **os.environ} + env = { + "RUSTFLAGS": "-Awarnings", + # "CARGO_TARGET_DIR": pytestconfig.cache.mkdir("target"), + **os.environ, + } # command to start process args = [ diff --git a/hislip/tests/test_hislip.py b/hislip/tests/test_hislip.py index dff3abc..957881f 100644 --- a/hislip/tests/test_hislip.py +++ b/hislip/tests/test_hislip.py @@ -2,11 +2,12 @@ import pytest import pyvisa +IDN_RESPONSE = "Cyberdyne systems,T800 Model 101,A9012.C,V2.4" def test_connect(hislip_example, resource_manager): if resource_manager.visalib.library_path == "py": pytest.skip("pyvisa-py does not support HiSLIP", allow_module_level=True) - inst = resource_manager.open_resource(hislip_example) + inst = resource_manager.open_resource(hislip_example, read_termination = "", write_termination = "") inst.close() @@ -14,12 +15,10 @@ def test_connect(hislip_example, resource_manager): def test_hislip_idn(hislip_example, resource_manager): if resource_manager.visalib.library_path == "py": pytest.skip("pyvisa-py does not support HiSLIP", allow_module_level=True) - inst = resource_manager.open_resource(hislip_example) - inst.read_termination = "" - inst.write_termination = "" + inst = resource_manager.open_resource(hislip_example, read_termination = "", write_termination = "") - resp = inst.query("*IDN?") - assert resp == "Cyberdyne systems,T800 Model 101,A9012.C,V2.4" + resp = inst.query("*IDN?\n") + assert resp == IDN_RESPONSE inst.close() @@ -27,9 +26,13 @@ def test_hislip_idn(hislip_example, resource_manager): def test_clear(hislip_example, resource_manager: pyvisa.ResourceManager): if resource_manager.visalib.library_path == "py": pytest.skip("pyvisa-py does not support HiSLIP", allow_module_level=True) - inst = resource_manager.open_resource(hislip_example) + inst: pyvisa.resources.MessageBasedResource = resource_manager.open_resource(hislip_example, read_termination = "", write_termination = "") + inst.send_end = False + inst.write("GARBAGE") + inst.send_end = True inst.clear() + assert inst.query("*IDN?") == IDN_RESPONSE inst.close() @@ -37,7 +40,7 @@ def test_clear(hislip_example, resource_manager: pyvisa.ResourceManager): def test_trigger(hislip_example, resource_manager: pyvisa.ResourceManager): if resource_manager.visalib.library_path == "py": pytest.skip("pyvisa-py does not support HiSLIP", allow_module_level=True) - inst = resource_manager.open_resource(hislip_example) + inst = resource_manager.open_resource(hislip_example, read_termination = "", write_termination = "") inst.assert_trigger() @@ -49,7 +52,7 @@ def test_hislip_exclusive_lock( ): if resource_manager.visalib.library_path == "py": pytest.skip("pyvisa-py does not support HiSLIP", allow_module_level=True) - inst = resource_manager.open_resource(hislip_example) + inst = resource_manager.open_resource(hislip_example, read_termination = "", write_termination = "") # Lock and unlock inst.lock_excl(25.0) @@ -61,9 +64,9 @@ def test_hislip_exclusive_lock( def test_hislip_shared_lock(hislip_example, resource_manager: pyvisa.ResourceManager): if resource_manager.visalib.library_path == "py": pytest.skip("pyvisa-py does not support HiSLIP", allow_module_level=True) - inst1 = resource_manager.open_resource(hislip_example) - inst2 = resource_manager.open_resource(hislip_example) - inst3 = resource_manager.open_resource(hislip_example) + inst1 = resource_manager.open_resource(hislip_example, read_termination = "", write_termination = "") + inst2 = resource_manager.open_resource(hislip_example, read_termination = "", write_termination = "") + inst3 = resource_manager.open_resource(hislip_example, read_termination = "", write_termination = "") # Lock inst1.lock(requested_key="foo", timeout=0) @@ -93,8 +96,8 @@ def test_hislip_clear_in_progress( ): if resource_manager.visalib.library_path == "py": pytest.skip("pyvisa-py does not support HiSLIP", allow_module_level=True) - inst1 = resource_manager.open_resource(hislip_example) - inst2 = resource_manager.open_resource(hislip_example) + inst1 = resource_manager.open_resource(hislip_example, read_termination = "", write_termination = "") + inst2 = resource_manager.open_resource(hislip_example, read_termination = "", write_termination = "") # Lock inst1.lock(requested_key="foo") diff --git a/hislip/tests/test_hislip_v2.py b/hislip/tests/test_hislip_v2.py new file mode 100644 index 0000000..0344b71 --- /dev/null +++ b/hislip/tests/test_hislip_v2.py @@ -0,0 +1,23 @@ +from pyvisa import highlevel + + +for backend in highlevel.list_backends(): + if backend.startswith("pyvisa-"): + backend = backend[7:] + + try: + cls = highlevel.get_wrapper_class(backend) + except Exception as e: + backend_details[backend] = [ + "Could not instantiate backend", + "-> %s" % str(e), + ] + continue + + try: + backend_details[backend] = cls.get_debug_info() + except Exception as e: + backend_details[backend] = [ + "Could not obtain debug info", + "-> %s" % str(e), + ] \ No newline at end of file diff --git a/raw/Cargo.toml b/raw/Cargo.toml index d77901d..a3266bc 100644 --- a/raw/Cargo.toml +++ b/raw/Cargo.toml @@ -14,14 +14,17 @@ async-std = { workspace = true } async-listen = { workspace = true } futures = { workspace = true } log = { workspace = true, features = ["kv_unstable_std"] } +async-rustls = { workspace = true, optional = true } [dependencies.lxi-device] path = "../device" version = "0.1.0" -[dependencies.libtelnet-rs] -version = "2.0.0" - [dev-dependencies] -femme = { workspace = true } -clap = { workspace = true } \ No newline at end of file +femme = { workspace = true } +clap = { workspace = true } +mio-serial = "5.0" +async-io = "1.9.0" + +[features] +tls = ["dep:async-rustls"] diff --git a/raw/README.md b/raw/README.md index c2fba10..1ac72c6 100644 --- a/raw/README.md +++ b/raw/README.md @@ -1,7 +1,8 @@ # lxi-socket - +# Serialport +See `examples/serial.rs` for a example on how to listen to a serial port. On linux one can use socat to create a virtual serialport for local testing. # License -This crate is licensed under GPLv3 or later. See ([LICENSE-GPL](../LICENSE-GPL) or https://opensource.org/licenses/GPL-3.0) +This crate is licensed under GPLv3 or later. See ([LICENSE-GPL](../LICENSE-GPL) or https://opensource.org/licenses/GPL-3.0) diff --git a/raw/examples/raw.rs b/raw/examples/scpi-raw.rs similarity index 91% rename from raw/examples/raw.rs rename to raw/examples/scpi-raw.rs index c736c1d..688585d 100644 --- a/raw/examples/raw.rs +++ b/raw/examples/scpi-raw.rs @@ -37,13 +37,8 @@ async fn main() -> std::io::Result<()> { log::info!("Running server on port {}:{}...", args.ip, args.port); if let Some(t) = args.timeout { - timeout( - Duration::from_millis(t), - ipv4_server - ) - .await + timeout(Duration::from_millis(t), ipv4_server).await } else { ipv4_server.await } - } diff --git a/raw/examples/scpi-tls.rs b/raw/examples/scpi-tls.rs new file mode 100644 index 0000000..37539ba --- /dev/null +++ b/raw/examples/scpi-tls.rs @@ -0,0 +1,133 @@ +use std::{ + fs::File, + io::{self, BufReader}, + sync::Arc, + time::Duration, +}; + +use async_std::io::timeout; +use lxi_device::{lock::SharedLock, util::SimpleDevice}; +use lxi_socket::{server::ServerConfig, SOCKET_STANDARD_PORT}; + +use clap::Parser; + +use async_rustls::{ + rustls::{ + internal::pemfile::{certs, pkcs8_private_keys, rsa_private_keys}, + AllowAnyAnonymousOrAuthenticatedClient, AllowAnyAuthenticatedClient, Certificate, + NoClientAuth, PrivateKey, RootCertStore, ServerConfig as TlsConfig, + }, + TlsAcceptor, +}; + +/// Simple program to greet a person +#[derive(Parser, Debug)] +#[clap(author, version, about, long_about = None)] +struct Args { + #[clap(default_value = "0.0.0.0")] + ip: String, + + /// Number of times to greet + #[clap(short, long, default_value_t = SOCKET_STANDARD_PORT)] + port: u16, + + /// Kill server after timeout (useful for coverage testing) + #[clap(short, long)] + timeout: Option, + + /// TLS certificate + #[clap(short, long, default_value = ".certificates/cert.pem")] + cert: String, + + /// TLS key + #[clap(short, long, default_value = ".certificates/key.pem")] + key: String, + + #[clap(long)] + client_cert: Vec, + + #[clap(long)] + require_authentication: bool, +} + +/// Load the passed certificates file +fn load_certs(path: &str) -> io::Result> { + certs(&mut BufReader::new(File::open(path)?)) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid cert")) +} + +/// Load the passed keys file +fn load_keys(path: &str) -> io::Result> { + // Try to load RSA key + match rsa_private_keys(&mut BufReader::new(File::open(path)?)) { + Ok(keys) => Ok(keys), + // Try PKCS#8 if not RSA + Err(_) => match pkcs8_private_keys(&mut BufReader::new(File::open(path)?)) { + Ok(keys) => Ok(keys), + Err(_) => { + return Err(io::Error::new(io::ErrorKind::InvalidInput, "Invalid key, expected RSA or PKCS#8 in PEM format")) + }, + }, + } +} + +/// Configure the server using rusttls +/// See https://docs.rs/rustls/0.16.0/rustls/struct.ServerConfig.html for details +/// +/// A TLS server needs a certificate and a fitting private key +fn load_config(options: &Args) -> io::Result { + let certs = load_certs(&options.cert)?; + let mut keys = load_keys(&options.key)?; + + let mut config = if !options.client_cert.is_empty() { + let mut store = RootCertStore::empty(); + for path in &options.client_cert { + let mut reader = BufReader::new(File::open(path)?); + store + .add_pem_file(&mut reader) + .expect("Failed to load client certificate"); + } + if options.require_authentication { + TlsConfig::new(AllowAnyAuthenticatedClient::new(store)) + } else { + TlsConfig::new(AllowAnyAnonymousOrAuthenticatedClient::new(store)) + } + } else { + if options.require_authentication { + log::error!("Client authentication required but no certificates were provided") + } + TlsConfig::new(NoClientAuth::new()) + }; + + config + // set this server to use one cert together with the loaded private key + .set_single_cert(certs, keys.remove(0)) + .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?; + + Ok(config) +} + +#[async_std::main] +async fn main() -> std::io::Result<()> { + femme::with_level(log::LevelFilter::Debug); + let args = Args::parse(); + + let device = SimpleDevice::new_arc(); + let shared_lock = SharedLock::new(); + + // TLS + let config = load_config(&args)?; + let acceptor = TlsAcceptor::from(Arc::new(config)); + + let ipv4_server = ServerConfig::default() + .read_buffer(16 * 1024) + .build() + .accept_tls((&args.ip[..], args.port), shared_lock, device, acceptor); + + log::info!("Running server on port {}:{}...", args.ip, args.port); + if let Some(t) = args.timeout { + timeout(Duration::from_millis(t), ipv4_server).await + } else { + ipv4_server.await + } +} diff --git a/raw/examples/serial.rs b/raw/examples/serial.rs new file mode 100644 index 0000000..80ccf63 --- /dev/null +++ b/raw/examples/serial.rs @@ -0,0 +1,53 @@ +use std::time::Duration; + +use async_io::Async; +use async_std::io::timeout; +use futures::AsyncReadExt; +use lxi_device::{lock::SharedLock, util::SimpleDevice}; +use lxi_socket::server::ServerConfig; + +use clap::Parser; +use mio_serial::SerialPortBuilderExt; + +/// Simple program to greet a person +#[derive(Parser, Debug)] +#[clap(author, version, about, long_about = None)] +struct Args { + #[clap(default_value = "/dev/ttyS0")] + path: String, + + #[clap(short, long, default_value_t = 9600)] + baudrate: u32, + + /// Kill server after timeout (useful for coverage testing) + #[clap(short, long)] + timeout: Option, +} + +#[async_std::main] +async fn main() -> std::io::Result<()> { + femme::with_level(log::LevelFilter::Debug); + let args = Args::parse(); + + let device = SimpleDevice::new_arc(); + let shared_lock = SharedLock::new(); + + let (reader, writer) = mio_serial::new(&args.path, args.baudrate) + .open_native_async() + .map_err(|e| e.into()) + .and_then(Async::new) + .expect("Failed to open port") + .split(); + + let serial_server = ServerConfig::default() + .read_buffer(16 * 1024) + .build() + .process_client(reader, writer, shared_lock, device, &args.path); + + log::info!("Running server on port {}...", args.path); + if let Some(t) = args.timeout { + timeout(Duration::from_millis(t), serial_server).await + } else { + serial_server.await + } +} diff --git a/raw/src/lib.rs b/raw/src/lib.rs index 1e805d4..f609c82 100644 --- a/raw/src/lib.rs +++ b/raw/src/lib.rs @@ -2,4 +2,9 @@ pub mod server; pub mod common {} +/// Standard port for raw SCPI socket communication pub const SOCKET_STANDARD_PORT: u16 = 5025; +/// Our standard port for secure raw communication. +/// **This is not a LXI standard port, just ours!** +pub const TLS_PORT: u16 = 6025; + diff --git a/raw/src/server/mod.rs b/raw/src/server/mod.rs index c5152ca..4c15ed6 100644 --- a/raw/src/server/mod.rs +++ b/raw/src/server/mod.rs @@ -21,9 +21,13 @@ use lxi_device::{ #[cfg(unix)] use async_std::os::unix::net::UnixListener; +#[cfg(feature = "tls")] +pub mod tls; + pub struct Server(ServerConfig); impl Server { + /// Listen to a socket for clients pub async fn accept( self: Arc, addr: impl ToSocketAddrs, @@ -64,6 +68,58 @@ impl Server { Ok(()) } + /// Listen to a socket for clients with a TLS acceptor + #[cfg(feature = "tls")] + pub async fn accept_tls( + self: Arc, + addr: impl ToSocketAddrs, + shared_lock: Arc>, + device: Arc>, + acceptor: async_rustls::TlsAcceptor, + ) -> io::Result<()> + where + DEV: Device + Send + 'static, + { + let listener = TcpListener::bind(addr).await?; + let mut incoming = listener + .incoming() + .log_warnings(|warn| log::warn!("Listening error: {}", warn)) + .handle_errors(Duration::from_millis(100)) + .backpressure(self.0.limit); + + while let Some((token, stream)) = incoming.next().await { + let s = self.clone(); + let peer = stream.peer_addr()?; + log::error!("Accepted from: {}", peer); + + let shared_lock = shared_lock.clone(); + let device = device.clone(); + let acceptor = acceptor.clone(); + + stream.set_nodelay(true)?; + + task::spawn(async move { + match acceptor.accept(stream).await { + Ok(stream) => { + let (reader, writer) = stream.split(); + if let Err(err) = s + .process_client(reader, writer, shared_lock, device, peer) + .await + { + log::warn!("Error processing client: {}", err) + } + } + Err(err) => { + log::warn!("TLS handshake failed: {err}") + }, + } + drop(token); + }); + } + Ok(()) + } + + /// Listen to a unix socket for client #[cfg(unix)] pub async fn accept_unix( self: Arc, @@ -104,6 +160,7 @@ impl Server { Ok(()) } + /// Process a generic reader/writer pub async fn process_client( self: Arc, reader: RD, @@ -135,17 +192,17 @@ impl Server { log::trace!("{:?} read {} bytes", peer, cmd.len()); - let mut resp = { + let resp = { let mut device = handle.async_lock().await.unwrap(); cmd.pop(); // Remove read_termination device.execute(&cmd) }; // Write back - if !resp.is_empty() { - resp.push(self.0.write_termination); - log::trace!("{:?} write {} bytes", peer, resp.len()); - writer.write_all(&resp).await?; + if let Some(mut data) = resp { + data.push(self.0.write_termination); + log::trace!("{:?} write {} bytes", peer, data.len()); + writer.write_all(&data).await?; //writer.flush().await?; } diff --git a/raw/src/server/tls.rs b/raw/src/server/tls.rs new file mode 100644 index 0000000..482f914 --- /dev/null +++ b/raw/src/server/tls.rs @@ -0,0 +1,13 @@ + + + +struct TlsServerConfig { + +} + + + + + + + diff --git a/raw/tests/conftest.py b/raw/tests/conftest.py index 3dac4b2..25d790a 100644 --- a/raw/tests/conftest.py +++ b/raw/tests/conftest.py @@ -2,22 +2,27 @@ import pytest from xprocess import ProcessStarter + @pytest.fixture -def socket_example(xprocess, request, free_port): +def socket_example(xprocess, request, pytestconfig, free_port): target = os.environ.get("DEBUG_TARGET") if target is not None: port = os.environ.get("SOCKET_PORT", default="5025") yield f"TCPIP::{target}::{port}::SOCKET" else: - port = free_port + port = os.environ.get("SOCKET_PORT", default=str(free_port)) class Starter(ProcessStarter): # startup pattern pattern = "Running server" # Hide warnings - env = {"RUSTFLAGS": "-Awarnings", **os.environ} + env = { + "RUSTFLAGS": "-Awarnings", + # "CARGO_TARGET_DIR": pytestconfig.cache.mkdir("target"), + **os.environ, + } # command to start process args = [ diff --git a/requirements.txt b/requirements.txt index bf75ee0..a493b40 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,10 @@ # Python requirements # Testing -pytest >= 7.1 -pytest-xprocess >= 0.18 -pytest-order >= 1.0.1 +pytest == 7.1 +pytest-xprocess >= 0.19 +pytest-order == 1.0.1 +lxml >= 4.9.1 # VISA framework pyvisa >= 1.11 diff --git a/telnet/Cargo.toml b/telnet/Cargo.toml index 1ecdec3..32fcefc 100644 --- a/telnet/Cargo.toml +++ b/telnet/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lxi-telnet" -description = "Raw LXI socket support library" +description = "LXI Telnet support library" license = "GPL-3.0-or-later" version = { workspace = true } authors = { workspace = true } @@ -14,14 +14,12 @@ async-std = { workspace = true } async-listen = { workspace = true } futures = { workspace = true } log = { workspace = true, features = ["kv_unstable_std"] } +libtelnet-rs = "2.0.0" [dependencies.lxi-device] path = "../device" version = "0.1.0" -[dependencies.libtelnet-rs] -version = "2.0.0" - [dev-dependencies] femme = { workspace = true } clap = { workspace = true } \ No newline at end of file diff --git a/telnet/examples/telnet.rs b/telnet/examples/telnet.rs index da08c45..c6b3110 100644 --- a/telnet/examples/telnet.rs +++ b/telnet/examples/telnet.rs @@ -37,11 +37,7 @@ async fn main() -> std::io::Result<()> { log::info!("Running server on port {}:{}...", args.ip, args.port); if let Some(t) = args.timeout { - timeout( - Duration::from_millis(t), - ipv4_server - ) - .await + timeout(Duration::from_millis(t), ipv4_server).await } else { ipv4_server.await } diff --git a/telnet/src/server/mod.rs b/telnet/src/server/mod.rs index 072cb26..905bf82 100644 --- a/telnet/src/server/mod.rs +++ b/telnet/src/server/mod.rs @@ -23,7 +23,6 @@ use lxi_device::{ pub struct Server(ServerConfig); impl Server { - /// Accept client connections pub async fn accept( self: Arc, @@ -114,7 +113,7 @@ impl Server { if instance.options.get_option(options::ECHO).local_state { stream.write_all(&[b]).await?; } - + if b == b'\n' { // Remove \r cmd.pop(); @@ -126,8 +125,8 @@ impl Server { cmd.clear(); // Send back response if any - if !resp.is_empty() { - let to_send = Parser::escape_iac(resp); + if let Some(data) = resp { + let to_send = Parser::escape_iac(data); stream.write_all(&to_send).await?; stream.write_all(b"\r\n").await?; } diff --git a/telnet/tests/conftest.py b/telnet/tests/conftest.py index cdacef2..f1b7491 100644 --- a/telnet/tests/conftest.py +++ b/telnet/tests/conftest.py @@ -2,22 +2,27 @@ import pytest from xprocess import ProcessStarter + @pytest.fixture -def telnet_example(xprocess, request, free_port): +def telnet_example(xprocess, request, pytestconfig, free_port): target = os.environ.get("DEBUG_TARGET") if target is not None: port = os.environ.get("TELNET_PORT", default="5024") yield (target, port) else: - port = free_port + port = port = os.environ.get("TELNET_PORT", default=str(free_port)) class Starter(ProcessStarter): # startup pattern pattern = "Running server" # Hide warnings - env = {"RUSTFLAGS": "-Awarnings", **os.environ} + env = { + "RUSTFLAGS": "-Awarnings", + # "CARGO_TARGET_DIR": pytestconfig.cache.mkdir("target"), + **os.environ, + } # command to start process args = [ diff --git a/vxi11/examples/vxi11.rs b/vxi11/examples/vxi11.rs index 5173fee..42cfb30 100644 --- a/vxi11/examples/vxi11.rs +++ b/vxi11/examples/vxi11.rs @@ -1,15 +1,16 @@ -use std::{io, net::Ipv4Addr, time::Duration}; +use std::{io, net::Ipv4Addr, sync::Arc, time::Duration}; use async_std::{ future::pending, net::TcpListener, task::{self, spawn}, }; -use futures::{try_join, FutureExt}; +use futures::{lock::Mutex, try_join, FutureExt}; use lxi_device::{ lock::SharedLock, status::Sender as StatusSender, - util::SimpleDevice, + util::{EchoDevice, SimpleDevice}, + Device, }; use lxi_vxi11::{ client::portmapper::prelude::*, @@ -43,8 +44,12 @@ async fn main() -> io::Result<()> { femme::with_level(log::LevelFilter::Debug); let args = Args::parse(); - let device = SimpleDevice::new_arc(); - let shared = SharedLock::new(); + let shared_lock0 = SharedLock::new(); + let device0: Arc>> = + Arc::new(Mutex::new(Box::new(SimpleDevice::new()))); + + let shared_lock1 = SharedLock::new(); + let device1: Arc>> = Arc::new(Mutex::new(Box::new(EchoDevice))); let core_listener = TcpListener::bind(args.core_addr).await?; let core_port = core_listener.local_addr()?.port(); @@ -80,7 +85,9 @@ async fn main() -> io::Result<()> { let (vxi11_core, vxi11_async) = VxiServerBuilder::new() .core_port(core_listener.local_addr()?.port()) .async_port(async_listener.local_addr()?.port()) - .build(shared, device, srq); + .device("inst0".to_string(), device0, shared_lock0) + .device("inst1".to_string(), device1, shared_lock1) + .build(srq); if args.register { let mut portmap = @@ -120,7 +127,8 @@ async fn main() -> io::Result<()> { DEVICE_ASYNC_VERSION, PORTMAPPER_PROT_TCP, async_listener.local_addr()?.port() as u32, - )]); + ), + ]); log::info!("Running portmap ..."); spawn(async move { diff --git a/vxi11/src/client/portmapper.rs b/vxi11/src/client/portmapper.rs index f0fd904..8297625 100644 --- a/vxi11/src/client/portmapper.rs +++ b/vxi11/src/client/portmapper.rs @@ -54,7 +54,6 @@ impl PortMapperClient { Ok(()) } - pub async fn null(&mut self) -> Result<(), RpcError> { self.0.call(PMAPPROC_NULL, ()).await } diff --git a/vxi11/src/common/onc_rpc/mod.rs b/vxi11/src/common/onc_rpc/mod.rs index 356561c..f60cc42 100644 --- a/vxi11/src/common/onc_rpc/mod.rs +++ b/vxi11/src/common/onc_rpc/mod.rs @@ -48,7 +48,7 @@ pub enum RpcError { impl From for RpcError { fn from(err: Error) -> Self { - return Self::Io(err); + Self::Io(err) } } @@ -137,7 +137,7 @@ pub(crate) trait RpcService { RpcError::Io(err) => return Err(err), RpcError::RpcMissmatch(_) => unreachable!(), RpcError::AuthError(_) => unreachable!(), - RpcError::Portmap => unreachable!(), + RpcError::Portmap => unreachable!(), } } else { xdr::AcceptStat::Success diff --git a/vxi11/src/common/onc_rpc/xdr.rs b/vxi11/src/common/onc_rpc/xdr.rs index c8465a6..761fa91 100644 --- a/vxi11/src/common/onc_rpc/xdr.rs +++ b/vxi11/src/common/onc_rpc/xdr.rs @@ -403,7 +403,7 @@ impl XdrDecode for Callbody { } } -#[derive(Debug, Default, PartialEq, PartialOrd)] +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord)] pub struct MissmatchInfo { pub low: u32, pub high: u32, diff --git a/vxi11/src/lib.rs b/vxi11/src/lib.rs index cf7e395..001e74a 100644 --- a/vxi11/src/lib.rs +++ b/vxi11/src/lib.rs @@ -1,4 +1,4 @@ -//! +//! //! pub(crate) mod common; diff --git a/vxi11/src/server/vxi11/abort_service.rs b/vxi11/src/server/vxi11/abort_service.rs index 1642841..a33283e 100644 --- a/vxi11/src/server/vxi11/abort_service.rs +++ b/vxi11/src/server/vxi11/abort_service.rs @@ -8,7 +8,11 @@ use std::{ use async_listen::ListenExt; use async_std::{net::TcpListener, task}; -use crate::common::{onc_rpc::prelude::*, vxi11::{self, xdr}, xdr::prelude::*}; +use crate::common::{ + onc_rpc::prelude::*, + vxi11::{self, xdr}, + xdr::prelude::*, +}; use futures::{lock::Mutex, StreamExt}; diff --git a/vxi11/src/server/vxi11/core_service.rs b/vxi11/src/server/vxi11/core_service.rs index b352b88..894df22 100644 --- a/vxi11/src/server/vxi11/core_service.rs +++ b/vxi11/src/server/vxi11/core_service.rs @@ -147,34 +147,35 @@ where max_recv_size: self.max_recv_size, }; - if parms.device.starts_with("inst") { - let (lid, mut link) = { - let mut inner = self.inner.lock().await; - inner.new_link() - }; - resp.lid = lid.into(); - - // Try to lock - if parms.lock_device { - let res = timeout( - Duration::from_millis(parms.lock_timeout as u64), - link.handle.async_acquire_exclusive(), - ) - .await - .map_or(Err(SharedLockError::Timeout), |f| f); - match res { - Ok(()) => { - log::debug!(peer=format!("{}", self.peer), link=lid; "Exclusive lock acquired") + let mut inner = self.inner.lock().await; + resp.error = match inner.new_link(&parms.device) { + Ok((lid, mut link)) => { + resp.lid = lid.into(); + + // Try to lock + if parms.lock_device { + let res = timeout( + Duration::from_millis(parms.lock_timeout as u64), + link.handle.async_acquire_exclusive(), + ) + .await + .map_or(Err(SharedLockError::Timeout), |f| f); + match res { + Ok(()) => { + log::debug!(peer=format!("{}", self.peer), link=lid; "Exclusive lock acquired") + } + Err(err) => resp.error = err.into(), } - Err(err) => resp.error = err.into(), } + log::debug!(peer=format!("{}", self.peer), link=lid; "New link: {}, client_id={}", parms.device, parms.client_id); + self.links.lock().await.insert(lid, link); + xdr::DeviceErrorCode::NoError } - log::debug!(peer=format!("{}", self.peer), link=lid; "New link: {}, client_id={}", parms.device, parms.client_id); - self.links.lock().await.insert(lid, link); - } else { - log::debug!(peer=format!("{}", self.peer); "Invalid device address: {}", parms.device); - resp.error = xdr::DeviceErrorCode::InvalidAddress; - } + Err(err) => { + log::debug!(peer=format!("{}", self.peer); "Failed to create new link, {:?}: {}", err, parms.device); + xdr::DeviceErrorCode::InvalidAddress + } + }; resp.write_xdr(ret)?; Ok(()) @@ -208,9 +209,10 @@ where resp.size = parms.data.0.len() as u32; if parms.flags.is_end() { - let v = dev.execute(&link.in_buf); - //log::debug!(link=parms.lid.0; "Execute {:?} -> {:?}", link.in_buf, v); - link.out_buf.extend(&v); + if let Some(v) = dev.execute(&link.in_buf) { + //log::debug!(link=parms.lid.0; "Execute {:?} -> {:?}", link.in_buf, v); + link.out_buf.extend(&v); + } link.in_buf.clear(); } xdr::DeviceErrorCode::NoError @@ -343,19 +345,23 @@ where flags=format!("{}", parms.flags); "Trigger"); - let mut resp = xdr::DeviceError::default(); - - resp.error = match get_link!(self.links, &parms.lid.0) { - Some(link) => { - let dev = - lock_device!(link.handle, parms.flags, parms.lock_timeout, link.abort); - - match dev { - Ok(mut d) => d.trigger(Source::Bus).into(), - Err(err) => err.into(), + let resp = xdr::DeviceError { + error: match get_link!(self.links, &parms.lid.0) { + Some(link) => { + let dev = lock_device!( + link.handle, + parms.flags, + parms.lock_timeout, + link.abort + ); + + match dev { + Ok(mut d) => d.trigger(Source::Bus).into(), + Err(err) => err.into(), + } } - } - None => xdr::DeviceErrorCode::InvalidLinkIdentifier, + None => xdr::DeviceErrorCode::InvalidLinkIdentifier, + }, }; // Write response @@ -373,21 +379,25 @@ where flags=format!("{}", parms.flags); "Clear"); - let mut resp = xdr::DeviceError::default(); - - resp.error = match get_link!(self.links, &parms.lid.0) { - Some(link) => { - link.clear(); + let resp = xdr::DeviceError { + error: match get_link!(self.links, &parms.lid.0) { + Some(link) => { + link.clear(); - let dev = - lock_device!(link.handle, parms.flags, parms.lock_timeout, link.abort); + let dev = lock_device!( + link.handle, + parms.flags, + parms.lock_timeout, + link.abort + ); - match dev { - Ok(mut d) => d.clear().into(), - Err(err) => err.into(), + match dev { + Ok(mut d) => d.clear().into(), + Err(err) => err.into(), + } } - } - None => xdr::DeviceErrorCode::InvalidLinkIdentifier, + None => xdr::DeviceErrorCode::InvalidLinkIdentifier, + }, }; // Write response @@ -405,19 +415,23 @@ where flags=format!("{}", parms.flags); "Local {}", proc == vxi11::DEVICE_REMOTE); - let mut resp = xdr::DeviceError::default(); - - resp.error = match get_link!(self.links, &parms.lid.0) { - Some(link) => { - let dev = - lock_device!(link.handle, parms.flags, parms.lock_timeout, link.abort); - - match dev { - Ok(mut d) => d.set_remote(proc == vxi11::DEVICE_REMOTE).into(), - Err(err) => err.into(), + let resp = xdr::DeviceError { + error: match get_link!(self.links, &parms.lid.0) { + Some(link) => { + let dev = lock_device!( + link.handle, + parms.flags, + parms.lock_timeout, + link.abort + ); + + match dev { + Ok(mut d) => d.set_remote(proc == vxi11::DEVICE_REMOTE).into(), + Err(err) => err.into(), + } } - } - None => xdr::DeviceErrorCode::InvalidLinkIdentifier, + None => xdr::DeviceErrorCode::InvalidLinkIdentifier, + }, }; // Write response @@ -434,19 +448,19 @@ where flags=format!("{}", parms.flags); "Lock"); - let mut resp = xdr::DeviceError::default(); - - resp.error = match get_link!(self.links, &parms.lid.0) { - Some(link) if parms.flags.is_waitlock() => select! { - d = timeout( - Duration::from_millis(parms.lock_timeout as u64), - link.handle.async_acquire_exclusive(), - ).fuse() => d.map_or(Err(SharedLockError::Timeout), |f| f), - _ = link.abort.next() => Err(SharedLockError::Aborted) - } - .into(), - Some(link) => link.handle.try_acquire_exclusive().into(), - None => xdr::DeviceErrorCode::InvalidLinkIdentifier, + let resp = xdr::DeviceError { + error: match get_link!(self.links, &parms.lid.0) { + Some(link) if parms.flags.is_waitlock() => select! { + d = timeout( + Duration::from_millis(parms.lock_timeout as u64), + link.handle.async_acquire_exclusive(), + ).fuse() => d.map_or(Err(SharedLockError::Timeout), |f| f), + _ = link.abort.next() => Err(SharedLockError::Aborted) + } + .into(), + Some(link) => link.handle.try_acquire_exclusive().into(), + None => xdr::DeviceErrorCode::InvalidLinkIdentifier, + }, }; log::trace!(link=parms.lid.0; "Lock {:?}", resp.error); @@ -462,14 +476,14 @@ where log::debug!(peer=format!("{}", self.peer), link=parms.0; "Unlock"); - let mut resp = xdr::DeviceError::default(); - - resp.error = match get_link!(self.links, &parms.0) { - Some(link) => match link.handle.try_release() { - Ok(_) => xdr::DeviceErrorCode::NoError, - Err(err) => err.into(), + let resp = xdr::DeviceError { + error: match get_link!(self.links, &parms.0) { + Some(link) => match link.handle.try_release() { + Ok(_) => xdr::DeviceErrorCode::NoError, + Err(err) => err.into(), + }, + None => xdr::DeviceErrorCode::InvalidLinkIdentifier, }, - None => xdr::DeviceErrorCode::InvalidLinkIdentifier, }; // Write response @@ -487,52 +501,56 @@ where log::debug!(peer=format!("{}", self.peer), link=parms.lid.0; "Disable srq"); } - let mut resp = xdr::DeviceError::default(); - - resp.error = match get_link!(self.links, &parms.lid.0) { - Some(link) => { - let old = if parms.enable { - let client = self.srq.clone(); - let mut inner = self.inner.lock().await; - let mut reader = inner.status.get_new_receiver(); - - // Spawn a new tasks which monitors srq events - let fut: JoinHandle> = task::spawn(async move { - // Wait for status event - while let Some(stb) = reader.next().await { - // Check if interrupt channel is open - let mut tmp = client.lock().await; - if let Some(client) = tmp.as_mut() { - log::debug!(link=parms.lid.0; "Sending service request, stb={stb}"); - - // Send SRQ RPC to host - if let Err(err) = client.device_intr_srq(&parms.handle.0).await { - log::error!(link=parms.lid.0; "Failed to send service request: {err:?}"); - return Err(err); + let resp = xdr::DeviceError { + error: match get_link!(self.links, &parms.lid.0) { + Some(link) => { + let old = if parms.enable { + let client = self.srq.clone(); + let mut inner = self.inner.lock().await; + let mut reader = inner.status.get_new_receiver(); + + // Spawn a new tasks which monitors srq events + let fut: JoinHandle> = task::spawn( + async move { + // Wait for status event + while let Some(stb) = reader.next().await { + // Check if interrupt channel is open + let mut tmp = client.lock().await; + if let Some(client) = tmp.as_mut() { + log::debug!(link=parms.lid.0; "Sending service request, stb={stb}"); + + // Send SRQ RPC to host + if let Err(err) = + client.device_intr_srq(&parms.handle.0).await + { + log::error!(link=parms.lid.0; "Failed to send service request: {err:?}"); + return Err(err); + } + } else { + log::error!(link=parms.lid.0; "Failed to send service request: No interrupt channel open"); + } } - } else { - log::error!(link=parms.lid.0; "Failed to send service request: No interrupt channel open"); - } - } - Ok(()) - }); - - // Replace any old srq task - link.srq_handle.replace(fut) - } else { - // Remove any old srq task - link.srq_handle.take() - }; - - // Cancel old SRQ task - if let Some(task) = old { - task.cancel().await; - log::debug!(peer=self.peer.to_string(), link=parms.lid.0; "Cancelled srq task"); - } + Ok(()) + }, + ); - xdr::DeviceErrorCode::NoError - } - None => xdr::DeviceErrorCode::InvalidLinkIdentifier, + // Replace any old srq task + link.srq_handle.replace(fut) + } else { + // Remove any old srq task + link.srq_handle.take() + }; + + // Cancel old SRQ task + if let Some(task) = old { + task.cancel().await; + log::debug!(peer=self.peer.to_string(), link=parms.lid.0; "Cancelled srq task"); + } + + xdr::DeviceErrorCode::NoError + } + None => xdr::DeviceErrorCode::InvalidLinkIdentifier, + }, }; resp.write_xdr(ret)?; @@ -560,17 +578,17 @@ where log::debug!(peer=format!("{}", self.peer), link=parms.0; "Destroy link"); - let mut resp = xdr::DeviceError::default(); - - resp.error = match get_link!(self.links, &parms.0) { - Some(link) => { - let mut inner = self.inner.lock().await; + let resp = xdr::DeviceError { + error: match get_link!(self.links, &parms.0) { + Some(link) => { + let mut inner = self.inner.lock().await; - link.handle.force_release(); - inner.remove_link(parms.0); - xdr::DeviceErrorCode::NoError - } - None => xdr::DeviceErrorCode::InvalidLinkIdentifier, + link.handle.force_release(); + inner.remove_link(parms.0); + xdr::DeviceErrorCode::NoError + } + None => xdr::DeviceErrorCode::InvalidLinkIdentifier, + }, }; resp.write_xdr(ret)?; @@ -593,21 +611,19 @@ where if srq.is_some() { resp.error = xdr::DeviceErrorCode::ChannelAlreadyEstablished + } else if let Ok(client) = VxiSrqClient::new( + parms.host_addr, + parms.host_port, + parms.prog_num, + parms.prog_vers, + parms.prog_family == xdr::DeviceAddrFamily::Udp, + ) + .await + { + srq.replace(client); + resp.error = xdr::DeviceErrorCode::NoError; } else { - if let Ok(client) = VxiSrqClient::new( - parms.host_addr, - parms.host_port, - parms.prog_num, - parms.prog_vers, - parms.prog_family == xdr::DeviceAddrFamily::Udp, - ) - .await - { - srq.replace(client); - resp.error = xdr::DeviceErrorCode::NoError; - } else { - resp.error = xdr::DeviceErrorCode::ChannelNotEstablished; - } + resp.error = xdr::DeviceErrorCode::ChannelNotEstablished; } resp.write_xdr(ret)?; diff --git a/vxi11/src/server/vxi11/mod.rs b/vxi11/src/server/vxi11/mod.rs index dfeec68..049517a 100644 --- a/vxi11/src/server/vxi11/mod.rs +++ b/vxi11/src/server/vxi11/mod.rs @@ -1,7 +1,4 @@ -use std::{ - collections::HashMap, - sync::Arc, -}; +use std::{collections::HashMap, sync::Arc}; use async_std::{net::ToSocketAddrs, task::JoinHandle}; @@ -24,9 +21,9 @@ use crate::{ }, }; -pub(crate) mod intr_client; -pub(crate) mod core_service; pub(crate) mod abort_service; +pub(crate) mod core_service; +pub(crate) mod intr_client; pub mod prelude { pub use super::{abort_service::VxiAsyncServer, core_service::VxiCoreServer, VxiServerBuilder}; @@ -36,7 +33,6 @@ pub mod prelude { }; } - use prelude::*; impl From for xdr::DeviceErrorCode { @@ -125,22 +121,22 @@ impl Drop for Link { } } +type DeviceMap = HashMap>, Arc>)>; + struct VxiInner { link_id: u32, links: HashMap>, - shared: Arc>, - device: Arc>, + devices: DeviceMap, status: StatusSender, } impl VxiInner { - fn new(shared: Arc>, device: Arc>, status: StatusSender) -> Arc> { + fn new(devices: DeviceMap, status: StatusSender) -> Arc> { Arc::new(Mutex::new(Self { link_id: 0, links: HashMap::default(), - shared, - device, - status + devices, + status, })) } @@ -152,12 +148,13 @@ impl VxiInner { self.link_id } - fn new_link(&mut self) -> (u32, Link) { + fn new_link(&mut self, subaddr: &String) -> Result<(u32, Link), ()> { let id = self.next_link_id(); - let handle = LockHandle::new(self.shared.clone(), self.device.clone()); + let (device, shared) = self.devices.get(subaddr).ok_or(())?; + let handle = LockHandle::new(shared.clone(), device.clone()); let (link, sender) = Link::new(id, handle); self.links.insert(id, sender); - (id, link) + Ok((id, link)) } fn remove_link(&mut self, lid: u32) { @@ -165,20 +162,27 @@ impl VxiInner { } } - /// Builder used to create a VXI11 server -pub struct VxiServerBuilder { +pub struct VxiServerBuilder { core_port: u16, async_port: u16, + devices: DeviceMap, } -impl VxiServerBuilder { - pub fn new() -> Self { +impl Default for VxiServerBuilder { + fn default() -> Self { Self { core_port: 4322, async_port: 4323, + devices: Default::default(), } } +} + +impl VxiServerBuilder { + pub fn new() -> Self { + Default::default() + } /// Set the vxi server core port. pub fn core_port(mut self, core_port: u16) -> Self { @@ -225,13 +229,21 @@ impl VxiServerBuilder { } } - pub fn build( + pub fn device( + mut self, + subaddr: String, + dev: Arc>, + shared_lock: Arc>, + ) -> Self { + self.devices.insert(subaddr, (dev, shared_lock)); + self + } + + pub fn build( self, - shared: Arc>, - device: Arc>, status: StatusSender, ) -> (Arc>, Arc>) { - let inner = VxiInner::new(shared, device, status); + let inner = VxiInner::new(self.devices, status); ( Arc::new(VxiCoreServer { inner: inner.clone(), @@ -239,7 +251,7 @@ impl VxiServerBuilder { max_recv_size: 128 * 1024, }), Arc::new(VxiAsyncServer { - inner: inner.clone(), + inner, async_port: self.async_port, }), ) diff --git a/vxi11/tests/conftest.py b/vxi11/tests/conftest.py index df372c5..c1d19ec 100644 --- a/vxi11/tests/conftest.py +++ b/vxi11/tests/conftest.py @@ -2,11 +2,12 @@ import pytest from xprocess import ProcessStarter + @pytest.fixture -def vxi11_example(xprocess, request): +def vxi11_example(xprocess, request, pytestconfig): target = os.environ.get("DEBUG_TARGET") if target is not None: - yield f"TCPIP::{target}::inst0::INSTR" + yield f"TCPIP::{target}" else: class Starter(ProcessStarter): @@ -14,7 +15,11 @@ class Starter(ProcessStarter): pattern = "Running server" # Hide warnings - env = {"RUSTFLAGS": "-Awarnings", **os.environ} + env = { + "RUSTFLAGS": "-Awarnings", + # "CARGO_TARGET_DIR": pytestconfig.cache.mkdir("target"), + **os.environ, + } # command to start process args = [ @@ -31,7 +36,7 @@ class Starter(ProcessStarter): name = request.function.__name__ xprocess.ensure(f"vxi11_example-{name}", Starter) - yield "TCPIP::localhost::inst0::INSTR" + yield "TCPIP::localhost" # clean up whole process tree afterwards xprocess.getinfo(f"vxi11_example-{name}").terminate() diff --git a/vxi11/tests/test_vxi11.py b/vxi11/tests/test_vxi11.py index 369191d..9624404 100644 --- a/vxi11/tests/test_vxi11.py +++ b/vxi11/tests/test_vxi11.py @@ -2,13 +2,17 @@ import pyvisa -def test_create_link(vxi11_example, resource_manager: pyvisa.ResourceManager): - inst = resource_manager.open_resource(vxi11_example) +def test_inst0(vxi11_example, resource_manager: pyvisa.ResourceManager): + inst = resource_manager.open_resource(f"{vxi11_example}::inst0::INSTR") + inst.close() + +def test_inst1(vxi11_example, resource_manager: pyvisa.ResourceManager): + inst = resource_manager.open_resource(f"{vxi11_example}::inst1::INSTR") inst.close() def test_query(vxi11_example, resource_manager: pyvisa.ResourceManager): - inst = resource_manager.open_resource(vxi11_example) + inst = resource_manager.open_resource(f"{vxi11_example}::inst0::INSTR") inst.read_termination = "" inst.write_termination = "" @@ -19,7 +23,7 @@ def test_query(vxi11_example, resource_manager: pyvisa.ResourceManager): def test_read_stb(vxi11_example, resource_manager: pyvisa.ResourceManager): - inst = resource_manager.open_resource(vxi11_example) + inst = resource_manager.open_resource(f"{vxi11_example}::inst0::INSTR") status = inst.read_stb() assert status == 0 @@ -28,7 +32,7 @@ def test_read_stb(vxi11_example, resource_manager: pyvisa.ResourceManager): def test_trigger(vxi11_example, resource_manager: pyvisa.ResourceManager): - inst = resource_manager.open_resource(vxi11_example) + inst = resource_manager.open_resource(f"{vxi11_example}::inst0::INSTR") status = inst.read_stb() assert status & 0x40 == 0 @@ -45,8 +49,8 @@ def test_trigger(vxi11_example, resource_manager: pyvisa.ResourceManager): def test_lock(vxi11_example, resource_manager: pyvisa.ResourceManager): - inst1 = resource_manager.open_resource(vxi11_example) - inst2 = resource_manager.open_resource(vxi11_example) + inst1 = resource_manager.open_resource(f"{vxi11_example}::inst0::INSTR") + inst2 = resource_manager.open_resource(f"{vxi11_example}::inst0::INSTR") # Two clients cannot lock at the same time inst1.lock_excl() From 45fcb0a07af9a279232ec9e0bda9e933fffa2019 Mon Sep 17 00:00:00 2001 From: Gustav Palmqvist Date: Tue, 8 Nov 2022 19:32:51 +0100 Subject: [PATCH 5/5] Shit doesn't work --- .gitignore | 1 + Cargo.toml | 2 +- README.md | 18 +- conftest.py | 8 - coverage.sh | 0 deny.toml | 270 ++++++++++++++++++++++ device/Cargo.toml | 3 +- hislip/README.md | 6 + hislip/examples/hislip.rs | 51 +++- hislip/src/common/descriptors.rs | 2 +- hislip/src/common/messages.rs | 20 ++ hislip/src/common/stream.rs | 14 +- hislip/src/server/config.rs | 32 ++- hislip/src/server/mod.rs | 34 ++- hislip/src/server/session/asynchronous.rs | 116 ++++++++-- hislip/src/server/session/mod.rs | 13 +- hislip/src/server/session/synchronous.rs | 98 +++++++- hislip/tests/conftest.py | 14 +- hislip/tests/test_hislip_v2.py | 4 + lxi-common/Cargo.toml | 15 ++ lxi-common/src/lib.rs | 18 ++ lxi-common/src/security.rs | 63 +++++ raw/Cargo.toml | 2 +- raw/examples/scpi-tls.rs | 49 +++- update_readme.sh | 0 25 files changed, 782 insertions(+), 71 deletions(-) mode change 100755 => 100644 coverage.sh create mode 100644 deny.toml create mode 100644 lxi-common/Cargo.toml create mode 100644 lxi-common/src/lib.rs create mode 100644 lxi-common/src/security.rs mode change 100755 => 100644 update_readme.sh diff --git a/.gitignore b/.gitignore index acb32ed..5d6e257 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ lcov.info # Certificates /.certificates +ssl.log diff --git a/Cargo.toml b/Cargo.toml index acf7ef5..8d3fbab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["device", "hislip", "raw", "telnet", "vxi11"] +members = ["device", "hislip", "raw", "telnet", "vxi11", "lxi-common"] [workspace.package] version = "0.1.0" diff --git a/README.md b/README.md index e0c51d3..059b096 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,25 @@ # lxi-rs This crate aims to simplify implementation of the [LXI Device Specification](https://www.lxistandard.org/Specifications/Specifications.aspx). -The specifications consists of a [core specification](https://www.lxistandard.org/members/Adopted%20Specifications/Latest%20Version%20of%20Standards_/LXI%20Standard%201.5%20Specifications/LXI%20Device%20Specification%20v1_5_01.pdf) and a optional set of extended functions. +The specifications consists of a [core specification](https://www.lxistandard.org/members/Adopted%20Specifications/Latest%20Version%20of%20Standards_/LXI%20Version%201.6/LXI_Device_Specification_1.6_2022-06-09.pdf) and a optional set of extended functions. -Currently the focus is on implementing HiSLIP/VXI-11/Socket protocols for Unix-like environments. A long-term goal is to support an async no-std environment like [](https://github.com/embassy-rs/embassy) +Currently the focus is on implementing HiSLIP/VXI-11/Socket protocols for Unix-like environments. A long-term goal is to support an async no-std environment like [embassy](https://github.com/embassy-rs/embassy) or [smol-tcp](). # Relevant standards: -* [IVI-6.1 High-Speed LAN Instrument Protocol (HiSLIP) v2.0](https://www.ivifoundation.org/specifications/) -* [VXI-11 REVISION v1.0](https://www.vxibus.org/specifications.html) -* [LXI Device specification v1.5](https://www.lxistandard.org/members/Adopted%20Specifications/Latest%20Version%20of%20Standards_/LXI%20Standard%201.5%20Specifications/LXI%20Device%20Specification%20v1_5_01.pdf) +* [LXI Device specification v1.6](https://www.lxistandard.org/members/Adopted%20Specifications/Latest%20Version%20of%20Standards_/LXI%20Version%201.6/LXI_Device_Specification_1.6_2022-06-09.pdf) # Scope This crate does not handle command parsing and/or execution, look at [scpi-rs](https://github.com/Atmelfan/scpi-rs)(:crab:) or [libscpi](https://github.com/j123b567/scpi-parser)(C) for that. +# Architecture +* [device](./device/) Common abstractions for the core device. Crate is `no-std` compatible [but do require alloc (TODO)](https://github.com/Atmelfan/lxi-rs/issues/3). +* [raw](./raw/) Server for Scpi-raw and Scpi-TLS protocols (`TCPIP::hostname::port::SOCKET`). +* [telnet](./telnet/) Server for Telnet protocol, mostly useful for interactive debugging. +* [hislip](./hislip/) HiSLIP v2.0 server, more modern VXI-11 replacement. See [IVI-6.1 High-Speed LAN Instrument Protocol (HiSLIP) v2.0](https://www.ivifoundation.org/specifications/). +* [vxi-11](./vxi11/) VXI-11 server. See [VXI-11 REVISION v1.0](https://www.vxibus.org/specifications.html). + + # Certificates Secure extensions and https server requires a certificate and key. @@ -38,6 +44,6 @@ This crate uses two types of tests, the cargo test framework and pytest. Cargo t 2. Run `./coverage --open` # Licensing -Lxi-rs is available under GPLv3 License, see [LICENSE-GPL](./LICENSE-GPL). +Lxi-rs is available under dual GPLv3 and commercial license, see [LICENSE-GPL](./LICENSE-GPL) and `TBD`. Core crates like [lxi-device](device) are licensed under MIT and APACHE version 2. diff --git a/conftest.py b/conftest.py index a248aa6..3c1ad3b 100644 --- a/conftest.py +++ b/conftest.py @@ -5,14 +5,6 @@ import socket from contextlib import closing -pytest.fixture(scope='session', autouse=True) -def prep_cargo(db, data): - print("Building...") - return_code = subprocess.call("cargo build --examples", shell=True) - # yield, to let all tests within the scope run - yield - - @pytest.fixture def free_port(request): with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s: diff --git a/coverage.sh b/coverage.sh old mode 100755 new mode 100644 diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000..b4abc8b --- /dev/null +++ b/deny.toml @@ -0,0 +1,270 @@ +# This template contains all of the possible sections and their default values + +# Note that all fields that take a lint level have these possible values: +# * deny - An error will be produced and the check will fail +# * warn - A warning will be produced, but the check will not fail +# * allow - No warning or error will be produced, though in some cases a note +# will be + +# The values provided in this template are the default values that will be used +# when any section or field is not specified in your own configuration + +# Root options + +# If 1 or more target triples (and optionally, target_features) are specified, +# only the specified targets will be checked when running `cargo deny check`. +# This means, if a particular package is only ever used as a target specific +# dependency, such as, for example, the `nix` crate only being used via the +# `target_family = "unix"` configuration, that only having windows targets in +# this list would mean the nix crate, as well as any of its exclusive +# dependencies not shared by any other crates, would be ignored, as the target +# list here is effectively saying which targets you are building for. +targets = [ + # The triple can be any string, but only the target triples built in to + # rustc (as of 1.40) can be checked against actual config expressions + #{ triple = "x86_64-unknown-linux-musl" }, + # You can also specify which target_features you promise are enabled for a + # particular target. target_features are currently not validated against + # the actual valid features supported by the target architecture. + #{ triple = "wasm32-unknown-unknown", features = ["atomics"] }, +] +# When creating the dependency graph used as the source of truth when checks are +# executed, this field can be used to prune crates from the graph, removing them +# from the view of cargo-deny. This is an extremely heavy hammer, as if a crate +# is pruned from the graph, all of its dependencies will also be pruned unless +# they are connected to another crate in the graph that hasn't been pruned, +# so it should be used with care. The identifiers are [Package ID Specifications] +# (https://doc.rust-lang.org/cargo/reference/pkgid-spec.html) +#exclude = [] +# If true, metadata will be collected with `--all-features`. Note that this can't +# be toggled off if true, if you want to conditionally enable `--all-features` it +# is recommended to pass `--all-features` on the cmd line instead +all-features = false +# If true, metadata will be collected with `--no-default-features`. The same +# caveat with `all-features` applies +no-default-features = false +# If set, these feature will be enabled when collecting metadata. If `--features` +# is specified on the cmd line they will take precedence over this option. +#features = [] +# When outputting inclusion graphs in diagnostics that include features, this +# option can be used to specify the depth at which feature edges will be added. +# This option is included since the graphs can be quite large and the addition +# of features from the crate(s) to all of the graph roots can be far too verbose. +# This option can be overridden via `--feature-depth` on the cmd line +feature-depth = 1 + +# This section is considered when running `cargo deny check advisories` +# More documentation for the advisories section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html +[advisories] +# The path where the advisory database is cloned/fetched into +db-path = "~/.cargo/advisory-db" +# The url(s) of the advisory databases to use +db-urls = ["https://github.com/rustsec/advisory-db"] +# The lint level for security vulnerabilities +vulnerability = "deny" +# The lint level for unmaintained crates +unmaintained = "warn" +# The lint level for crates that have been yanked from their source registry +yanked = "warn" +# The lint level for crates with security notices. Note that as of +# 2019-12-17 there are no security notice advisories in +# https://github.com/rustsec/advisory-db +notice = "warn" +# A list of advisory IDs to ignore. Note that ignored advisories will still +# output a note when they are encountered. +ignore = [ + #"RUSTSEC-0000-0000", +] +# Threshold for security vulnerabilities, any vulnerability with a CVSS score +# lower than the range specified will be ignored. Note that ignored advisories +# will still output a note when they are encountered. +# * None - CVSS Score 0.0 +# * Low - CVSS Score 0.1 - 3.9 +# * Medium - CVSS Score 4.0 - 6.9 +# * High - CVSS Score 7.0 - 8.9 +# * Critical - CVSS Score 9.0 - 10.0 +#severity-threshold = + +# If this is true, then cargo deny will use the git executable to fetch advisory database. +# If this is false, then it uses a built-in git library. +# Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support. +# See Git Authentication for more information about setting up git authentication. +#git-fetch-with-cli = true + +# This section is considered when running `cargo deny check licenses` +# More documentation for the licenses section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html +[licenses] +# The lint level for crates which do not have a detectable license +unlicensed = "deny" +# List of explicitly allowed licenses +# See https://spdx.org/licenses/ for list of possible licenses +# [possible values: any SPDX 3.11 short identifier (+ optional exception)]. +allow = [ + "MIT", + "Apache-2.0", + "Apache-2.0 WITH LLVM-exception", + "BSD-2-Clause", + "BSD-3-Clause", + "ISC", + "Unicode-DFS-2016", + "OpenSSL", +] +# List of explicitly disallowed licenses +# See https://spdx.org/licenses/ for list of possible licenses +# [possible values: any SPDX 3.11 short identifier (+ optional exception)]. +deny = [ + #"Nokia", +] +# Lint level for licenses considered copyleft +copyleft = "warn" +# Blanket approval or denial for OSI-approved or FSF Free/Libre licenses +# * both - The license will be approved if it is both OSI-approved *AND* FSF +# * either - The license will be approved if it is either OSI-approved *OR* FSF +# * osi-only - The license will be approved if is OSI-approved *AND NOT* FSF +# * fsf-only - The license will be approved if is FSF *AND NOT* OSI-approved +# * neither - This predicate is ignored and the default lint level is used +allow-osi-fsf-free = "neither" +# Lint level used when no other predicates are matched +# 1. License isn't in the allow or deny lists +# 2. License isn't copyleft +# 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither" +default = "deny" +# The confidence threshold for detecting a license from license text. +# The higher the value, the more closely the license text must be to the +# canonical license text of a valid SPDX license file. +# [possible values: any between 0.0 and 1.0]. +confidence-threshold = 0.8 +# Allow 1 or more licenses on a per-crate basis, so that particular licenses +# aren't accepted for every possible crate as with the normal allow list +exceptions = [ + # Each entry is the crate and version constraint, and its specific allow + # list + #{ allow = ["Zlib"], name = "adler32", version = "*" }, +] + +# Some crates don't have (easily) machine readable licensing information, +# adding a clarification entry for it allows you to manually specify the +# licensing information +[[licenses.clarify]] +name = "ring" +version = "*" +expression = "MIT AND ISC AND OpenSSL" +# One or more files in the crate's source used as the "source of truth" for +# the license expression. If the contents match, the clarification will be used +# when running the license check, otherwise the clarification will be ignored +# and the crate will be checked normally, which may produce warnings or errors +# depending on the rest of your configuration +license-files = [ + { path = "LICENSE", hash = 0xbd0eed23 } +] + +[licenses.private] +# If true, ignores workspace crates that aren't published, or are only +# published to private registries. +# To see how to mark a crate as unpublished (to the official registry), +# visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field. +ignore = false +# One or more private registries that you might publish crates to, if a crate +# is only published to private registries, and ignore is true, the crate will +# not have its license(s) checked +registries = [ + #"https://sekretz.com/registry +] + +# This section is considered when running `cargo deny check bans`. +# More documentation about the 'bans' section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html +[bans] +# Lint level for when multiple versions of the same crate are detected +multiple-versions = "warn" +# Lint level for when a crate version requirement is `*` +wildcards = "allow" +# The graph highlighting used when creating dotgraphs for crates +# with multiple versions +# * lowest-version - The path to the lowest versioned duplicate is highlighted +# * simplest-path - The path to the version with the fewest edges is highlighted +# * all - Both lowest-version and simplest-path are used +highlight = "all" +# The default lint level for `default` features for crates that are members of +# the workspace that is being checked. This can be overriden by allowing/denying +# `default` on a crate-by-crate basis if desired. +workspace-default-features = "allow" +# The default lint level for `default` features for external crates that are not +# members of the workspace. This can be overriden by allowing/denying `default` +# on a crate-by-crate basis if desired. +external-default-features = "allow" +# List of crates that are allowed. Use with care! +allow = [ + #{ name = "ansi_term", version = "=0.11.0" }, +] +# List of crates to deny +deny = [ + # Each entry the name of a crate and a version range. If version is + # not specified, all versions will be matched. + #{ name = "ansi_term", version = "=0.11.0" }, + # + # Wrapper crates can optionally be specified to allow the crate when it + # is a direct dependency of the otherwise banned crate + #{ name = "ansi_term", version = "=0.11.0", wrappers = [] }, +] + +# List of features to allow/deny +# Each entry the name of a crate and a version range. If version is +# not specified, all versions will be matched. +#[[bans.features]] +#name = "reqwest" +# Features to not allow +#deny = ["json"] +# Features to allow +#allow = [ +# "rustls", +# "__rustls", +# "__tls", +# "hyper-rustls", +# "rustls", +# "rustls-pemfile", +# "rustls-tls-webpki-roots", +# "tokio-rustls", +# "webpki-roots", +#] +# If true, the allowed features must exactly match the enabled feature set. If +# this is set there is no point setting `deny` +#exact = true + +# Certain crates/versions that will be skipped when doing duplicate detection. +skip = [ + #{ name = "ansi_term", version = "=0.11.0" }, +] +# Similarly to `skip` allows you to skip certain crates during duplicate +# detection. Unlike skip, it also includes the entire tree of transitive +# dependencies starting at the specified crate, up to a certain depth, which is +# by default infinite. +skip-tree = [ + #{ name = "ansi_term", version = "=0.11.0", depth = 20 }, +] + +# This section is considered when running `cargo deny check sources`. +# More documentation about the 'sources' section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html +[sources] +# Lint level for what to happen when a crate from a crate registry that is not +# in the allow list is encountered +unknown-registry = "warn" +# Lint level for what to happen when a crate from a git repository that is not +# in the allow list is encountered +unknown-git = "warn" +# List of URLs for allowed crate registries. Defaults to the crates.io index +# if not specified. If it is specified but empty, no registries are allowed. +allow-registry = ["https://github.com/rust-lang/crates.io-index"] +# List of URLs for allowed Git repositories +allow-git = [] + +[sources.allow-org] +# 1 or more github.com organizations to allow git sources for +#github = [""] +# 1 or more gitlab.com organizations to allow git sources for +#gitlab = [""] +# 1 or more bitbucket.org organizations to allow git sources for +#bitbucket = [""] diff --git a/device/Cargo.toml b/device/Cargo.toml index 74b713d..b970618 100644 --- a/device/Cargo.toml +++ b/device/Cargo.toml @@ -22,4 +22,5 @@ clap = { workspace = true } [features] default = [] std = [] -experimental = [] \ No newline at end of file +experimental = [] +security = ["std"] \ No newline at end of file diff --git a/hislip/README.md b/hislip/README.md index 3264a94..f1f2761 100644 --- a/hislip/README.md +++ b/hislip/README.md @@ -4,6 +4,12 @@ * Currently only supports overlapped mode * Asynchronous commands cannot be aborted +# Testing +On windows + +```TARGET=localhost CREDENTIALS=MyCred pytest -s``` + # License This crate is licensed under GPLv3 or later. See ([LICENSE-GPL](../LICENSE-GPL) or https://opensource.org/licenses/GPL-3.0) + diff --git a/hislip/examples/hislip.rs b/hislip/examples/hislip.rs index 14581b0..60af67b 100644 --- a/hislip/examples/hislip.rs +++ b/hislip/examples/hislip.rs @@ -1,4 +1,4 @@ -use std::{sync::Arc, time::Duration}; +use std::{fs::File, io::BufReader, sync::Arc, time::Duration}; use async_std::{ io::{self, timeout}, @@ -32,6 +32,20 @@ struct Args { /// Kill server after timeout (useful for coverage testing) #[clap(short, long)] timeout: Option, + + /// TLS certificate + #[cfg(feature = "secure-capability")] + #[clap(short, long, default_value = ".certificates/cert.pem")] + cert: String, + + /// TLS key + #[cfg(feature = "secure-capability")] + #[clap(short, long, default_value = ".certificates/key.pem")] + key: String, + + #[cfg(feature = "secure-capability")] + #[clap(long)] + client_cert: Vec, } struct DummySpawner; @@ -45,6 +59,20 @@ impl Spawn for DummySpawner { } } +#[cfg(feature = "secure-capability")] +fn load_certs(path: &str) -> io::Result> { + async_rustls::rustls::internal::pemfile::certs(&mut BufReader::new(File::open(path)?)) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid cert")) +} + +#[cfg(feature = "secure-capability")] +fn load_keys(path: &str) -> io::Result> { + async_rustls::rustls::internal::pemfile::pkcs8_private_keys(&mut BufReader::new(File::open( + path, + )?)) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid key")) +} + #[async_std::main] async fn main() -> Result<(), io::Error> { femme::with_level(log::LevelFilter::Debug); @@ -72,8 +100,25 @@ async fn main() -> Result<(), io::Error> { let config = ServerConfig::default().vendor_id(0x1234); let server = ServerBuilder::new(config) .device("hislip0".to_string(), device0, shared_lock0) - .device("hislip1".to_string(), device1, shared_lock1) - .build(); + .device("hislip1".to_string(), device1, shared_lock1); + + cfg_if::cfg_if! { + if #[cfg(feature = "secure-capability")] { + let certs = load_certs(&args.cert)?; + let mut keys = load_keys(&args.key)?; + + let mut config = + async_rustls::rustls::ServerConfig::new(async_rustls::rustls::NoClientAuth::new()); + config + .set_single_cert(certs, keys.remove(0)) + .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?; + config.key_log = Arc::new(async_rustls::rustls::KeyLogFile::new()); + let server = server.build(Arc::new(config)); + + } else { + let server = server.build(); + } + }; log::info!("Running server on port {}:{}...", args.ip, args.port); if let Some(t) = args.timeout { diff --git a/hislip/src/common/descriptors.rs b/hislip/src/common/descriptors.rs index 39b2c2b..0c963a5 100644 --- a/hislip/src/common/descriptors.rs +++ b/hislip/src/common/descriptors.rs @@ -47,7 +47,7 @@ impl Descriptor { pub fn write_descriptor(&self, writer: &mut W) -> io::Result<()> { match self { Descriptor::SupportedTlsVersions(versions) => { - writer.write_u16::(versions.len() as u16)?; + writer.write_u16::((versions.len()*2) as u16)?; writer.write_u8(0)?; for v in versions { writer.write_u16::(*v)?; diff --git a/hislip/src/common/messages.rs b/hislip/src/common/messages.rs index 960c427..8240255 100644 --- a/hislip/src/common/messages.rs +++ b/hislip/src/common/messages.rs @@ -461,3 +461,23 @@ pub(crate) enum ReleaseLockControl { SuccessShared = 2, Error = 3, } + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum TlsStatus { + Busy = 0, + Success = 1, + Error = 3, +} + +impl TryFrom for TlsStatus { + type Error = NonFatalErrorCode; + + fn try_from(value: u8) -> Result>::Error> { + match value { + 0 => Ok(Self::Busy), + 1 => Ok(Self::Success), + 3 => Ok(Self::Error), + _ => Err(NonFatalErrorCode::UnrecognizedControlCode) + } + } +} \ No newline at end of file diff --git a/hislip/src/common/stream.rs b/hislip/src/common/stream.rs index c9a8ccf..eebadb3 100644 --- a/hislip/src/common/stream.rs +++ b/hislip/src/common/stream.rs @@ -12,6 +12,16 @@ impl HislipStream { pub(crate) fn new(io: IO) -> Self { Self::Insecure(io) } + + pub(crate) fn is_secure(&self) -> bool { + cfg_if::cfg_if!{ + if #[cfg(feature = "secure-capability")] { + matches!(self, Self::Secure(..)) + } else { + false + } + } + } } impl HislipStream @@ -37,9 +47,9 @@ where } #[cfg(feature = "secure-capability")] - pub(crate) async fn end_tls(self) -> std::io::Result { + pub(crate) async fn end_tls(self) -> Result { match self { - HislipStream::Insecure(_) => Err(std::io::ErrorKind::Other.into()), + HislipStream::Insecure(_) => Err((std::io::ErrorKind::Other.into(), self)), HislipStream::Secure(mut _tls) => { let (_io, _session) = _tls.get_mut(); todo!("Implement end_tls when async-rustls is updated") diff --git a/hislip/src/server/config.rs b/hislip/src/server/config.rs index 79418c3..7a17453 100644 --- a/hislip/src/server/config.rs +++ b/hislip/src/server/config.rs @@ -1,4 +1,4 @@ -#[derive(Debug, Clone)] +#[derive(Clone)] pub struct ServerConfig { pub vendor_id: u16, /// Maximum server message size @@ -7,6 +7,12 @@ pub struct ServerConfig { pub prefer_overlap: bool, /// Maximum allowed number of sessions pub max_num_sessions: usize, + /// Force use of encryption and do do not allow clients to end encryption + #[cfg(feature="secure-capability")] + pub encryption_mandatory: bool, + /// Clients must encrypt/authenticate after initializing the session + #[cfg(feature="secure-capability")] + pub initial_encryption: bool, } impl ServerConfig { @@ -34,6 +40,24 @@ impl ServerConfig { self.prefer_overlap = false; self } + + #[cfg(feature="secure-capability")] + pub fn encryption_mandatory(mut self, encryption_mandatory: bool) -> Self { + self.encryption_mandatory = encryption_mandatory; + self + } + + #[cfg(feature="secure-capability")] + pub fn initial_encryption(mut self, initial_encryption: bool) -> Self { + self.initial_encryption = initial_encryption; + self + } + + #[cfg(feature="secure-capability")] + pub fn is_secure(&self) -> bool { + return self.encryption_mandatory && self.initial_encryption; + } + } impl Default for ServerConfig { @@ -43,6 +67,10 @@ impl Default for ServerConfig { max_message_size: 1024 * 1024, prefer_overlap: true, max_num_sessions: 64, + #[cfg(feature="secure-capability")] + encryption_mandatory: false, + #[cfg(feature="secure-capability")] + initial_encryption: false, } } -} \ No newline at end of file +} diff --git a/hislip/src/server/mod.rs b/hislip/src/server/mod.rs index 3a79e4c..b0c29b2 100644 --- a/hislip/src/server/mod.rs +++ b/hislip/src/server/mod.rs @@ -22,8 +22,8 @@ use crate::DEFAULT_DEVICE_SUBADRESS; pub use self::config::ServerConfig; use self::session::SessionHandle; -pub mod session; pub mod config; +pub mod session; type DeviceMap = HashMap>, Arc>)>; @@ -71,12 +71,20 @@ where self } - pub fn build(self) -> Arc> { + pub fn build( + self, + #[cfg(feature = "secure-capability")] tls_config: Arc, + ) -> Arc> { assert!( !self.devices.is_empty(), "Server must have one or more devices" ); - Server::with_config(self.config, self.devices) + Server::with_config( + self.config, + self.devices, + #[cfg(feature = "secure-capability")] + tls_config, + ) } } @@ -87,22 +95,31 @@ where inner: Arc>>, devices: DeviceMap, config: ServerConfig, + #[cfg(feature = "secure-capability")] + tls_acceptor: async_rustls::TlsAcceptor, } impl Server where DEV: Device + Send + 'static, { + #[cfg(not(feature = "secure-capability"))] pub fn new(devices: DeviceMap) -> Arc { let config = ServerConfig::default(); Self::with_config(config, devices) } - pub fn with_config(config: ServerConfig, devices: DeviceMap) -> Arc { + pub fn with_config( + config: ServerConfig, + devices: DeviceMap, + #[cfg(feature = "secure-capability")] tls_config: Arc, + ) -> Arc { Arc::new(Server { inner: InnerServer::new(config.max_num_sessions), config, devices, + #[cfg(feature = "secure-capability")] + tls_acceptor: async_rustls::TlsAcceptor::from(tls_config), }) } @@ -243,7 +260,9 @@ where shared, RemoteLockHandle::new(device), receiver, - protocol + protocol, + #[cfg(feature = "secure-capability")] + self.tls_acceptor.clone(), ) .handle_session(stream, peer.clone()) .await; @@ -321,8 +340,11 @@ where shared, device, sender, + protocol, + #[cfg(feature = "secure-capability")] + self.tls_acceptor.clone(), ) - .handle_session(stream, peer.clone(), srq, protocol) + .handle_session(stream, peer.clone(), srq) .await; log::debug!(peer=peer.to_string(), session_id=id; "Async session closed: {res:?}"); return res; diff --git a/hislip/src/server/session/asynchronous.rs b/hislip/src/server/session/asynchronous.rs index 12b303c..0d16ebf 100644 --- a/hislip/src/server/session/asynchronous.rs +++ b/hislip/src/server/session/asynchronous.rs @@ -13,8 +13,9 @@ use futures::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, Stream}; use lxi_device::lock::{LockHandle, SharedLockError, SharedLockMode, SpinMutex}; use lxi_device::{Device, DeviceError}; +use crate::common::descriptors::Descriptor; use crate::common::errors::{Error, FatalErrorCode, NonFatalErrorCode}; -use crate::common::messages::{prelude::*, send_fatal, send_nonfatal}; +use crate::common::messages::{prelude::*, send_fatal, send_nonfatal, TlsStatus}; use crate::common::stream::HislipStream; use crate::common::{Protocol, PROTOCOL_2_0}; @@ -37,6 +38,11 @@ where handle: Arc>>, clear: Sender<()>, + + protocol: Protocol, + + #[cfg(feature = "secure-capability")] + acceptor: async_rustls::TlsAcceptor, } impl AsyncSession @@ -49,6 +55,8 @@ where shared: Arc>, handle: Arc>>, clear: Sender<()>, + protocol: Protocol, + #[cfg(feature = "secure-capability")] acceptor: async_rustls::TlsAcceptor, ) -> Self { Self { id, @@ -56,6 +64,9 @@ where shared, handle, clear, + protocol, + #[cfg(feature = "secure-capability")] + acceptor, } } @@ -64,7 +75,6 @@ where mut stream: HislipStream, peer: String, mut srq: SRQ, - protocol: Protocol, ) -> Result<(), io::Error> where S: AsyncRead + AsyncWrite + Unpin, @@ -92,14 +102,14 @@ where .await?; srq_bit = true; } - }, + } // Srq is closed, server is shutting down None => { log::info!(peer=peer.to_string(), session_id=self.id; "Server shutting down..."); - return Ok(()) - }, + return Ok(()); + } } - // Finish receiving message + // Finish receiving message // This is important as dropping the future mid-message can corrupt the datastream msg.await } @@ -394,12 +404,52 @@ where .write_to(&mut stream) .await?; } + Message { + message_type: MessageType::GetDescriptors, + control_code, + message_parameter, + .. + } if self.protocol >= PROTOCOL_2_0 => { + log::debug!(session_id=self.id, control_code=control_code, message_parameter=message_parameter; "Get descriptors (async)"); + + let mut payload = Vec::new(); + Descriptor::SupportedTlsVersions(vec![0x0303, 0x0304]) + .write_descriptor(&mut payload)?; + Descriptor::TlsInformation(b"1.2".to_vec()) + .write_descriptor(&mut payload)?; + Descriptor::TlsLastError(b"OK".to_vec()) + .write_descriptor(&mut payload)?; + + log::debug!(session_id=self.id, control_code=control_code, message_parameter=message_parameter; "Response -> {payload:?}"); + + MessageType::GetDescriptorsResponse + .message_params(0, 0) + .with_payload(payload) + .write_to(&mut stream) + .await?; + } + #[cfg(not(feature = "secure-capability"))] + Message { + message_type: + MessageType::AsyncStartTLS + | MessageType::AsyncEndTLS, + .. + } if self.protocol >= PROTOCOL_2_0 => { + send_fatal!( + &mut stream, + FatalErrorCode::SecureConnectionFailed, + "Secure capablity not supported" + ) + } + #[cfg(feature = "secure-capability")] Message { message_type: MessageType::AsyncStartTLS, control_code, message_parameter, payload, - } if protocol >= PROTOCOL_2_0 && cfg!(feature = "secure-capability") => { + } if self.protocol >= PROTOCOL_2_0 => { + let shared = self.shared.lock().await; + if payload.len() != 4 { send_fatal!(peer=peer.to_string(), session_id=self.id; &mut stream, FatalErrorCode::PoorlyFormattedMessageHeader, @@ -413,21 +463,51 @@ where log::debug!(session_id=self.id, message_id_sent=message_id_sent, message_id_read=message_id_read; "Start async TLS"); - // TODO: Encryption support - //stream = stream.start_tls(acceptor)?; - send_fatal!( - &mut stream, - FatalErrorCode::SecureConnectionFailed, - "Secure connection not supported" - ) + let control = if stream.is_secure() { + TlsStatus::Error + } else if message_id_sent != shared.read_message_id + || message_id_read != shared.sent_message_id + { + TlsStatus::Busy + } else { + TlsStatus::Success + }; + log::trace!("Response = {control:?}"); + + // Drop the shared object to avoid blocking + drop(shared); + + MessageType::AsyncStartTLSResponse + .message_params(control as u8, 0) + .no_payload() + .write_to(&mut stream) + .await?; + + if control == TlsStatus::Success { + // Switch over to TLS + stream = match stream.start_tls(&mut self.acceptor.clone()).await { + Ok(stream) => stream, + Err((err, mut stream)) => { + send_fatal!( + &mut stream, + FatalErrorCode::SecureConnectionFailed, + "Failed to establish TLS connections: {err}" + ) + } + }; + log::info!(session_id=self.id; "Async channel switched to TLS") + } else { + log::error!(session_id=self.id; "Failed to switch async session to TLS: {control:?}") + } } + #[cfg(feature = "secure-capability")] Message { message_type: MessageType::AsyncEndTLS, control_code, message_parameter, payload, - } if protocol >= PROTOCOL_2_0 => { - // Only supported >= 2.0 + } if self.protocol >= PROTOCOL_2_0 => { + // Only supported >= 2.0 and supports secure-capability let _control = RmtDeliveredControl(control_code); let message_id_sent = message_parameter; @@ -442,9 +522,7 @@ where "Secure connection not supported" ) } - Message { - message_type, .. - } => { + Message { message_type, .. } => { send_nonfatal!(peer=peer.to_string(), session_id=self.id; &mut stream, NonFatalErrorCode::UnrecognizedMessageType, "Unexpected {message_type:?} in asynchronous channel", diff --git a/hislip/src/server/session/mod.rs b/hislip/src/server/session/mod.rs index 911aa75..36dbc32 100644 --- a/hislip/src/server/session/mod.rs +++ b/hislip/src/server/session/mod.rs @@ -1,7 +1,10 @@ use std::sync::Weak; -use async_std::{channel::{self, Receiver, Sender}}; -use lxi_device::{Device, lock::{Mutex, SpinMutex, LockHandle}}; +use async_std::channel::{self, Receiver, Sender}; +use lxi_device::{ + lock::{LockHandle, Mutex, SpinMutex}, + Device, +}; use super::ServerConfig; use crate::common::Protocol; @@ -51,9 +54,9 @@ impl SharedSession { mode: SessionMode::Overlapped, max_message_size: 256, clear: channel::bounded(1), - read_message_id: 0, + read_message_id: 0xffff_fefe, enable_remote: true, - sent_message_id: 0, + sent_message_id: 0xffff_fefe, } } @@ -123,4 +126,4 @@ where pub(crate) fn active(&self) -> bool { self.shared.strong_count() > 0 && self.device.strong_count() > 0 } -} \ No newline at end of file +} diff --git a/hislip/src/server/session/synchronous.rs b/hislip/src/server/session/synchronous.rs index 5c10c52..5677b8d 100644 --- a/hislip/src/server/session/synchronous.rs +++ b/hislip/src/server/session/synchronous.rs @@ -11,8 +11,10 @@ use lxi_device::lock::RemoteLockHandle; use lxi_device::trigger::Source; use lxi_device::Device; +use crate::common::descriptors::Descriptor; use crate::common::errors::{Error, FatalErrorCode, NonFatalErrorCode}; use crate::common::messages::{prelude::*, send_fatal, send_nonfatal}; +use crate::common::stream::HislipStream; use crate::common::{Protocol, PROTOCOL_2_0}; use super::{ServerConfig, SharedSession}; @@ -37,6 +39,9 @@ where clear: Receiver<()>, protocol: Protocol, + + #[cfg(feature = "secure-capability")] + acceptor: async_rustls::TlsAcceptor, } impl SyncSession @@ -50,6 +55,7 @@ where handle: RemoteLockHandle, clear: Receiver<()>, protocol: Protocol, + #[cfg(feature = "secure-capability")] acceptor: async_rustls::TlsAcceptor, ) -> Self { Self { id, @@ -58,6 +64,8 @@ where handle, clear, protocol, + #[cfg(feature = "secure-capability")] + acceptor, } } @@ -97,7 +105,7 @@ where pub(crate) async fn handle_session( self, - mut stream: S, + mut stream: HislipStream, peer: String, ) -> Result<(), io::Error> where @@ -323,22 +331,94 @@ where }, Message { message_type: MessageType::GetDescriptors, + control_code, + message_parameter, .. } if self.protocol >= PROTOCOL_2_0 => { - todo!() + log::debug!(session_id=self.id, control_code=control_code, message_parameter=message_parameter; "Get descriptors (sync)"); + + let mut payload = Vec::new(); + Descriptor::SupportedTlsVersions(vec![0x0303, 0x0304]) + .write_descriptor(&mut payload)?; + Descriptor::TlsInformation(b"1.2".to_vec()) + .write_descriptor(&mut payload)?; + Descriptor::TlsLastError(b"OK".to_vec()) + .write_descriptor(&mut payload)?; + + log::debug!(session_id=self.id, control_code=control_code, message_parameter=message_parameter; "Response -> {payload:?}"); + + MessageType::GetDescriptorsResponse + .message_params(0, 0) + .with_payload(payload) + .write_to(&mut stream) + .await?; } + #[cfg(not(feature = "secure-capability"))] Message { - message_type: MessageType::StartTLS | MessageType::EndTLS, + message_type: + MessageType::StartTLS + | MessageType::EndTLS + | MessageType::GetSaslMechanismList + | MessageType::AuthenticationStart + | MessageType::AuthenticationExchange, .. } if self.protocol >= PROTOCOL_2_0 => { - log::debug!(peer=peer.to_string(), session_id=self.id; "Start/end TLS"); - send_fatal!( &mut stream, FatalErrorCode::SecureConnectionFailed, - "Secure connection not supported" + "Secure capablity not supported" ) } + #[cfg(feature = "secure-capability")] + Message { + message_type: MessageType::StartTLS, + .. + } if self.protocol >= PROTOCOL_2_0 => { + log::debug!(session_id=self.id; "Sync start TLS"); + + // Switch over to TLS + stream = match stream.start_tls(&mut self.acceptor.clone()).await { + Ok(stream) => stream, + Err((err, mut stream)) => { + send_fatal!( + &mut stream, + FatalErrorCode::SecureConnectionFailed, + "Failed to establish TLS connections: {err}" + ) + } + }; + log::info!(session_id=self.id; "Sync channel switched to TLS") + } + #[cfg(feature = "secure-capability")] + Message { + message_type: MessageType::EndTLS, + .. + } if self.protocol >= PROTOCOL_2_0 => { + log::debug!(session_id=self.id; "Sync end TLS"); + + // Disconnect client if encrption is mandatory + if self.config.encryption_mandatory { + send_fatal!( + &mut stream, + FatalErrorCode::SecureConnectionFailed, + "Authentication not supported" + ) + } + + // Switch off TLS + stream = match stream.end_tls().await { + Ok(stream) => stream, + Err((err, mut stream)) => { + send_fatal!( + &mut stream, + FatalErrorCode::SecureConnectionFailed, + "Failed to end TLS connections: {err}" + ) + } + }; + log::info!(session_id=self.id; "Sync channel switched to TLS") + } + #[cfg(feature = "secure-capability")] Message { message_type: MessageType::GetSaslMechanismList @@ -349,11 +429,7 @@ where } if self.protocol >= PROTOCOL_2_0 => { log::debug!(peer=peer.to_string(), session_id=self.id; "Authentication Start/Exchange"); - send_fatal!( - &mut stream, - FatalErrorCode::SecureConnectionFailed, - "Authentication not supported" - ) + todo!() } msg => { send_nonfatal!(peer=peer.to_string(), session_id=self.id; diff --git a/hislip/tests/conftest.py b/hislip/tests/conftest.py index a44c588..d9dba58 100644 --- a/hislip/tests/conftest.py +++ b/hislip/tests/conftest.py @@ -6,13 +6,21 @@ @pytest.fixture def hislip_example(xprocess, request, pytestconfig, free_port): target = os.environ.get("DEBUG_TARGET") + + # Add credentials if set + credentials = os.environ.get("HISLIP_CRED") + if credentials is not None: + prefix = f"{credentials}@" + else: + prefix = "" + if target is not None: port = os.environ.get("HISLIP_PORT") if port is not None: - yield f"TCPIP::{target}::hislip0,{port}::INSTR" + yield f"TCPIP::{prefix}{target}::hislip0,{port}::INSTR" else: - yield f"TCPIP::{target}::hislip0::INSTR" + yield f"TCPIP::{prefix}{target}::hislip0::INSTR" else: port = os.environ.get("HISLIP_PORT", default=str(free_port)) @@ -44,7 +52,7 @@ class Starter(ProcessStarter): name = request.function.__name__ xprocess.ensure(f"hislip_example-{name}", Starter) - yield f"TCPIP::localhost::hislip0,{port}::INSTR" + yield f"TCPIP::{prefix}localhost::hislip0,{port}::INSTR" # clean up whole process tree afterwards xprocess.getinfo(f"hislip_example-{name}").terminate() diff --git a/hislip/tests/test_hislip_v2.py b/hislip/tests/test_hislip_v2.py index 0344b71..0c9be1b 100644 --- a/hislip/tests/test_hislip_v2.py +++ b/hislip/tests/test_hislip_v2.py @@ -1,3 +1,7 @@ +# Only works with Keysight IO Libraries and Secure communications expert +# See README on how to setup +# + from pyvisa import highlevel diff --git a/lxi-common/Cargo.toml b/lxi-common/Cargo.toml new file mode 100644 index 0000000..7103d64 --- /dev/null +++ b/lxi-common/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "lxi-common" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +digest = { version = "0.10", optional = true } +sha1 = { version = "0.10", optional = true } +sha2 = { version = "0.10", optional = true } + +[features] +default = ["security"] +security = ["dep:digest", "dep:sha1", "dep:sha2"] diff --git a/lxi-common/src/lib.rs b/lxi-common/src/lib.rs new file mode 100644 index 0000000..c118405 --- /dev/null +++ b/lxi-common/src/lib.rs @@ -0,0 +1,18 @@ + + +pub mod security; + +pub fn add(left: usize, right: usize) -> usize { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/lxi-common/src/security.rs b/lxi-common/src/security.rs new file mode 100644 index 0000000..d8db9e4 --- /dev/null +++ b/lxi-common/src/security.rs @@ -0,0 +1,63 @@ +use digest::Digest; + +struct ClientAuthentication {} + + +/// Algorithm used to calculate thumbprint +#[non_exhaustive] +#[derive(Debug, Clone, Copy)] +pub enum ThumbprintHash { + Sha1, + Sha224, + Sha256, + Sha384, + Sha512, +} + +impl ThumbprintHash { + pub fn from_str(hash: &str) -> Option { + match hash { + "sha-1" | "SHA-1" => Some(Self::Sha1), + "sha-224" | "SHA-224" => Some(Self::Sha224), + "sha-256" | "SHA-256" => Some(Self::Sha256), + "sha-384" | "SHA-384" => Some(Self::Sha384), + "sha-512" | "SHA-512" => Some(Self::Sha512), + _ => None, + } + } + + pub fn digest(&self, data: &[u8]) -> Vec { + match self { + ThumbprintHash::Sha1 => sha1::Sha1::digest(data).to_vec(), + ThumbprintHash::Sha224 => sha2::Sha224::digest(data).to_vec(), + ThumbprintHash::Sha256 => sha2::Sha256::digest(data).to_vec(), + ThumbprintHash::Sha384 => sha2::Sha384::digest(data).to_vec(), + ThumbprintHash::Sha512 => sha2::Sha512::digest(data).to_vec(), + } + } +} + +pub struct CertificateThumbprint { + hash: ThumbprintHash, + thumbprint: Vec, +} + +impl CertificateThumbprint { + pub fn new(hash: ThumbprintHash, thumbprint: Vec) -> Self { + Self { hash, thumbprint } + } + + pub fn new_from_hash_name(hash_name: &str, thumbprint: Vec) -> Option { + Some(Self { + hash: ThumbprintHash::from_str(hash_name)?, + thumbprint, + }) + } + + pub fn eq_certificate(&self, cert: &[u8]) -> bool { + let cert_hash = self.hash.digest(cert); + + // Compare the calculated hash to our thumbprint + cert_hash.as_slice() == self.thumbprint.as_slice() + } +} diff --git a/raw/Cargo.toml b/raw/Cargo.toml index 116faf2..c40a522 100644 --- a/raw/Cargo.toml +++ b/raw/Cargo.toml @@ -14,7 +14,7 @@ async-std = { workspace = true } async-listen = { workspace = true } futures = { workspace = true } log = { workspace = true, features = ["kv_unstable_std"] } -async-rustls = { workspace = true, optional = true } +async-rustls = { workspace = true, features = ["dangerous_configuration"], optional = true } [dependencies.lxi-device] path = "../device" diff --git a/raw/examples/scpi-tls.rs b/raw/examples/scpi-tls.rs index 37539ba..04de135 100644 --- a/raw/examples/scpi-tls.rs +++ b/raw/examples/scpi-tls.rs @@ -65,12 +65,57 @@ fn load_keys(path: &str) -> io::Result> { Err(_) => match pkcs8_private_keys(&mut BufReader::new(File::open(path)?)) { Ok(keys) => Ok(keys), Err(_) => { - return Err(io::Error::new(io::ErrorKind::InvalidInput, "Invalid key, expected RSA or PKCS#8 in PEM format")) - }, + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Invalid key, expected RSA or PKCS#8 in PEM format", + )) + } }, } } +struct LxiClientCertVerifier { + inner: V, + thumbprints: Vec<()> +} + +impl LxiClientCertVerifier { + fn get_certificate_thumbprint() { + + } +} + +impl async_rustls::rustls::ClientCertVerifier for LxiClientCertVerifier +where + V: async_rustls::rustls::ClientCertVerifier, +{ + fn offer_client_auth(&self) -> bool { + self.inner.offer_client_auth() + } + + fn client_auth_mandatory(&self, sni: Option<&async_rustls::webpki::DNSName>) -> Option { + self.inner.client_auth_mandatory(sni) + } + + fn client_auth_root_subjects( + &self, + sni: Option<&async_rustls::webpki::DNSName>, + ) -> Option { + self.inner.client_auth_root_subjects(sni) + } + + fn verify_client_cert( + &self, + presented_certs: &[Certificate], + sni: Option<&async_rustls::webpki::DNSName>, + ) -> Result { + if let Some(end_cert) = presented_certs.first() { + //end_cert + } + self.inner.verify_client_cert(presented_certs, sni) + } +} + /// Configure the server using rusttls /// See https://docs.rs/rustls/0.16.0/rustls/struct.ServerConfig.html for details /// diff --git a/update_readme.sh b/update_readme.sh old mode 100755 new mode 100644