Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/Skidbladnir

src/federation/nexus.rs

//! **Sonatype Nexus** harbour.
//!
//! Serves hosted/proxy content at `{base}/repository/{repo}/{layout-path}`, where
//! the layout path is the per-ecosystem mapping in [`upstream_path`]. Basic auth
//! (the Nexus norm) via the shared [`UpstreamAuth`].

#[cfg(feature = "federation")]
use anyhow::Result;

use super::{ArtifactCoord, RegistryFormat, UpstreamAuth, upstream_path};
#[cfg(feature = "federation")]
use super::{ArtifactMeta, UpstreamRegistry, verify_sha256};
#[cfg(feature = "federation")]
use super::{get_bytes, head_meta};

/// A read-only harbour proxying one Nexus hosted/proxy repository.
#[cfg_attr(not(feature = "federation"), allow(dead_code))]
pub struct NexusRegistry {
    name: String,
    /// Nexus base URL, e.g. `https://nexus.corp`.
    base_url: String,
    /// The hosted/proxy repository name, e.g. `maven-central` or `crates-proxy`.
    repo: String,
    auth: UpstreamAuth,
    format: RegistryFormat,
}

impl NexusRegistry {
    pub fn new(
        name: impl Into<String>,
        base_url: impl Into<String>,
        repo: impl Into<String>,
        auth: UpstreamAuth,
        format: RegistryFormat,
    ) -> Self {
        Self {
            name: name.into(),
            base_url: base_url.into().trim_end_matches('/').to_string(),
            repo: repo.into(),
            auth,
            format,
        }
    }

    /// Content URL for one artifact: `{base}/repository/{repo}/{layout-path}`.
    pub fn content_url(&self, id: &ArtifactCoord) -> String {
        format!(
            "{}/repository/{}/{}",
            self.base_url,
            self.repo,
            upstream_path(self.format, id)
        )
    }
}

#[cfg(feature = "federation")]
impl UpstreamRegistry for NexusRegistry {
    fn name(&self) -> &str {
        &self.name
    }
    fn base_url(&self) -> &str {
        &self.base_url
    }
    fn auth(&self) -> &UpstreamAuth {
        &self.auth
    }
    fn format(&self) -> RegistryFormat {
        self.format
    }

    fn fetch(&self, id: &ArtifactCoord) -> Result<Option<Vec<u8>>> {
        let (bytes, sha256) = match get_bytes(&self.base_url, &self.content_url(id), &self.auth)? {
            Some(b) => b,
            None => return Ok(None),
        };
        if let Some(expected) = sha256 {
            verify_sha256(&bytes, &expected)?;
        }
        Ok(Some(bytes))
    }

    fn metadata(&self, id: &ArtifactCoord) -> Result<Option<ArtifactMeta>> {
        head_meta(&self.base_url, &self.content_url(id), &self.auth)
    }
}

#[cfg(test)]
mod tests {
    #[cfg(feature = "federation")]
    use super::super::testutil::MockServer;
    use super::*;

    fn id(name: &str, version: &str) -> ArtifactCoord {
        ArtifactCoord::new(name, version)
    }

    #[test]
    fn content_url_uses_nexus_repository_layout() {
        let up = NexusRegistry::new(
            "nx",
            "https://nexus.corp/",
            "crates-proxy",
            UpstreamAuth::None,
            RegistryFormat::Rust,
        );
        assert_eq!(
            up.content_url(&id("serde", "1.0.0")),
            "https://nexus.corp/repository/crates-proxy/serde/serde-1.0.0.crate"
        );
    }

    #[test]
    #[cfg(feature = "federation")]
    fn miss_returns_none_and_hit_returns_bytes_at_nexus_layout() {
        let server = MockServer::start(|method, path| {
            assert_eq!(method, "GET");
            if path == "/repository/crates-proxy/rand/rand-0.8.5.crate" {
                (200, b"rand-crate-bytes".to_vec())
            } else {
                (404, b"nope".to_vec())
            }
        });
        let up = NexusRegistry::new(
            "nx",
            &server.base_url,
            "crates-proxy",
            UpstreamAuth::None,
            RegistryFormat::Rust,
        );
        assert_eq!(
            up.fetch(&id("rand", "0.8.5")).unwrap().as_deref(),
            Some(b"rand-crate-bytes".as_ref())
        );
        assert!(up.fetch(&id("ghost", "9.9.9")).unwrap().is_none());
        assert_eq!(
            server.requests()[0].path,
            "/repository/crates-proxy/rand/rand-0.8.5.crate"
        );
    }

    #[test]
    #[cfg(feature = "federation")]
    fn basic_auth_header_is_sent() {
        let server = MockServer::start(|_m, _p| (200, b"bytes".to_vec()));
        let up = NexusRegistry::new(
            "nx",
            &server.base_url,
            "repo",
            UpstreamAuth::Basic {
                username: "admin".into(),
                password: "s3cret".into(),
            },
            RegistryFormat::Rust,
        );
        up.fetch(&id("serde", "1.0.0")).unwrap();
        // base64("admin:s3cret") == YWRtaW46czNjcmV0
        assert_eq!(
            server.requests()[0].header("authorization"),
            Some("Basic YWRtaW46czNjcmV0")
        );
        crate::functional_status(
            "skidbladnir/federation-nexus",
            "basic_auth_attached",
            server.requests()[0].header("authorization") == Some("Basic YWRtaW46czNjcmV0"),
            "Nexus Basic auth attached",
        );
    }

    /// The ship's primary voyage: mirror a coordinate set, sink each fetched
    /// artifact, skip the 404s, and report the mirrored count.
    #[test]
    #[cfg(feature = "federation")]
    fn mirror_fetches_each_coord_and_skips_missing() {
        let server = MockServer::start(|_m, path| {
            if path.ends_with("rand-0.8.5.crate") {
                (200, b"rand".to_vec())
            } else if path.ends_with("serde-1.0.0.crate") {
                (200, b"serde".to_vec())
            } else {
                (404, b"no".to_vec())
            }
        });
        let up = NexusRegistry::new(
            "nx",
            &server.base_url,
            "crates-proxy",
            UpstreamAuth::None,
            RegistryFormat::Rust,
        );
        let coords = vec![
            id("rand", "0.8.5"),
            id("serde", "1.0.0"),
            id("ghost", "0.0.0"),
        ];
        let mut captured: Vec<(String, Vec<u8>)> = Vec::new();
        let n = up
            .mirror(&coords, &mut |c, bytes| {
                captured.push((c.name.clone(), bytes));
                Ok(())
            })
            .unwrap();
        assert_eq!(n, 2, "two present, one missing");
        assert_eq!(captured.len(), 2);
        assert_eq!(captured[0], ("rand".to_string(), b"rand".to_vec()));
        crate::functional_status(
            "skidbladnir/federation-nexus",
            "mirror_voyage",
            n == 2 && captured.len() == 2,
            "mirror fetched 2 of 3 coords (skipped the 404)",
        );
    }
}