From d0cc8b590c446045d42db91e878a2aae9949d947 Mon Sep 17 00:00:00 2001 From: Joe Ellis Date: Thu, 23 Jul 2020 12:01:37 +0100 Subject: [PATCH] Create `Connection` abstraction for client communication This commit is in response to issue #199. Here, we introduce a `Connection` struct which currently contains two things: * `stream` -- an object representing the communication stream between client and service. * `metadata` -- an optional enum instance that captures metadata about the connection. This abstraction allows us to carry more information forwards toward the frontend/authenticator/... . Specifically, this abstraction was created with UNIX domain sockets in mind (but the usefulness is not limited here). UNIX domain sockets allow incoming connections to be queried for peer metadata, which is a triple (uid, gid, pid) of the peer process that is connecting. Under certain configurations, this can be used for authentication. This commit places us in a position of being able to use said metadata for authentication if needed. Signed-off-by: Joe Ellis --- .../direct_authenticator/mod.rs | 16 +++++++++++---- src/authenticators/mod.rs | 12 +++++++++-- src/bin/main.rs | 4 ++-- src/front/domain_socket.rs | 11 +++++++--- src/front/front_end.rs | 12 +++++------ src/front/listener.rs | 20 ++++++++++++++++++- 6 files changed, 57 insertions(+), 18 deletions(-) diff --git a/src/authenticators/direct_authenticator/mod.rs b/src/authenticators/direct_authenticator/mod.rs index 00f8d3d9..ebe2d941 100644 --- a/src/authenticators/direct_authenticator/mod.rs +++ b/src/authenticators/direct_authenticator/mod.rs @@ -10,6 +10,7 @@ use super::ApplicationName; use super::Authenticate; +use crate::front::listener::ConnectionMetadata; use log::error; use parsec_interface::requests::request::RequestAuth; use parsec_interface::requests::{ResponseStatus, Result}; @@ -20,7 +21,11 @@ use std::str; pub struct DirectAuthenticator; impl Authenticate for DirectAuthenticator { - fn authenticate(&self, auth: &RequestAuth) -> Result { + fn authenticate( + &self, + auth: &RequestAuth, + _: Option, + ) -> Result { if auth.buffer.expose_secret().is_empty() { error!("The direct authenticator does not expect empty authentication values."); Err(ResponseStatus::AuthenticationError) @@ -49,9 +54,10 @@ mod test { let app_name = "app_name".to_string(); let req_auth = RequestAuth::new(app_name.clone().into_bytes()); + let conn_metadata = None; let auth_name = authenticator - .authenticate(&req_auth) + .authenticate(&req_auth, conn_metadata) .expect("Failed to authenticate"); assert_eq!(auth_name.get_name(), app_name); @@ -60,8 +66,9 @@ mod test { #[test] fn failed_authentication() { let authenticator = DirectAuthenticator {}; + let conn_metadata = None; let status = authenticator - .authenticate(&RequestAuth::new(vec![0xff; 5])) + .authenticate(&RequestAuth::new(vec![0xff; 5]), conn_metadata) .expect_err("Authentication should have failed"); assert_eq!(status, ResponseStatus::AuthenticationError); @@ -70,8 +77,9 @@ mod test { #[test] fn empty_auth() { let authenticator = DirectAuthenticator {}; + let conn_metadata = None; let status = authenticator - .authenticate(&RequestAuth::new(Vec::new())) + .authenticate(&RequestAuth::new(Vec::new()), conn_metadata) .expect_err("Empty auth should have failed"); assert_eq!(status, ResponseStatus::AuthenticationError); diff --git a/src/authenticators/mod.rs b/src/authenticators/mod.rs index d3ef653e..2c66f4ef 100644 --- a/src/authenticators/mod.rs +++ b/src/authenticators/mod.rs @@ -13,6 +13,7 @@ pub mod direct_authenticator; +use crate::front::listener::ConnectionMetadata; use parsec_interface::requests::request::RequestAuth; use parsec_interface::requests::Result; @@ -24,12 +25,19 @@ pub struct ApplicationName(String); /// /// Interface that must be implemented for each authentication type available for the service. pub trait Authenticate { - /// Authenticates a `RequestAuth` payload and returns the `ApplicationName` if successfull. + /// Authenticates a `RequestAuth` payload and returns the `ApplicationName` if successful. A + /// optional `ConnectionMetadata` object is passed in too, since it is sometimes possible to + /// perform authentication based on the connection's metadata (i.e. as is the case for UNIX + /// domain sockets with peer credentials). /// /// # Errors /// /// If the authentification fails, returns a `ResponseStatus::AuthenticationError`. - fn authenticate(&self, auth: &RequestAuth) -> Result; + fn authenticate( + &self, + auth: &RequestAuth, + meta: Option, + ) -> Result; } impl ApplicationName { diff --git a/src/bin/main.rs b/src/bin/main.rs index 9c03ec07..64bd9168 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -125,10 +125,10 @@ fn main() -> Result<()> { info!("Parsec configuration reloaded."); } - if let Some(stream) = listener.accept() { + if let Some(connection) = listener.accept() { let front_end_handler = front_end_handler.clone(); threadpool.execute(move || { - front_end_handler.handle_request(stream); + front_end_handler.handle_request(connection); trace!("handle_request egress"); }); } else { diff --git a/src/front/domain_socket.rs b/src/front/domain_socket.rs index 2b36867c..9caf915f 100644 --- a/src/front/domain_socket.rs +++ b/src/front/domain_socket.rs @@ -5,8 +5,8 @@ //! Expose Parsec functionality using Unix domain sockets as an IPC layer. //! The local socket is created at a predefined location. use super::listener; +use listener::Connection; use listener::Listen; -use listener::ReadWrite; use log::error; use std::fs; use std::fs::Permissions; @@ -91,7 +91,7 @@ impl Listen for DomainSocketListener { self.timeout = duration; } - fn accept(&self) -> Option> { + fn accept(&self) -> Option { let stream_result = self.listener.accept(); match stream_result { Ok((stream, _)) => { @@ -105,7 +105,12 @@ impl Listen for DomainSocketListener { format_error!("Failed to set stream as blocking", err); None } else { - Some(Box::from(stream)) + Some(Connection { + stream: Box::new(stream), + // TODO: when possible, we want to replace this with the (uid, gid, pid) + // triple for peer credentials. See listener.rs. + metadata: None, + }) } } Err(err) => { diff --git a/src/front/front_end.rs b/src/front/front_end.rs index 57f09368..62b33076 100644 --- a/src/front/front_end.rs +++ b/src/front/front_end.rs @@ -6,6 +6,7 @@ //! pass them to the rest of the service and write the responses back. use crate::authenticators::Authenticate; use crate::back::dispatcher::Dispatcher; +use crate::front::listener::Connection; use derivative::Derivative; use log::{info, trace}; use parsec_interface::requests::AuthType; @@ -13,7 +14,6 @@ use parsec_interface::requests::ResponseStatus; use parsec_interface::requests::{Request, Response}; use std::collections::HashMap; use std::io::{Error, ErrorKind, Result}; -use std::io::{Read, Write}; /// Read and verify request from IPC stream /// @@ -40,17 +40,17 @@ impl FrontEndHandler { /// /// If an error occurs during (un)marshalling, no operation will be performed and the /// method will return. - pub fn handle_request(&self, mut stream: T) { + pub fn handle_request(&self, mut connection: Connection) { trace!("handle_request ingress"); // Read bytes from stream // De-Serialise bytes into a request - let request = match Request::read_from_stream(&mut stream, self.body_len_limit) { + let request = match Request::read_from_stream(&mut connection.stream, self.body_len_limit) { Ok(request) => request, Err(status) => { format_error!("Failed to read request", status); let response = Response::from_status(status); - if let Err(status) = response.write_to_stream(&mut stream) { + if let Err(status) = response.write_to_stream(&mut connection.stream) { format_error!("Failed to write response", status); } return; @@ -63,7 +63,7 @@ impl FrontEndHandler { // Otherwise find an authenticator that is capable to authenticate the request } else if let Some(authenticator) = self.authenticators.get(&request.header.auth_type) { // Authenticate the request - match authenticator.authenticate(&request.auth) { + match authenticator.authenticate(&request.auth, connection.metadata) { // Send the request to the dispatcher // Get a response back Ok(app_name) => (Some(app_name), None), @@ -102,7 +102,7 @@ impl FrontEndHandler { // Serialise the response into bytes // Write bytes to stream - match response.write_to_stream(&mut stream) { + match response.write_to_stream(&mut connection.stream) { Ok(_) => { if crate::utils::GlobalConfig::log_error_details() { if let Some(app_name_string) = app_name { diff --git a/src/front/listener.rs b/src/front/listener.rs index f4d56ae3..a543ce45 100644 --- a/src/front/listener.rs +++ b/src/front/listener.rs @@ -5,6 +5,7 @@ //! The [`Listen`](https://parallaxsecond.github.io/parsec-book/parsec_service/listeners.html) //! trait acts as an interface for the operations that must be supported by any implementation //! of the IPC mechanism used as a Parsec front. +use derivative::Derivative; use serde::Deserialize; use std::time::Duration; @@ -25,6 +26,23 @@ pub struct ListenerConfig { pub timeout: u64, } +/// Specifies metadata associated with a connection, if any. +#[derive(Copy, Clone, Debug)] +pub enum ConnectionMetadata { + // TODO: nothing here right now. Metadata types will be added as needed. +} + +/// Represents a connection to a single client. Contains a stream, used for communication with the +/// client, and some metadata associated with the connection that might be useful elsewhere (i.e. +/// authentication, etc). +#[derive(Derivative)] +#[derivative(Debug)] +pub struct Connection { + #[derivative(Debug = "ignore")] + pub stream: Box, + pub metadata: Option, +} + /// IPC front manager interface /// /// Interface defining the functionality that any IPC front manager has to expose to Parsec for normal @@ -45,5 +63,5 @@ pub trait Listen { /// # Panics /// /// If the listener has not been initialised before, with the `init` method. - fn accept(&self) -> Option>; + fn accept(&self) -> Option; }