Skip to main content

soma_self_update/
directive.rs

1use std::net::IpAddr;
2
3use url::Url;
4
5use crate::{ArtifactTransportPolicy, Result, UpdateDirective, UpdateError};
6
7impl UpdateDirective {
8    /// Resolves the artifact reference against a heartbeat/API endpoint.
9    pub fn resolve_artifact_url(
10        &self,
11        endpoint: &Url,
12        policy: ArtifactTransportPolicy,
13    ) -> Result<Url> {
14        if endpoint.cannot_be_a_base() || endpoint.host_str().is_none() {
15            return Err(UpdateError::InvalidBaseUrl {
16                url: endpoint.to_string(),
17                message: "URL must be hierarchical and contain a host".into(),
18            });
19        }
20        let artifact = endpoint.join(self.artifact_url()).map_err(|error| {
21            UpdateError::InvalidArtifactUrl {
22                url: self.artifact_url().to_owned(),
23                message: error.to_string(),
24            }
25        })?;
26        validate_artifact_url(endpoint, &artifact, policy)?;
27        Ok(artifact)
28    }
29
30    /// Validates one redirect hop or the final response URL against the
31    /// authenticated endpoint and transport policy.
32    ///
33    /// Transport adapters must disable automatic redirects or invoke this for
34    /// every redirect target and again for the final response URL.
35    pub fn validate_artifact_response_url(
36        &self,
37        endpoint: &Url,
38        response_url: &Url,
39        policy: ArtifactTransportPolicy,
40    ) -> Result<()> {
41        self.resolve_artifact_url(endpoint, policy)?;
42        validate_artifact_url(endpoint, response_url, policy)
43    }
44}
45
46fn validate_artifact_url(
47    endpoint: &Url,
48    artifact: &Url,
49    policy: ArtifactTransportPolicy,
50) -> Result<()> {
51    if artifact.cannot_be_a_base() || artifact.host_str().is_none() {
52        return Err(UpdateError::InvalidArtifactUrl {
53            url: artifact.to_string(),
54            message: "URL must be hierarchical and contain a host".into(),
55        });
56    }
57    let same_origin = endpoint.scheme() == artifact.scheme()
58        && endpoint.host() == artifact.host()
59        && endpoint.port_or_known_default() == artifact.port_or_known_default();
60    if !same_origin {
61        return Err(UpdateError::CrossOriginArtifact {
62            base: endpoint.to_string(),
63            artifact: artifact.to_string(),
64        });
65    }
66    let secure = artifact.scheme() == "https";
67    let loopback_http =
68        artifact.scheme() == "http" && is_loopback(artifact.host_str().unwrap_or_default());
69    if !(secure || policy == ArtifactTransportPolicy::HttpsOrLoopbackHttp && loopback_http) {
70        return Err(UpdateError::InsecureTransport(artifact.to_string()));
71    }
72    Ok(())
73}
74
75fn is_loopback(host: &str) -> bool {
76    host.eq_ignore_ascii_case("localhost")
77        || host
78            .trim_matches(['[', ']'])
79            .parse::<IpAddr>()
80            .is_ok_and(|address| address.is_loopback())
81}