Skip to content
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
28 changes: 14 additions & 14 deletions aw-client-rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ impl AwClient {
}

pub async fn get_bucket(&self, bucketname: &str) -> Result<Bucket, reqwest::Error> {
let url = format!("{}/api/0/buckets/{}", self.baseurl, bucketname);
let url = format!("{}api/0/buckets/{}", self.baseurl, bucketname);
let bucket = self
.client
.get(url)
Expand All @@ -73,12 +73,12 @@ impl AwClient {
}

pub async fn get_buckets(&self) -> Result<HashMap<String, Bucket>, reqwest::Error> {
let url = format!("{}/api/0/buckets/", self.baseurl);
let url = format!("{}api/0/buckets/", self.baseurl);
self.client.get(url).send().await?.json().await
}

pub async fn create_bucket(&self, bucket: &Bucket) -> Result<(), reqwest::Error> {
let url = format!("{}/api/0/buckets/{}", self.baseurl, bucket.id);
let url = format!("{}api/0/buckets/{}", self.baseurl, bucket.id);
self.client.post(url).json(bucket).send().await?;
Ok(())
}
Expand All @@ -104,7 +104,7 @@ impl AwClient {
}

pub async fn delete_bucket(&self, bucketname: &str) -> Result<(), reqwest::Error> {
let url = format!("{}/api/0/buckets/{}", self.baseurl, bucketname);
let url = format!("{}api/0/buckets/{}", self.baseurl, bucketname);
self.client.delete(url).send().await?;
Ok(())
}
Expand All @@ -114,7 +114,7 @@ impl AwClient {
query: &str,
timeperiods: Vec<(DateTime<Utc>, DateTime<Utc>)>,
) -> Result<Vec<serde_json::Value>, reqwest::Error> {
let url = reqwest::Url::parse(format!("{}/api/0/query", self.baseurl).as_str()).unwrap();
let url = reqwest::Url::parse(format!("{}api/0/query", self.baseurl).as_str()).unwrap();

// Format timeperiods as ISO8601 strings, separated by /
let timeperiods_str: Vec<String> = timeperiods
Expand Down Expand Up @@ -144,7 +144,7 @@ impl AwClient {
limit: Option<u64>,
) -> Result<Vec<Event>, reqwest::Error> {
let mut url = reqwest::Url::parse(
format!("{}/api/0/buckets/{}/events", self.baseurl, bucketname).as_str(),
format!("{}api/0/buckets/{}/events", self.baseurl, bucketname).as_str(),
)
.unwrap();

Expand All @@ -169,7 +169,7 @@ impl AwClient {
bucketname: &str,
event: &Event,
) -> Result<(), reqwest::Error> {
let url = format!("{}/api/0/buckets/{}/events", self.baseurl, bucketname);
let url = format!("{}api/0/buckets/{}/events", self.baseurl, bucketname);
let eventlist = vec![event.clone()];
self.client.post(url).json(&eventlist).send().await?;
Ok(())
Expand All @@ -180,7 +180,7 @@ impl AwClient {
bucketname: &str,
events: Vec<Event>,
) -> Result<(), reqwest::Error> {
let url = format!("{}/api/0/buckets/{}/events", self.baseurl, bucketname);
let url = format!("{}api/0/buckets/{}/events", self.baseurl, bucketname);
self.client.post(url).json(&events).send().await?;
Ok(())
}
Expand All @@ -192,7 +192,7 @@ impl AwClient {
pulsetime: f64,
) -> Result<(), reqwest::Error> {
let url = format!(
"{}/api/0/buckets/{}/heartbeat?pulsetime={}",
"{}api/0/buckets/{}/heartbeat?pulsetime={}",
self.baseurl, bucketname, pulsetime
);
self.client.post(url).json(&event).send().await?;
Expand All @@ -205,15 +205,15 @@ impl AwClient {
event_id: i64,
) -> Result<(), reqwest::Error> {
let url = format!(
"{}/api/0/buckets/{}/events/{}",
"{}api/0/buckets/{}/events/{}",
self.baseurl, bucketname, event_id
);
self.client.delete(url).send().await?;
Ok(())
}

pub async fn get_event_count(&self, bucketname: &str) -> Result<i64, reqwest::Error> {
let url = format!("{}/api/0/buckets/{}/events/count", self.baseurl, bucketname);
let url = format!("{}api/0/buckets/{}/events/count", self.baseurl, bucketname);
let res = self
.client
.get(url)
Expand All @@ -230,17 +230,17 @@ impl AwClient {
}

pub async fn get_info(&self) -> Result<aw_models::Info, reqwest::Error> {
let url = format!("{}/api/0/info", self.baseurl);
let url = format!("{}api/0/info", self.baseurl);
self.client.get(url).send().await?.json().await
}

pub async fn get_setting(&self, setting: &str) -> Result<serde_json::Value, reqwest::Error> {
let url = format!("{}/api/0/settings/{}", self.baseurl, setting);
let url = format!("{}api/0/settings/{}", self.baseurl, setting);
self.client.get(url).send().await?.json().await
}

pub async fn get_settings(&self) -> Result<aw_models::Settings, reqwest::Error> {
let url = format!("{}/api/0/settings", self.baseurl);
let url = format!("{}api/0/settings", self.baseurl);
self.client.get(url).send().await?.json().await
}

Expand Down
17 changes: 15 additions & 2 deletions aw-server/src/endpoints/apikey.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,13 +111,18 @@ impl Fairing for ApiKeyCheck {
return;
}

let path = request.uri().path().as_str();

// Normalize leading slashes to prevent bypass via `//api/...`
let normalized_path = format!("/{}", path.trim_start_matches('/'));

// Only gate API endpoints — static web UI assets are not under /api/
if !request.uri().path().as_str().starts_with("/api/") {
if !normalized_path.starts_with("/api/") {
return;
}

// Always allow public API paths (e.g. /api/0/info for health checks)
if PUBLIC_PATHS.contains(&request.uri().path().as_str()) {
if PUBLIC_PATHS.contains(&normalized_path.as_str()) {
return;
}

Expand Down Expand Up @@ -205,6 +210,14 @@ mod tests {
.header(Header::new("Host", "localhost:5600"))
.dispatch();
assert_eq!(res.status(), Status::Unauthorized);

// Double slash should also require auth
let res = client
.get("//api/0/buckets/")
.header(ContentType::JSON)
.header(Header::new("Host", "localhost:5600"))
.dispatch();
assert_eq!(res.status(), Status::Unauthorized);
}
Comment on lines +214 to 221
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing test: //api/0/info should remain publicly accessible

The new test verifies that //api/0/buckets/ requires auth after normalization, but doesn't cover the complementary case: //api/0/info with a double-slash should still be allowed through without credentials (public path). Without this test, a future refactor that normalizes the path before the PUBLIC_PATHS check but in the wrong order could silently break the public endpoint for double-slash requests.

// Suggested addition inside test_api_key_required:
let res = client
    .get("//api/0/info")
    .header(ContentType::JSON)
    .header(Header::new("Host", "localhost:5600"))
    .dispatch();
assert_eq!(res.status(), Status::Ok);


#[test]
Expand Down
Loading