# guacamole-client
Rust client library for the [Apache Guacamole REST API](https://guacamole.apache.org/).
## Features
- **Async** — built on [reqwest](https://crates.io/crates/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`:
```toml
[dependencies]
guacamole-client = "0.5.1"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
```
## Quick start
```rust,no_run
#[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
```rust,no_run
async fn example(client: &mut guacamole_client::GuacamoleClient) -> guacamole_client::Result<()> {
// Login — stores the session token and default data source
let auth = client.login("guacadmin", "guacadmin").await?;
println!("Available data sources: {:?}", auth.available_data_sources);
// Logout — clears the stored session
client.logout().await?;
Ok(())
}
```
### Users
```rust,no_run
async fn example(client: &guacamole_client::GuacamoleClient) -> guacamole_client::Result<()> {
// List / get
let users = client.list_users(None).await?;
let user = client.get_user(None, "guacadmin").await?;
let me = client.get_self(None).await?;
// Create
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?;
// Update / delete
client.update_user(None, "alice", &new_user).await?;
client.delete_user(None, "alice").await?;
// Password
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?;
// Permissions
let perms = client.get_user_permissions(None, "alice").await?;
let effective = client.get_user_effective_permissions(None, "alice").await?;
// Groups & history
let groups = client.get_user_groups(None, "alice").await?;
let history = client.get_user_history(None, "alice").await?;
// Patch operations (permissions, group membership)
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
```rust,no_run
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?;
// Create / update / delete
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?;
// Member & parent management
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
```rust,no_run
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?;
// Create
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?;
// Update / delete
client.update_connection(None, "1", &new_conn).await?;
client.delete_connection(None, "1").await?;
// Parameters & history
let params = client.get_connection_parameters(None, "1").await?;
let history = client.get_connection_history(None, "1").await?;
// Sharing profiles for a connection
let profiles = client.get_connection_sharing_profiles(None, "1").await?;
// Active connections
let active = client.list_active_connections(None).await?;
client.kill_connections(None, &[
PatchOperation::remove("/abc-123-def"),
]).await?;
Ok(())
}
```
### Connection groups
```rust,no_run
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?;
// Connection tree (recursive)
let tree = client.get_connection_group_tree(None, "ROOT", Some("READ")).await?;
// Create / update / delete
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
```rust,no_run
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?;
// Create / delete
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
```rust,no_run
async fn example(client: &guacamole_client::GuacamoleClient) -> guacamole_client::Result<()> {
// Connection history (with optional filter and sort order)
let history = client.list_connection_history(None, Some("my-server"), Some("desc")).await?;
// User history
let user_history = client.list_user_history(None, Some("asc")).await?;
Ok(())
}
```
### Schema & protocols
```rust,no_run
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
```rust,no_run
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
```rust,no_run
async fn example(client: &guacamole_client::GuacamoleClient) -> guacamole_client::Result<()> {
let languages = client.list_languages().await?;
// e.g. {"en": "English", "de": "Deutsch", ...}
Ok(())
}
```
## Error handling
All methods return `guacamole_client::Result<T>`. The `Error` enum maps HTTP status codes to typed variants:
```rust,no_run
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](LICENSE)