guacamole-client
Rust client library for the Apache Guacamole REST API.
Features
- Async — built on reqwest and Tokio
- Type-safe — strongly typed request/response models with serde
- Input validation — all identifiers are validated before they reach the network
- Security-first — auth tokens and passwords are redacted from
Debug output, #![forbid(unsafe_code)]
- Ergonomic errors — typed
Error enum with per-status-code variants
- Multi-data-source — every method accepts an optional data source override
Installation
Add to your Cargo.toml:
[dependencies]
guacamole-client = "0.5"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
Quick start
#[tokio::main]
async fn main() -> guacamole_client::Result<()> {
let mut client = guacamole_client::GuacamoleClient::new("http://localhost:8080/guacamole")?;
client.login("guacadmin", "guacadmin").await?;
let users = client.list_users(None).await?;
for (username, user) in &users {
println!("{username}: {user:?}");
}
client.logout().await?;
Ok(())
}
API overview
Every method that operates on a data source takes an Option<&str> parameter. Pass None to use the default data source from your login session, or Some("postgresql") to target a specific one.
Authentication
# async fn example(client: &mut guacamole_client::GuacamoleClient) -> guacamole_client::Result<()> {
let auth = client.login("guacadmin", "guacadmin").await?;
println!("Available data sources: {:?}", auth.available_data_sources);
client.logout().await?;
# Ok(())
# }
Users
# async fn example(client: &guacamole_client::GuacamoleClient) -> guacamole_client::Result<()> {
let users = client.list_users(None).await?;
let user = client.get_user(None, "guacadmin").await?;
let me = client.get_self(None).await?;
use guacamole_client::User;
let new_user = User {
username: Some("alice".into()),
password: Some("secret".into()),
..Default::default()
};
client.create_user(None, &new_user).await?;
client.update_user(None, "alice", &new_user).await?;
client.delete_user(None, "alice").await?;
use guacamole_client::PasswordChange;
let pw = PasswordChange {
old_password: Some("secret".into()),
new_password: Some("new-secret".into()),
};
client.update_user_password(None, "alice", &pw).await?;
let perms = client.get_user_permissions(None, "alice").await?;
let effective = client.get_user_effective_permissions(None, "alice").await?;
let groups = client.get_user_groups(None, "alice").await?;
let history = client.get_user_history(None, "alice").await?;
use guacamole_client::PatchOperation;
client.update_user_permissions(None, "alice", &[
PatchOperation::add("/connectionPermissions/1", "READ"),
]).await?;
client.update_user_groups(None, "alice", &[
PatchOperation::add("/", "developers"),
]).await?;
# Ok(())
# }
User groups
# async fn example(client: &guacamole_client::GuacamoleClient) -> guacamole_client::Result<()> {
use guacamole_client::{UserGroup, PatchOperation};
let groups = client.list_user_groups(None).await?;
let group = client.get_user_group(None, "developers").await?;
let new_group = UserGroup {
identifier: Some("qa-team".into()),
..Default::default()
};
client.create_user_group(None, &new_group).await?;
client.update_user_group(None, "qa-team", &new_group).await?;
client.delete_user_group(None, "qa-team").await?;
client.update_user_group_member_users(None, "developers", &[
PatchOperation::add("/", "alice"),
]).await?;
client.update_user_group_member_groups(None, "developers", &[
PatchOperation::add("/", "interns"),
]).await?;
client.update_user_group_parent_groups(None, "developers", &[
PatchOperation::add("/", "engineering"),
]).await?;
client.update_user_group_permissions(None, "developers", &[
PatchOperation::add("/connectionPermissions/1", "READ"),
]).await?;
# Ok(())
# }
Connections
# async fn example(client: &guacamole_client::GuacamoleClient) -> guacamole_client::Result<()> {
use std::collections::HashMap;
use guacamole_client::{Connection, PatchOperation};
let connections = client.list_connections(None).await?;
let conn = client.get_connection(None, "1").await?;
let new_conn = Connection {
name: Some("my-server".into()),
protocol: Some("ssh".into()),
parent_identifier: Some("ROOT".into()),
parameters: Some(HashMap::from([
("hostname".into(), "10.0.0.5".into()),
("port".into(), "22".into()),
])),
..Default::default()
};
let created = client.create_connection(None, &new_conn).await?;
client.update_connection(None, "1", &new_conn).await?;
client.delete_connection(None, "1").await?;
let params = client.get_connection_parameters(None, "1").await?;
let history = client.get_connection_history(None, "1").await?;
let profiles = client.get_connection_sharing_profiles(None, "1").await?;
let active = client.list_active_connections(None).await?;
client.kill_connections(None, &[
PatchOperation::remove("/abc-123-def"),
]).await?;
# Ok(())
# }
Connection groups
# async fn example(client: &guacamole_client::GuacamoleClient) -> guacamole_client::Result<()> {
use guacamole_client::ConnectionGroup;
let groups = client.list_connection_groups(None).await?;
let group = client.get_connection_group(None, "ROOT").await?;
let tree = client.get_connection_group_tree(None, "ROOT", Some("READ")).await?;
let new_group = ConnectionGroup {
name: Some("Servers".into()),
type_: Some("ORGANIZATIONAL".into()),
parent_identifier: Some("ROOT".into()),
..Default::default()
};
let created = client.create_connection_group(None, &new_group).await?;
client.update_connection_group(None, "1", &new_group).await?;
client.delete_connection_group(None, "1").await?;
# Ok(())
# }
Sharing profiles
# async fn example(client: &guacamole_client::GuacamoleClient) -> guacamole_client::Result<()> {
use guacamole_client::SharingProfile;
let profiles = client.list_sharing_profiles(None).await?;
let profile = client.get_sharing_profile(None, "1").await?;
let params = client.get_sharing_profile_parameters(None, "1").await?;
let new_profile = SharingProfile {
name: Some("read-only-view".into()),
primary_connection_identifier: Some("1".into()),
..Default::default()
};
let created = client.create_sharing_profile(None, &new_profile).await?;
client.delete_sharing_profile(None, "1").await?;
# Ok(())
# }
History
# async fn example(client: &guacamole_client::GuacamoleClient) -> guacamole_client::Result<()> {
let history = client.list_connection_history(None, Some("my-server"), Some("desc")).await?;
let user_history = client.list_user_history(None, Some("asc")).await?;
# Ok(())
# }
Schema & protocols
# async fn example(client: &guacamole_client::GuacamoleClient) -> guacamole_client::Result<()> {
let user_schema = client.list_user_attributes_schema(None).await?;
let group_schema = client.list_user_group_attributes_schema(None).await?;
let conn_schema = client.list_connection_attributes_schema(None).await?;
let sp_schema = client.list_sharing_profile_attributes_schema(None).await?;
let cg_schema = client.list_connection_group_attributes_schema(None).await?;
let protocols = client.list_protocols(None).await?;
# Ok(())
# }
Tunnels & server patches
# async fn example(client: &guacamole_client::GuacamoleClient) -> guacamole_client::Result<()> {
let tunnels = client.list_tunnels().await?;
let active = client.get_tunnel_active_connection("abc-123").await?;
let patches = client.list_patches().await?;
# Ok(())
# }
Languages
# async fn example(client: &guacamole_client::GuacamoleClient) -> guacamole_client::Result<()> {
let languages = client.list_languages().await?;
# Ok(())
# }
Error handling
All methods return guacamole_client::Result<T>. The Error enum maps HTTP status codes to typed variants:
# async fn example(client: &guacamole_client::GuacamoleClient) -> guacamole_client::Result<()> {
use guacamole_client::Error;
match client.get_user(None, "alice").await {
Ok(user) => println!("{user:?}"),
Err(Error::NotFound { resource, .. }) => eprintln!("{resource} does not exist"),
Err(Error::Unauthorized { .. }) => eprintln!("bad credentials"),
Err(Error::Forbidden { .. }) => eprintln!("insufficient permissions"),
Err(Error::RateLimited { retry_after, .. }) => eprintln!("slow down: {retry_after:?}"),
Err(e) => eprintln!("unexpected error: {e}"),
}
# Ok(())
# }
Security
#![forbid(unsafe_code)] — no unsafe Rust anywhere in the crate
- All identifiers (usernames, connection IDs, data sources, etc.) are validated against path-traversal and injection attacks before use
- Auth tokens and passwords are redacted from
Debug output — safe to log
- HTTP timeouts are enforced (30 s request, 10 s connect)
- Error response bodies are truncated to 512 bytes to prevent memory exhaustion
License
MIT