Skip to content
This repository was archived by the owner on Jul 28, 2025. It is now read-only.

Commit 239c2a1

Browse files
committed
client: Add RegistryUrl type
This wraps a url::Url for a couple of purposes: - Asserts the invariant that the Url is a valid warg base URL - Provdies a `::safe_label` method which produces a mangled version of the URL appropriate for e.g. file paths This new type is used in client config and replaces the 'host' string used in keyring management.
1 parent 5e01fb6 commit 239c2a1

9 files changed

Lines changed: 262 additions & 128 deletions

File tree

crates/api/src/v1/paths.rs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
//! The paths of the Warg REST API.
22
3-
use warg_crypto::hash::AnyHash;
43
use warg_protocol::registry::{LogId, RecordId};
54

65
/// The path of the "fetch logs" API.
@@ -23,11 +22,6 @@ pub fn package_record(log_id: &LogId, record_id: &RecordId) -> String {
2322
format!("v1/package/{log_id}/record/{record_id}")
2423
}
2524

26-
/// The path for a package record's content.
27-
pub fn package_record_content(log_id: &LogId, record_id: &RecordId, digest: &AnyHash) -> String {
28-
format!("v1/package/{log_id}/record/{record_id}/content/{digest}")
29-
}
30-
3125
/// The path for proving checkpoint consistency.
3226
pub fn prove_consistency() -> &'static str {
3327
"v1/proof/consistency"

crates/client/src/api.rs

Lines changed: 14 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
//! A module for Warg registry API clients.
22
3-
use anyhow::{anyhow, bail, Context, Result};
3+
use anyhow::{anyhow, Result};
44
use bytes::Bytes;
55
use futures_util::{Stream, TryStreamExt};
6-
use reqwest::{Body, IntoUrl, Response, StatusCode, Url};
6+
use reqwest::{Body, IntoUrl, Response, StatusCode};
77
use serde::de::DeserializeOwned;
88
use thiserror::Error;
9-
use url::Host;
109
use warg_api::v1::{
1110
fetch::{FetchError, FetchLogsRequest, FetchLogsResponse},
1211
package::{
@@ -27,6 +26,8 @@ use warg_transparency::{
2726
map::MapProofBundle,
2827
};
2928

29+
use crate::registry_url::RegistryUrl;
30+
3031
/// Represents an error that occurred while communicating with the registry.
3132
#[derive(Debug, Error)]
3233
pub enum ClientError {
@@ -122,71 +123,28 @@ async fn into_result<T: DeserializeOwned, E: DeserializeOwned + Into<ClientError
122123
/// Represents a Warg API client for communicating with
123124
/// a Warg registry server.
124125
pub struct Client {
125-
url: Url,
126+
url: RegistryUrl,
126127
client: reqwest::Client,
127128
}
128129

129130
impl Client {
130131
/// Creates a new API client with the given URL.
131132
pub fn new(url: impl IntoUrl) -> Result<Self> {
132-
let url = Self::validate_url(url)?;
133+
let url = RegistryUrl::new(url)?;
133134
Ok(Self {
134135
url,
135136
client: reqwest::Client::new(),
136137
})
137138
}
138139

139140
/// Gets the URL of the API client.
140-
pub fn url(&self) -> &str {
141-
self.url.as_str()
142-
}
143-
144-
/// Parses and validates the given URL.
145-
///
146-
/// Returns the validated URL on success.
147-
pub fn validate_url(url: impl IntoUrl) -> Result<Url> {
148-
// Default to a HTTPS scheme if none is provided
149-
let url: Url = if !url.as_str().contains("://") {
150-
Url::parse(&format!("https://{url}", url = url.as_str()))
151-
.context("failed to parse registry server URL")?
152-
} else {
153-
url.into_url()
154-
.context("failed to parse registry server URL")?
155-
};
156-
157-
match url.scheme() {
158-
"https" => {}
159-
"http" => {
160-
// Only allow HTTP connections to loopback
161-
match url
162-
.host()
163-
.ok_or_else(|| anyhow!("expected a host for URL `{url}`"))?
164-
{
165-
Host::Domain(d) => {
166-
if d != "localhost" {
167-
bail!("an unsecured connection is not permitted to `{d}`");
168-
}
169-
}
170-
Host::Ipv4(ip) => {
171-
if !ip.is_loopback() {
172-
bail!("an unsecured connection is not permitted to address `{ip}`");
173-
}
174-
}
175-
Host::Ipv6(ip) => {
176-
if !ip.is_loopback() {
177-
bail!("an unsecured connection is not permitted to address `{ip}`");
178-
}
179-
}
180-
}
181-
}
182-
_ => bail!("expected a HTTPS scheme for URL `{url}`"),
183-
}
184-
Ok(url)
141+
pub fn url(&self) -> &RegistryUrl {
142+
&self.url
185143
}
186144

187145
/// Gets the latest checkpoint from the registry.
188146
pub async fn latest_checkpoint(&self) -> Result<SerdeEnvelope<MapCheckpoint>, ClientError> {
189-
let url = self.url.join(paths::fetch_checkpoint()).unwrap();
147+
let url = self.url.join(paths::fetch_checkpoint());
190148
tracing::debug!("getting latest checkpoint at `{url}`");
191149
into_result::<_, FetchError>(reqwest::get(url).await?).await
192150
}
@@ -196,7 +154,7 @@ impl Client {
196154
&self,
197155
request: FetchLogsRequest<'_>,
198156
) -> Result<FetchLogsResponse, ClientError> {
199-
let url = self.url.join(paths::fetch_logs()).unwrap();
157+
let url = self.url.join(paths::fetch_logs());
200158
tracing::debug!("fetching logs at `{url}`");
201159

202160
let response = self.client.post(url).json(&request).send().await?;
@@ -209,10 +167,7 @@ impl Client {
209167
log_id: &LogId,
210168
request: PublishRecordRequest<'_>,
211169
) -> Result<PackageRecord, ClientError> {
212-
let url = self
213-
.url
214-
.join(&paths::publish_package_record(log_id))
215-
.unwrap();
170+
let url = self.url.join(&paths::publish_package_record(log_id));
216171
tracing::debug!(
217172
"appending record to package `{id}` at `{url}`",
218173
id = request.id
@@ -228,10 +183,7 @@ impl Client {
228183
log_id: &LogId,
229184
record_id: &RecordId,
230185
) -> Result<PackageRecord, ClientError> {
231-
let url = self
232-
.url
233-
.join(&paths::package_record(log_id, record_id))
234-
.unwrap();
186+
let url = self.url.join(&paths::package_record(log_id, record_id));
235187
tracing::debug!("getting record `{record_id}` for package `{log_id}` at `{url}`");
236188

237189
let response = reqwest::get(url).await?;
@@ -283,7 +235,7 @@ impl Client {
283235

284236
/// Proves the inclusion of the given package log heads in the registry.
285237
pub async fn prove_inclusion(&self, request: InclusionRequest<'_>) -> Result<(), ClientError> {
286-
let url = self.url.join(paths::prove_inclusion()).unwrap();
238+
let url = self.url.join(paths::prove_inclusion());
287239
tracing::debug!("proving checkpoint inclusion at `{url}`");
288240

289241
let response = into_result::<InclusionResponse, ProofError>(
@@ -303,7 +255,7 @@ impl Client {
303255
&self,
304256
request: ConsistencyRequest<'_>,
305257
) -> Result<(), ClientError> {
306-
let url = self.url.join(paths::prove_consistency()).unwrap();
258+
let url = self.url.join(paths::prove_consistency());
307259
let response = into_result::<ConsistencyResponse, ProofError>(
308260
self.client.post(url).json(&request).send().await?,
309261
)

crates/client/src/config.rs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
//! Module for client configuration.
22
3-
use crate::{api, ClientError};
3+
use crate::{ClientError, RegistryUrl};
44
use anyhow::{anyhow, Context, Result};
55
use normpath::PathExt;
66
use once_cell::sync::Lazy;
7-
use reqwest::Url;
87
use serde::{Deserialize, Serialize};
98
use std::{
109
env::current_dir,
@@ -63,7 +62,7 @@ fn normalize_path(path: &Path) -> PathBuf {
6362
/// Paths used for storage
6463
pub struct StoragePaths {
6564
/// The registry URL relating to the storage paths.
66-
pub url: Url,
65+
pub registry_url: RegistryUrl,
6766
/// The path to the registry storage directory.
6867
pub registries_dir: PathBuf,
6968
/// The path to the content storage directory.
@@ -242,16 +241,16 @@ impl Config {
242241
&self,
243242
url: Option<&str>,
244243
) -> Result<StoragePaths, ClientError> {
245-
let url = api::Client::validate_url(
244+
let registry_url = RegistryUrl::new(
246245
url.or(self.default_url.as_deref())
247246
.ok_or(ClientError::NoDefaultUrl)?,
248247
)?;
249248

250-
let host = url.host().unwrap().to_string().to_ascii_lowercase();
251-
let registries_dir = self.registries_dir()?.join(host);
249+
let label = registry_url.safe_label();
250+
let registries_dir = self.registries_dir()?.join(label);
252251
let content_dir = self.content_dir()?;
253252
Ok(StoragePaths {
254-
url,
253+
registry_url,
255254
registries_dir,
256255
content_dir,
257256
})

crates/client/src/lib.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ pub mod api;
3030
mod config;
3131
pub mod lock;
3232
pub mod storage;
33+
mod registry_url;
3334
pub use self::config::*;
35+
pub use self::registry_url::RegistryUrl;
3436

3537
/// A client for a Warg registry.
3638
pub struct Client<R, C> {
@@ -51,7 +53,7 @@ impl<R: RegistryStorage, C: ContentStorage> Client<R, C> {
5153
}
5254

5355
/// Gets the URL of the client.
54-
pub fn url(&self) -> &str {
56+
pub fn url(&self) -> &RegistryUrl {
5557
self.api.url()
5658
}
5759

@@ -580,7 +582,7 @@ impl FileSystemClient {
580582
config: &Config,
581583
) -> Result<StorageLockResult<Self>, ClientError> {
582584
let StoragePaths {
583-
url,
585+
registry_url: url,
584586
registries_dir,
585587
content_dir,
586588
} = config.storage_paths_for_url(url)?;
@@ -595,7 +597,9 @@ impl FileSystemClient {
595597
};
596598

597599
Ok(StorageLockResult::Acquired(Self::new(
598-
url, packages, content,
600+
url.into_url(),
601+
packages,
602+
content,
599603
)?))
600604
}
601605

@@ -607,12 +611,12 @@ impl FileSystemClient {
607611
/// This method blocks if storage locks cannot be acquired.
608612
pub fn new_with_config(url: Option<&str>, config: &Config) -> Result<Self, ClientError> {
609613
let StoragePaths {
610-
url,
614+
registry_url,
611615
registries_dir,
612616
content_dir,
613617
} = config.storage_paths_for_url(url)?;
614618
Self::new(
615-
url,
619+
registry_url.into_url(),
616620
FileSystemRegistryStorage::lock(registries_dir)?,
617621
FileSystemContentStorage::lock(content_dir)?,
618622
)

0 commit comments

Comments
 (0)