diff --git a/CHANGELOG.md b/CHANGELOG.md index b362037..de84222 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased +### Added +- Fixed bug in parsing command line options for bridge +- Added support for getting the client IP address from a proxy header (e.g. `X-Forwarded-For`) +- Cleaned up port handling, so URLs with default ports don't have the ports specified ## [0.0.8] - 2024-10-24 ### Added diff --git a/bridge/Cargo.toml b/bridge/Cargo.toml index 299fa7e..a1c5f4c 100644 --- a/bridge/Cargo.toml +++ b/bridge/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "op-bridge" -version = "0.0.1" +version = "0.0.9" description = "An example of an OpenPortal user portal to OpenPortal bridge" edition = "2021" license = "MIT" diff --git a/bridge/src/main.rs b/bridge/src/main.rs index f11d6df..5c3c18d 100644 --- a/bridge/src/main.rs +++ b/bridge/src/main.rs @@ -40,6 +40,7 @@ async fn main() -> Result<()> { Some("127.0.0.1".to_owned()), Some(8044), None, + None, Some("http://localhost:3000".to_owned()), Some("127.0.0.1".to_owned()), Some(3000), diff --git a/cluster/Cargo.toml b/cluster/Cargo.toml index c436c77..52c013d 100644 --- a/cluster/Cargo.toml +++ b/cluster/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "op-cluster" -version = "0.0.1" +version = "0.0.9" description = "An example of an OpenPortal HPC cluster platform agent" edition = "2021" license = "MIT" diff --git a/cluster/src/main.rs b/cluster/src/main.rs index b43a44d..860bf59 100644 --- a/cluster/src/main.rs +++ b/cluster/src/main.rs @@ -35,6 +35,7 @@ async fn main() -> Result<()> { Some("127.0.0.1".to_owned()), Some(8045), None, + None, Some(AgentType::Platform), ); diff --git a/docs/cmdline/cluster/Cargo.toml b/docs/cmdline/cluster/Cargo.toml index 3700f5f..d632896 100644 --- a/docs/cmdline/cluster/Cargo.toml +++ b/docs/cmdline/cluster/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "example-cluster" -version = "0.0.1" +version = "0.0.9" description = "templemeads command line example - cluster agent" edition = "2021" license = "MIT" diff --git a/docs/cmdline/cluster/src/main.rs b/docs/cmdline/cluster/src/main.rs index fd942f0..80900e2 100644 --- a/docs/cmdline/cluster/src/main.rs +++ b/docs/cmdline/cluster/src/main.rs @@ -25,6 +25,7 @@ async fn main() -> Result<()> { Some("127.0.0.1".to_owned()), Some(8091), None, + None, Some(AgentType::Instance), ); diff --git a/docs/cmdline/portal/Cargo.toml b/docs/cmdline/portal/Cargo.toml index 1219c28..042e1c6 100644 --- a/docs/cmdline/portal/Cargo.toml +++ b/docs/cmdline/portal/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "example-portal" -version = "0.0.1" +version = "0.0.9" description = "templemeads command line example - portal agent" edition = "2021" license = "MIT" diff --git a/docs/cmdline/portal/src/main.rs b/docs/cmdline/portal/src/main.rs index 91d7b7c..e51931e 100644 --- a/docs/cmdline/portal/src/main.rs +++ b/docs/cmdline/portal/src/main.rs @@ -21,6 +21,7 @@ async fn main() -> Result<()> { Some("127.0.0.1".to_owned()), Some(8090), None, + None, Some(AgentType::Portal), ); diff --git a/docs/echo/Cargo.toml b/docs/echo/Cargo.toml index 78caf65..4f5b164 100644 --- a/docs/echo/Cargo.toml +++ b/docs/echo/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "example-echo" -version = "0.0.1" +version = "0.0.9" description = "Paddington Echo Service example" edition = "2021" license = "MIT" diff --git a/docs/echo/src/main.rs b/docs/echo/src/main.rs index 84bd08c..1a819c6 100644 --- a/docs/echo/src/main.rs +++ b/docs/echo/src/main.rs @@ -187,6 +187,7 @@ async fn run_client(invitation: &Path) -> Result<(), Error> { "127.0.0.1", &6502, &None, + &None, )?; // now give the invitation to connect to the server to the client @@ -262,7 +263,7 @@ async fn run_server( invitation: &Path, ) -> Result<(), Error> { // create the echo-server service - let mut service = ServiceConfig::new("echo-server", url, ip, port, &None)?; + let mut service = ServiceConfig::new("echo-server", url, ip, port, &None, &None)?; let invite = service.add_client("echo-client", range)?; diff --git a/docs/job/Cargo.toml b/docs/job/Cargo.toml index f86d909..a0e1f6f 100644 --- a/docs/job/Cargo.toml +++ b/docs/job/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "example-job" -version = "0.0.1" +version = "0.0.9" description = "templemeads job example" edition = "2021" license = "MIT" diff --git a/docs/job/src/main.rs b/docs/job/src/main.rs index f303a6d..5164423 100644 --- a/docs/job/src/main.rs +++ b/docs/job/src/main.rs @@ -205,6 +205,7 @@ async fn run_cluster(invitation: &Path) -> Result<(), Error> { "127.0.0.1", &6502, &None, + &None, )?; // now give the invitation to connect to the server to the client @@ -260,7 +261,7 @@ async fn run_portal( invitation: &Path, ) -> Result<(), Error> { // create a paddington service configuration for the portal agent - let mut service = ServiceConfig::new("portal", url, ip, port, &None)?; + let mut service = ServiceConfig::new("portal", url, ip, port, &None, &None)?; // add the cluster to the portal, returning an invitation let invite = service.add_client("cluster", range)?; diff --git a/filesystem/Cargo.toml b/filesystem/Cargo.toml index 9d80506..12a7180 100644 --- a/filesystem/Cargo.toml +++ b/filesystem/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "op-filesystem" -version = "0.0.1" +version = "0.0.9" description = "Agent that interfaces OpenPortal with a filesystem" edition = "2021" license = "MIT" diff --git a/filesystem/src/main.rs b/filesystem/src/main.rs index 36cd475..ae01436 100644 --- a/filesystem/src/main.rs +++ b/filesystem/src/main.rs @@ -40,6 +40,7 @@ async fn main() -> Result<()> { Some("127.0.0.1".to_owned()), Some(8047), None, + None, Some(AgentType::Filesystem), ); diff --git a/freeipa/Cargo.toml b/freeipa/Cargo.toml index 5539041..f0a73c3 100644 --- a/freeipa/Cargo.toml +++ b/freeipa/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "op-freeipa" -version = "0.0.1" +version = "0.0.9" description = "Agent that interfaces OpenPortal with FreeIPA" edition = "2021" license = "MIT" diff --git a/freeipa/src/main.rs b/freeipa/src/main.rs index f4f7ccc..dc0404b 100644 --- a/freeipa/src/main.rs +++ b/freeipa/src/main.rs @@ -45,6 +45,7 @@ async fn main() -> Result<()> { Some("127.0.0.1".to_owned()), Some(8046), None, + None, Some(AgentType::Account), ); diff --git a/paddington/Cargo.toml b/paddington/Cargo.toml index 819f772..35ae750 100644 --- a/paddington/Cargo.toml +++ b/paddington/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "paddington" -version = "0.0.1" +version = "0.0.9" description = "A library for implementing the OpenPortal communication protocol" edition = "2021" license = "MIT" @@ -32,9 +32,10 @@ serde_json = "1.0.120" serde_with = { version="3.9.0", features = ["hex"] } thiserror = "1.0.63" tokio = { version = "1.0", features = ["full", "tracing"] } -tokio-tungstenite = { version = "0.23.1", features = ["rustls-tls-native-roots"] } +tokio-tungstenite = { version = "0.24.0", features = ["rustls-tls-native-roots"] } toml = "0.8.16" tracing = "0.1.40" +tungstenite = "0.24.0" url = {version="2.5.2", features=["serde"]} [lints.rust] diff --git a/paddington/src/config.rs b/paddington/src/config.rs index 6ee5bd1..04aa634 100644 --- a/paddington/src/config.rs +++ b/paddington/src/config.rs @@ -74,6 +74,7 @@ pub struct Defaults { ip: String, port: u16, healthcheck_port: Option, + proxy_header: Option, } impl Defaults { @@ -84,6 +85,7 @@ impl Defaults { ip: Option, port: Option, healthcheck_port: Option, + proxy_header: Option, ) -> Self { let config_file = config_file.unwrap_or( dirs::config_local_dir() @@ -102,6 +104,7 @@ impl Defaults { ip: ip.unwrap_or("127.0.0.1".to_owned()), port: port.unwrap_or(8042), healthcheck_port, + proxy_header, } } @@ -439,6 +442,7 @@ pub struct ServiceConfig { ip: IpAddr, port: u16, heathcheck_port: Option, + proxy_header: Option, servers: Vec, clients: Vec, @@ -452,6 +456,7 @@ impl ServiceConfig { ip: &str, port: &u16, healthcheck_port: &Option, + proxy_header: &Option, ) -> Result { Ok(ServiceConfig { name: name.to_string(), @@ -461,6 +466,7 @@ impl ServiceConfig { .with_context(|| format!("Could not parse IP address: {}", ip))?, port: *port, heathcheck_port: *healthcheck_port, + proxy_header: proxy_header.clone(), servers: Vec::new(), clients: Vec::new(), encryption: None, @@ -531,6 +537,10 @@ impl ServiceConfig { self.heathcheck_port } + pub fn proxy_header(&self) -> Option { + self.proxy_header.clone() + } + pub fn add_client(&mut self, name: &str, ip: &str) -> Result { let ip = IpOrRange::new(ip) .with_context(|| format!("Could not parse into an IP address or IP range: {}", ip))?; @@ -612,6 +622,7 @@ impl ServiceConfig { ip: IpAddr, port: u16, healthcheck_port: &Option, + proxy_header: &Option, ) -> Result { // see if this config_dir exists - return an error if it does let config_file = path::absolute(config_file).with_context(|| { @@ -625,7 +636,14 @@ impl ServiceConfig { return Err(Error::NotExists(config_file.to_string_lossy().to_string())); } - let config = ServiceConfig::new(&name, &url, &ip.to_string(), &port, healthcheck_port)?; + let config = ServiceConfig::new( + &name, + &url, + &ip.to_string(), + &port, + healthcheck_port, + proxy_header, + )?; save::(config.clone(), &config_file)?; // check we can read the config and return it @@ -688,17 +706,29 @@ mod tests { #[test] fn test_invitations() { - let mut primary = - ServiceConfig::new("primary", "http://localhost", "127.0.0.1", &5544, &None) - .unwrap_or_else(|e| { - unreachable!("Cannot create service config: {}", e); - }); - - let mut secondary = - ServiceConfig::new("secondary", "http://localhost", "127.0.0.1", &5545, &None) - .unwrap_or_else(|e| { - unreachable!("Cannot create service config: {}", e); - }); + let mut primary = ServiceConfig::new( + "primary", + "http://localhost", + "127.0.0.1", + &5544, + &None, + &None, + ) + .unwrap_or_else(|e| { + unreachable!("Cannot create service config: {}", e); + }); + + let mut secondary = ServiceConfig::new( + "secondary", + "http://localhost", + "127.0.0.1", + &5545, + &None, + &None, + ) + .unwrap_or_else(|e| { + unreachable!("Cannot create service config: {}", e); + }); // introduce the secondary to the primary let invite = primary diff --git a/paddington/src/connection.rs b/paddington/src/connection.rs index 94ba593..2525dd3 100644 --- a/paddington/src/connection.rs +++ b/paddington/src/connection.rs @@ -9,12 +9,15 @@ use futures_channel::mpsc::{unbounded, UnboundedSender}; use futures_util::{future, pin_mut, stream::TryStreamExt}; use secrecy::ExposeSecret; use serde::{de::DeserializeOwned, Serialize}; +use std::sync::Arc; use tokio::net::TcpStream; use tokio::sync::Mutex as TokioMutex; use tokio_tungstenite::connect_async; use tokio_tungstenite::tungstenite::protocol::Message as TokioMessage; - -use std::sync::Arc; +use tungstenite::handshake::server::{ + ErrorResponse as HandshakeErrorResponse, Request as HandshakeRequest, + Response as HandshakeResponse, +}; use crate::command::Command; use crate::config::{ClientConfig, PeerConfig, ServiceConfig}; @@ -358,10 +361,48 @@ impl Connection { // we now know we are the only ones handling the connection, // and are safe to update the keys etc. - let addr: std::net::SocketAddr = stream + let mut addr: std::net::SocketAddr = stream .peer_addr() .with_context(|| "Error getting the peer address. Ensure the connection is open.")?; + let proxy_header = self.config.proxy_header(); + let mut proxy_client = None; + + let process_headers = |request: &HandshakeRequest, + response: HandshakeResponse| + -> Result { + if let Some(proxy_header) = proxy_header { + if let Some(value) = request + .headers() + .get(proxy_header) + .and_then(|value| value.to_str().ok()) + { + proxy_client = Some(value.to_string()); + } + } + + Ok(response) + }; + + let ws_stream = tokio_tungstenite::accept_hdr_async(stream, process_headers) + .await + .with_context(|| { + format!( + "Error accepting WebSocket connection from: {}. Closing connection.", + addr + ) + })?; + + if let Some(proxy_client) = proxy_client { + tracing::info!("Proxy client: {:?}", proxy_client); + addr = proxy_client.parse().with_context(|| { + format!( + "Error parsing proxy client address: {}. Closing connection.", + proxy_client + ) + })?; + } + tracing::info!("Accepted connection from peer: {}", addr); let clients: Vec = self @@ -379,15 +420,6 @@ impl Connection { )); } - let ws_stream = tokio_tungstenite::accept_async(stream) - .await - .with_context(|| { - format!( - "Error accepting WebSocket connection from: {}. Closing connection.", - addr - ) - })?; - // Split the WebSocket stream into incoming and outgoing parts let (mut outgoing, mut incoming) = ws_stream.split(); diff --git a/paddington/src/eventloop.rs b/paddington/src/eventloop.rs index e6b4b04..bf24b2f 100644 --- a/paddington/src/eventloop.rs +++ b/paddington/src/eventloop.rs @@ -57,8 +57,14 @@ mod tests { async fn test_run() -> Result<()> { // this tests that the service can be configured and will run // (it will exit immediately as there are no clients or servers) - let config = - ServiceConfig::new("test_server", "http://localhost", "127.0.0.1", &5544, &None)?; + let config = ServiceConfig::new( + "test_server", + "http://localhost", + "127.0.0.1", + &5544, + &None, + &None, + )?; run(config).await?; Ok(()) diff --git a/portal/Cargo.toml b/portal/Cargo.toml index 71b9a35..a5c2374 100644 --- a/portal/Cargo.toml +++ b/portal/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "op-portal" -version = "0.0.1" +version = "0.0.9" description = "An example of an OpenPortal portal interface service" edition = "2021" license = "MIT" diff --git a/portal/src/main.rs b/portal/src/main.rs index b5e2635..d5749b0 100644 --- a/portal/src/main.rs +++ b/portal/src/main.rs @@ -41,6 +41,7 @@ async fn main() -> Result<()> { Some("127.0.0.1".to_owned()), Some(8040), None, + None, Some(AgentType::Portal), ); diff --git a/provider/Cargo.toml b/provider/Cargo.toml index 4f61812..bf6b940 100644 --- a/provider/Cargo.toml +++ b/provider/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "op-provider" -version = "0.0.1" +version = "0.0.9" description = "An example of an OpenPortal provider interface service" edition = "2021" license = "MIT" diff --git a/provider/src/main.rs b/provider/src/main.rs index f6759de..02b59ff 100644 --- a/provider/src/main.rs +++ b/provider/src/main.rs @@ -41,6 +41,7 @@ async fn main() -> Result<()> { Some("127.0.0.1".to_owned()), Some(8041), None, + None, Some(AgentType::Provider), ); diff --git a/python/Cargo.toml b/python/Cargo.toml index 44fd8d9..dc1ea86 100644 --- a/python/Cargo.toml +++ b/python/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "openportal" -version = "0.0.1" +version = "0.0.9" description = "Python wrappers for OpenPortal" edition = "2021" license = "MIT" diff --git a/slurm/Cargo.toml b/slurm/Cargo.toml index 69e634f..46af131 100644 --- a/slurm/Cargo.toml +++ b/slurm/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "op-slurm" -version = "0.0.1" +version = "0.0.9" description = "An example of an OpenPortal Slurm cluster instance agent" edition = "2021" license = "MIT" diff --git a/slurm/src/main.rs b/slurm/src/main.rs index fa23d42..07780b9 100644 --- a/slurm/src/main.rs +++ b/slurm/src/main.rs @@ -41,6 +41,7 @@ async fn main() -> Result<()> { Some("127.0.0.1".to_owned()), Some(8046), None, + None, Some(AgentType::Instance), ); diff --git a/templemeads/Cargo.toml b/templemeads/Cargo.toml index 3edbd7d..e24ea6c 100644 --- a/templemeads/Cargo.toml +++ b/templemeads/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "templemeads" -version = "0.0.1" +version = "0.0.9" description = "A library for interfacing OpenPortal with specific portals" edition = "2021" license = "MIT" diff --git a/templemeads/src/agent_bridge.rs b/templemeads/src/agent_bridge.rs index 62be82e..12b15ed 100644 --- a/templemeads/src/agent_bridge.rs +++ b/templemeads/src/agent_bridge.rs @@ -73,12 +73,21 @@ impl Defaults { ip: Option, port: Option, healthcheck_port: Option, + proxy_header: Option, bridge_url: Option, bridge_ip: Option, bridge_port: Option, ) -> Self { Self { - service: ServiceDefaults::parse(name, config_file, url, ip, port, healthcheck_port), + service: ServiceDefaults::parse( + name, + config_file, + url, + ip, + port, + healthcheck_port, + proxy_header, + ), bridge: BridgeDefaults::parse(bridge_url, bridge_ip, bridge_port), } } @@ -120,6 +129,7 @@ pub async fn process_args(defaults: &Defaults) -> Result, Error> bridge_ip, bridge_port, healthcheck_port, + proxy_header, force, }) => { let local_healthcheck_port; @@ -141,6 +151,7 @@ pub async fn process_args(defaults: &Defaults) -> Result, Error> .to_string(), &port.unwrap_or_else(|| defaults.service.port()), &local_healthcheck_port, + proxy_header, )? }, bridge: BridgeConfig::new( @@ -400,6 +411,13 @@ enum Commands { )] healthcheck_port: Option, + #[arg( + long, + short = 'x', + help = "Optional header to use for proxying requests - look in this for the client IP address" + )] + proxy_header: Option, + #[arg(long, short = 'f', help = "Force reinitialisation")] force: bool, }, diff --git a/templemeads/src/agent_core.rs b/templemeads/src/agent_core.rs index 4a4facd..e477b0f 100644 --- a/templemeads/src/agent_core.rs +++ b/templemeads/src/agent_core.rs @@ -74,6 +74,7 @@ pub struct Defaults { } impl Defaults { + #[allow(clippy::too_many_arguments)] pub fn parse( name: Option, config_file: Option, @@ -81,10 +82,19 @@ impl Defaults { ip: Option, port: Option, healthcheck_port: Option, + proxy_header: Option, agent: Option, ) -> Self { Self { - service: ServiceDefaults::parse(name, config_file, url, ip, port, healthcheck_port), + service: ServiceDefaults::parse( + name, + config_file, + url, + ip, + port, + healthcheck_port, + proxy_header, + ), agent: agent.unwrap_or(AgentType::Portal), extras: HashMap::new(), } @@ -132,6 +142,7 @@ pub async fn process_args(defaults: &Defaults) -> Result, Error> ip, port, healthcheck_port, + proxy_header, force, }) => { let local_healthcheck_port; @@ -153,6 +164,7 @@ pub async fn process_args(defaults: &Defaults) -> Result, Error> .to_string(), &port.unwrap_or_else(|| defaults.service.port()), &local_healthcheck_port, + proxy_header, )? }, agent: defaults.agent.clone(), @@ -393,6 +405,13 @@ enum Commands { )] healthcheck_port: Option, + #[arg( + long, + short = 'x', + help = "Proxy header to use for the client IP address - look here for the client IP address" + )] + proxy_header: Option, + #[arg(long, short = 'f', help = "Force reinitialisation")] force: bool, },