Skip to content

Commit 5fa9583

Browse files
authored
fix(cli): accept Artifact downloads from a service subdomain (#134)
1 parent 0bdd451 commit 5fa9583

2 files changed

Lines changed: 77 additions & 6 deletions

File tree

crates/skilld-command/src/remote.rs

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1098,7 +1098,7 @@ impl SkilldRemote {
10981098
body: vec![],
10991099
response_limit: limit,
11001100
};
1101-
self.execute(request, AllowedOrigin::Service(self.endpoint.clone()))
1101+
self.execute(request, AllowedOrigin::Artifact(self.endpoint.clone()))
11021102
.map(|response| response.body)
11031103
}
11041104

@@ -2371,9 +2371,29 @@ fn invalid_update_response() -> RemoteError {
23712371
#[derive(Clone)]
23722372
enum AllowedOrigin {
23732373
Service(Url),
2374+
/// Artifact bytes come from the service origin or from one of its
2375+
/// subdomains, such as `artifacts.skilld.dev` for `skilld.dev`.
2376+
Artifact(Url),
23742377
Github,
23752378
}
23762379

2380+
fn same_origin(url: &Url, base: &Url) -> bool {
2381+
url.scheme() == base.scheme()
2382+
&& url.host_str() == base.host_str()
2383+
&& url.port_or_known_default() == base.port_or_known_default()
2384+
}
2385+
2386+
fn is_https_subdomain_of(url: &Url, base: &Url) -> bool {
2387+
let (Some(host), Some(base_host)) = (url.host_str(), base.host_str()) else {
2388+
return false;
2389+
};
2390+
url.scheme() == "https"
2391+
&& url.port().is_none()
2392+
&& host
2393+
.strip_suffix(base_host)
2394+
.is_some_and(|prefix| prefix.len() > 1 && prefix.ends_with('.'))
2395+
}
2396+
23772397
fn validate_request_url(value: &str, allowed: &AllowedOrigin) -> Result<(), RemoteError> {
23782398
let url = Url::parse(value)
23792399
.map_err(|_| RemoteError::new("INVALID_REMOTE_URL", "a remote URL is invalid"))?;
@@ -2388,11 +2408,8 @@ fn validate_url(url: &Url, allowed: &AllowedOrigin) -> Result<(), RemoteError> {
23882408
));
23892409
}
23902410
let allowed = match allowed {
2391-
AllowedOrigin::Service(base) => {
2392-
url.scheme() == base.scheme()
2393-
&& url.host_str() == base.host_str()
2394-
&& url.port_or_known_default() == base.port_or_known_default()
2395-
}
2411+
AllowedOrigin::Service(base) => same_origin(url, base),
2412+
AllowedOrigin::Artifact(base) => same_origin(url, base) || is_https_subdomain_of(url, base),
23962413
AllowedOrigin::Github => {
23972414
url.scheme() == "https"
23982415
&& url.host_str() == Some("api.github.com")

crates/skilld-command/tests/remote.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1745,6 +1745,60 @@ fn a_verified_remote_install_uses_resolution_root_grant_and_content_in_order() {
17451745
assert!(requests[3].url.ends_with("/content"));
17461746
}
17471747

1748+
#[test]
1749+
fn a_public_grant_may_serve_content_from_a_service_subdomain() {
1750+
let (pin, mut responses) = verified_remote_responses();
1751+
let mut grant: serde_json::Value = serde_json::from_slice(&responses[2].body).unwrap();
1752+
grant["contentUrl"] = json!("https://artifacts.skilld.dev/sha256/example");
1753+
responses[2] = response(200, serde_json::to_vec(&grant).unwrap());
1754+
let http = Arc::new(FakeHttp::with(responses));
1755+
let remote = SkilldRemote::new(
1756+
http.clone(),
1757+
Arc::new(NoTokenProvider),
1758+
NativeRemoteConfig::Pinned(pin),
1759+
)
1760+
.with_endpoint("https://skilld.dev")
1761+
.unwrap()
1762+
.with_sleeper(Arc::new(NoSleep));
1763+
let selector = RemoteSelector::parse("skilld:skilld-dev/skills/example").unwrap();
1764+
1765+
let prepared = remote.prepare(&selector, false).unwrap();
1766+
1767+
assert!(matches!(
1768+
prepared.source_status,
1769+
SourceStatus::Verified { .. }
1770+
));
1771+
let requests = http.requests.lock().unwrap();
1772+
assert_eq!(requests.len(), 4);
1773+
assert_eq!(
1774+
requests[3].url,
1775+
"https://artifacts.skilld.dev/sha256/example"
1776+
);
1777+
}
1778+
1779+
#[test]
1780+
fn a_public_grant_on_an_unrelated_origin_is_rejected_before_download() {
1781+
let (pin, mut responses) = verified_remote_responses();
1782+
let mut grant: serde_json::Value = serde_json::from_slice(&responses[2].body).unwrap();
1783+
grant["contentUrl"] = json!("https://example.com/sha256/example");
1784+
responses[2] = response(200, serde_json::to_vec(&grant).unwrap());
1785+
let http = Arc::new(FakeHttp::with(responses));
1786+
let remote = SkilldRemote::new(
1787+
http.clone(),
1788+
Arc::new(NoTokenProvider),
1789+
NativeRemoteConfig::Pinned(pin),
1790+
)
1791+
.with_endpoint("https://skilld.dev")
1792+
.unwrap()
1793+
.with_sleeper(Arc::new(NoSleep));
1794+
let selector = RemoteSelector::parse("skilld:skilld-dev/skills/example").unwrap();
1795+
1796+
let error = remote.prepare(&selector, false).unwrap_err();
1797+
1798+
assert_eq!(error.code, "REMOTE_ORIGIN_REJECTED");
1799+
assert_eq!(http.requests.lock().unwrap().len(), 3);
1800+
}
1801+
17481802
#[test]
17491803
fn a_private_artifact_download_sends_the_account_and_one_time_grant() {
17501804
let (pin, mut responses) = verified_remote_responses();

0 commit comments

Comments
 (0)