Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make HTTP client and TLS parts optional for "domain model only" use #26

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ description = "RabbitMQ HTTP API client"
license = "MIT OR Apache-2.0"

[dependencies]
reqwest = { version = "0.12", features = ["json", "multipart", "blocking", "rustls-tls"] }
reqwest = { version = "0.12", features = [
"json",
"multipart",
], optional = true }
thiserror = "1"
serde = { version = "1.0", features = ["derive", "std"] }
serde-aux = "4.5"
Expand All @@ -23,6 +26,7 @@ amqprs = "1"
# tokio = { version = "1", features = ["rt", "net"] }

[features]
default = ["core"]
default = ["core", "blocking"]
core = []
blocking = ["dep:reqwest", "reqwest/blocking"]
tabled = ["dep:tabled"]
247 changes: 148 additions & 99 deletions src/blocking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use reqwest::{
blocking::Client as HttpClient,
header::{HeaderMap, HeaderValue, InvalidHeaderValue},
tls,
};
use serde::Serialize;
use serde_json::{json, Map, Value};
use std::{collections::HashMap, fmt::Display};
use std::{
collections::HashMap,
fmt::{self, Display},
};

use crate::responses::DefinitionSet;
use thiserror::Error;
Expand Down Expand Up @@ -43,6 +45,96 @@ pub enum Error {

pub type Result<T> = std::result::Result<T, Error>;

/// A `ClientBuilder` can be used to create a `Client` with custom configuration.
///
/// Example
/// ```rust
/// use rabbitmq_http_client::blocking::ClientBuilder;
///
/// let endpoint = "http://localhost:15672";
/// let username = "username";
/// let password = "password";
/// let rc = ClientBuilder::new().with_endpoint(&endpoint).with_basic_auth_credentials(&username, &password).build();
/// // list cluster nodes
/// rc.list_nodes();
/// // list user connections
/// rc.list_connections();
/// // fetch information and metrics of a specific queue
/// rc.get_queue_info("/", "qq.1");
/// ```
pub struct ClientBuilder<E, U, P> {
endpoint: E,
username: U,
password: P,
client: HttpClient,
}

impl Default for ClientBuilder<&'static str, &'static str, &'static str> {
fn default() -> Self {
Self::new()
}
}

impl ClientBuilder<&'static str, &'static str, &'static str> {
/// Constructs a new `ClientBuilder`.
///
/// This is the same as `Client::builder()`.
pub fn new() -> Self {
let client = HttpClient::new();
Self {
endpoint: "http://localhost:15672",
username: "guest",
password: "guest",
client,
}
}
}

impl<E, U, P> ClientBuilder<E, U, P>
where
E: fmt::Display,
U: fmt::Display,
P: fmt::Display,
{
pub fn with_basic_auth_credentials<NewU, NewP>(
self,
username: NewU,
password: NewP,
) -> ClientBuilder<E, NewU, NewP>
where
NewU: fmt::Display,
NewP: fmt::Display,
{
ClientBuilder {
endpoint: self.endpoint,
username,
password,
client: self.client,
}
}

pub fn with_endpoint<T>(self, endpoint: T) -> ClientBuilder<T, U, P>
where
T: fmt::Display,
{
ClientBuilder {
endpoint,
username: self.username,
password: self.password,
client: self.client,
}
}

pub fn with_client(self, client: HttpClient) -> Self {
ClientBuilder { client, ..self }
}

/// Returns a `Client` that uses this `ClientBuilder` configuration.
pub fn build(self) -> Client<E, U, P> {
Client::from_http_client(self.client, self.endpoint, self.username, self.password)
}
}

/// A client for the [RabbitMQ HTTP API](https://rabbitmq.com/docs/management/#http-api).
///
/// Most functions provided by this type represent various HTTP API operations.
Expand All @@ -57,96 +149,80 @@ pub type Result<T> = std::result::Result<T, Error>;
/// ```rust
/// use rabbitmq_http_client::blocking::Client;
///
/// let endpoint = "http://localhost:15672/api/";
/// let endpoint = "http://localhost:15672";
/// let username = "username";
/// let password = "password";
/// let rc = Client::new(&endpoint).with_basic_auth_credentials(&username, &password);
/// let rc = Client::new(&endpoint, &username, &password);
/// // list cluster nodes
/// rc.list_nodes();
/// // list user connections
/// rc.list_connections();
/// // fetch information and metrics of a specific queue
/// rc.get_queue_info("/", "qq.1");
/// ```
pub struct Client<'a> {
endpoint: &'a str,
username: &'a str,
password: &'a str,
ca_certificate: Option<reqwest::Certificate>,
skip_tls_peer_verification: bool,
pub struct Client<E, U, P> {
endpoint: E,
username: U,
password: P,
client: HttpClient,
}

impl<'a> Client<'a> {
/// Instantiates a client for the specified endpoint.
/// Credentials default to guest/guest.
impl<E, U, P> Client<E, U, P>
where
E: fmt::Display,
U: fmt::Display,
P: fmt::Display,
{
/// Instantiates a client for the specified endpoint with username and password.
///
/// Example
/// ```rust
/// use rabbitmq_http_client::blocking::Client;
///
/// let endpoint = "http://localhost:15672/api/";
/// let rc = Client::new(&endpoint);
/// let endpoint = "http://localhost:15672";
/// let username = "username";
/// let password = "password";
/// let rc = Client::new(endpoint, username, password);
/// ```
pub fn new(endpoint: &'a str) -> Self {
pub fn new(endpoint: E, username: U, password: P) -> Self {
let client = HttpClient::builder().build().unwrap();

Self {
endpoint,
username: "guest",
password: "guest",
ca_certificate: None,
skip_tls_peer_verification: false,
username,
password,
client,
}
}

/// Configures basic HTTP Auth for authentication.
/// Instantiates a client for the specified endpoint with username and password and pre-build HttpClient.
/// Credentials default to guest/guest.
///
/// Example
/// ```rust
/// use reqwest::blocking::Client as HttpClient;
/// use rabbitmq_http_client::blocking::Client;
///
/// let endpoint = "http://localhost:15672/api/";
/// let client = HttpClient::new();
/// let endpoint = "http://localhost:15672";
/// let username = "username";
/// let password = "password";
/// let rc = Client::new(&endpoint).with_basic_auth_credentials(&username, &password);
/// ```
pub fn with_basic_auth_credentials(mut self, username: &'a str, password: &'a str) -> Self {
self.username = username;
self.password = password;
self
}

/// Configures a custom CA Certificate for TLS peer certificate chain verification.
///
/// Example
/// ```rust
/// # use rabbitmq_http_client::blocking::Client;
/// # use std::fs::File;
/// # use std::io::Read;
/// # fn call() -> Result<(), Box<dyn std::error::Error>> {
/// let endpoint = "http://localhost:15672/api/";
/// let mut buf = Vec::new();
/// File::open("ca_certificate.pem")?.read_to_end(&mut buf)?;
/// let rc = Client::new(&endpoint).with_pem_ca_certificate(buf);
/// # drop(call);
/// # Ok(())
/// # }
/// let rc = Client::from_http_client(client, endpoint, username, password);
/// ```
pub fn with_pem_ca_certificate(mut self, ca_certificate: Vec<u8>) -> Result<Self> {
self.ca_certificate = Some(reqwest::Certificate::from_pem(&ca_certificate)?);
Ok(self)
pub fn from_http_client(client: HttpClient, endpoint: E, username: U, password: P) -> Self {
Self {
endpoint,
username,
password,
client,
}
}

/// Configures a custom CA Certificate for TLS peer certificate chain verification.
///
/// Example
/// ```rust
/// use rabbitmq_http_client::blocking::Client;
/// Creates a `ClientBuilder` to configure a `Client`.
///
/// let endpoint = "http://localhost:15672/api/";
/// let rc = Client::new(&endpoint).without_tls_peer_verification().list_nodes();
/// ```
pub fn without_tls_peer_verification(mut self) -> Self {
self.skip_tls_peer_verification = true;
self
/// This is the same as `ClientBuilder::new()`.
pub fn builder() -> ClientBuilder<&'static str, &'static str, &'static str> {
ClientBuilder::new()
}

/// Lists cluster nodes.
Expand Down Expand Up @@ -1182,9 +1258,9 @@ impl<'a> Client<'a> {

fn http_get(&self, path: &str) -> crate::blocking::Result<HttpClientResponse> {
let response = self
.http_client()
.client
.get(self.rooted_path(path))
.basic_auth(self.username, Some(self.password))
.basic_auth(&self.username, Some(&self.password))
.send();

self.ok_or_http_client_error(response)
Expand All @@ -1195,10 +1271,10 @@ impl<'a> Client<'a> {
T: Serialize,
{
let response = self
.http_client()
.client
.put(self.rooted_path(path))
.json(&payload)
.basic_auth(self.username, Some(self.password))
.basic_auth(&self.username, Some(&self.password))
.send();

self.ok_or_http_client_error(response)
Expand All @@ -1209,20 +1285,20 @@ impl<'a> Client<'a> {
T: Serialize,
{
let response = self
.http_client()
.client
.post(self.rooted_path(path))
.json(&payload)
.basic_auth(self.username, Some(self.password))
.basic_auth(&self.username, Some(&self.password))
.send();

self.ok_or_http_client_error(response)
}

fn http_delete(&self, path: &str) -> crate::blocking::Result<HttpClientResponse> {
let response = self
.http_client()
.client
.delete(self.rooted_path(path))
.basic_auth(self.username, Some(self.password))
.basic_auth(&self.username, Some(&self.password))
.send();
self.ok_or_http_client_error(response)
}
Expand All @@ -1233,9 +1309,9 @@ impl<'a> Client<'a> {
headers: HeaderMap,
) -> crate::blocking::Result<HttpClientResponse> {
let response = self
.http_client()
.client
.delete(self.rooted_path(path))
.basic_auth(self.username, Some(self.password))
.basic_auth(&self.username, Some(&self.password))
.headers(headers)
.send();
self.ok_or_http_client_error(response)
Expand Down Expand Up @@ -1282,27 +1358,6 @@ impl<'a> Client<'a> {
Ok(response)
}

fn http_client(&self) -> HttpClient {
let mut builder = HttpClient::builder();

if self.endpoint.starts_with("https://") {
builder = builder
.use_rustls_tls()
.min_tls_version(tls::Version::TLS_1_2)
.max_tls_version(tls::Version::TLS_1_3);

if self.skip_tls_peer_verification {
builder = builder.danger_accept_invalid_certs(true);
};

if let Some(cert) = &self.ca_certificate {
builder = builder.add_root_certificate(cert.clone());
}
}

builder.build().unwrap()
}

fn ok_or_status_code_error_except_503(
&self,
response: HttpClientResponse,
Expand All @@ -1322,19 +1377,13 @@ impl<'a> Client<'a> {
}

fn rooted_path(&self, path: &str) -> String {
format!("{}/{}", self.endpoint, path)
format!("{}/api/{}", self.endpoint, path)
}
}

impl<'a> Default for Client<'a> {
impl Default for Client<&'static str, &'static str, &'static str> {
fn default() -> Self {
Self {
endpoint: "http://localhost:15672",
username: "guest",
password: "guest",
ca_certificate: None,
skip_tls_peer_verification: false,
}
Self::new("http://localhost:15672", "guest", "guest")
}
}

Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
//! This means that the user can choose either of the licenses.

/// The primary API: a blocking HTTP API client
#[cfg(feature = "blocking")]
pub mod blocking;
/// Types commonly used by API requests and responses
pub mod commons;
Expand Down
Loading
Loading