Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add authorization #1

Closed
wants to merge 4 commits into from
Closed
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
64 changes: 49 additions & 15 deletions crates/cargo-test-support/src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,61 +15,75 @@ use std::sync::Arc;
use std::thread;
use tar::{Builder, Header};
use url::Url;
use std::ops::Deref;

/// Gets the path to the local index pretending to be crates.io. This is a Git repo
/// initialized with a `config.json` file pointing to `dl_path` for downloads
/// and `api_path` for uploads.
pub fn registry_path() -> PathBuf {
generate_path("registry")
}

pub fn registry_url() -> Url {
generate_url("registry")
}

/// Gets the path for local web API uploads. Cargo will place the contents of a web API
/// request here. For example, `api/v1/crates/new` is the result of publishing a crate.
pub fn api_path() -> PathBuf {
generate_path("api")
}

pub fn api_url() -> Url {
generate_url("api")
}

/// Gets the path where crates can be downloaded using the web API endpoint. Crates
/// should be organized as `{name}/{version}/download` to match the web API
/// endpoint. This is rarely used and must be manually set up.
pub fn dl_path() -> PathBuf {
generate_path("dl")
}

pub fn dl_url() -> Url {
generate_url("dl")
}

/// Gets the alternative-registry version of `registry_path`.
pub fn alt_registry_path() -> PathBuf {
generate_path("alternative-registry")
}

pub fn alt_registry_url() -> Url {
generate_url("alternative-registry")
}

/// Gets the alternative-registry version of `dl_path`.
pub fn alt_dl_path() -> PathBuf {
generate_path("alt_dl")
}

pub fn alt_dl_url() -> String {
generate_alt_dl_url("alt_dl")
}

/// Gets the alternative-registry version of `api_path`.
pub fn alt_api_path() -> PathBuf {
generate_path("alt_api")
}

pub fn alt_api_url() -> Url {
generate_url("alt_api")
}

pub fn generate_path(name: &str) -> PathBuf {
paths::root().join(name)
}

pub fn generate_url(name: &str) -> Url {
Url::from_file_path(generate_path(name)).ok().unwrap()
}

pub fn generate_alt_dl_url(name: &str) -> String {
let base = Url::from_file_path(generate_path(name)).ok().unwrap();
format!("{}/{{crate}}/{{version}}/{{crate}}-{{version}}.crate", base)
Expand Down Expand Up @@ -239,7 +253,7 @@ impl Drop for RegistryServer {
}

#[must_use]
pub fn serve_registry(registry_path: PathBuf) -> RegistryServer {
pub fn serve_registry(registry_path: PathBuf, auth_token: Option<String>) -> RegistryServer {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap();
let done = Arc::new(AtomicBool::new(false));
Expand Down Expand Up @@ -274,6 +288,7 @@ pub fn serve_registry(registry_path: PathBuf) -> RegistryServer {
// Grab some other headers we may care about.
let mut if_modified_since = None;
let mut if_none_match = None;
let mut token = None;
loop {
line.clear();
if buf.read_line(&mut line).unwrap() == 0 {
Expand All @@ -297,9 +312,21 @@ pub fn serve_registry(registry_path: PathBuf) -> RegistryServer {
if_modified_since = Some(value.to_owned());
} else if line.starts_with("If-None-Match:") {
if_none_match = Some(value.trim_matches('"').to_owned());
// It would make sense to check the auth. header before the "file.exists()", as
// everything after can be skipped. For the tests, it should be enough to check it here.
} else if line.starts_with("Authorization:") {
token = Some(value.to_owned())
}
}

// Check if authorization is expected and
// an authorization token is provided.
let authorized = match (&auth_token, token) {
(Some(expected), Some(actual)) => expected.deref() == actual,
(Some(expected), None) => false,
(_, _) => true
};

// Now grab info about the file.
let data = fs::read(&file).unwrap();
let etag = Sha256::new().update(&data).finish_hex();
Expand All @@ -325,7 +352,12 @@ pub fn serve_registry(registry_path: PathBuf) -> RegistryServer {
}

// Write out the main response line.
if any_match && all_match {
if !authorized {
buf.get_mut()
.write_all(b"HTTP/1.1 401 Unauthorized\r\n\r\n")
.unwrap();
}
else if any_match && all_match {
buf.get_mut()
.write_all(b"HTTP/1.1 304 Not Modified\r\n")
.unwrap();
Expand All @@ -335,19 +367,21 @@ pub fn serve_registry(registry_path: PathBuf) -> RegistryServer {
// TODO: Support 451 for crate index deletions.

// Write out other headers.
buf.get_mut()
.write_all(format!("Content-Length: {}\r\n", data.len()).as_bytes())
.unwrap();
buf.get_mut()
.write_all(format!("ETag: \"{}\"\r\n", etag).as_bytes())
.unwrap();
buf.get_mut()
.write_all(format!("Last-Modified: {}\r\n", last_modified).as_bytes())
.unwrap();
if authorized {
buf.get_mut()
.write_all(format!("Content-Length: {}\r\n", data.len()).as_bytes())
.unwrap();
buf.get_mut()
.write_all(format!("ETag: \"{}\"\r\n", etag).as_bytes())
.unwrap();
buf.get_mut()
.write_all(format!("Last-Modified: {}\r\n", last_modified).as_bytes())
.unwrap();

// And finally, write out the body.
buf.get_mut().write_all(b"\r\n").unwrap();
buf.get_mut().write_all(&data).unwrap();
// And finally, write out the body.
buf.get_mut().write_all(b"\r\n").unwrap();
buf.get_mut().write_all(&data).unwrap();
}
} else {
loop {
line.clear();
Expand Down Expand Up @@ -593,7 +627,7 @@ impl Package {
"yanked": self.yanked,
"links": self.links,
})
.to_string();
.to_string();

let file = match self.name.len() {
1 => format!("1/{}", self.name),
Expand Down
4 changes: 4 additions & 0 deletions src/cargo/core/source/source_id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,10 @@ impl SourceId {
pub fn full_hash<S: hash::Hasher>(self, into: &mut S) {
ptr::NonNull::from(self.inner).hash(into)
}

pub fn get_name(&self) -> Option<String> {
self.inner.name.clone()
}
}

impl PartialEq for SourceId {
Expand Down
16 changes: 16 additions & 0 deletions src/cargo/sources/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ struct SourceConfigDef {
directory: Option<ConfigRelativePath>,
/// A registry source. Value is a URL.
registry: OptValue<String>,
/// An optional authorization token
token: Option<String>,
/// A local registry source.
local_registry: Option<ConfigRelativePath>,
/// A git source. Value is a URL.
Expand All @@ -51,6 +53,7 @@ struct SourceConfigDef {
/// [source.crates-io]
/// registry = 'https://github.com/rust-lang/crates.io-index'
/// replace-with = 'foo' # optional
/// token = 'authorization_token' # optional
/// ```
#[derive(Clone)]
struct SourceConfig {
Expand All @@ -64,6 +67,11 @@ struct SourceConfig {
/// this configuration key was defined (such as the `.cargo/config` path
/// or the environment variable name).
replace_with: Option<(String, String)>,

/// Optional authorization token.
///
/// Use to authorize any request to a private registry.
token: Option<String>,
}

impl<'cfg> SourceConfigMap<'cfg> {
Expand All @@ -89,11 +97,17 @@ impl<'cfg> SourceConfigMap<'cfg> {
SourceConfig {
id: SourceId::crates_io(config)?,
replace_with: None,
token: None,
},
)?;
Ok(base)
}

pub fn token(&self, source_id: &SourceId) -> Option<String> {
let name: &str = self.id2name[source_id].as_ref();
self.cfgs.get(name).and_then(|c| c.token.clone())
}

pub fn config(&self) -> &'cfg Config {
self.config
}
Expand Down Expand Up @@ -269,11 +283,13 @@ restore the source replacement configuration to continue the build
.replace_with
.map(|val| (val.val, val.definition.to_string()));


self.add(
&name,
SourceConfig {
id: src,
replace_with,
token: def.token,
},
)?;

Expand Down
40 changes: 36 additions & 4 deletions src/cargo/sources/registry/http_remote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ pub struct HttpRegistry<'cfg> {
/// Store the server URL without the protocol prefix (sparse+)
url: Url,

/// Store optional authorization token
token: Option<String>,

/// Cached HTTP handle for synchronous requests (RegistryData::load).
http: RefCell<Option<Easy>>,

Expand Down Expand Up @@ -160,12 +163,16 @@ impl<'cfg> HttpRegistry<'cfg> {
.into_url()
.expect("a url with the protocol stripped should still be valid");

// I bet there is better way to get to the token that I'm not aware of
let token=crate::sources::SourceConfigMap::new(&config).unwrap().token(&source_id);

HttpRegistry {
index_path: config.registry_index_path().join(name),
cache_path: config.registry_cache_path().join(name),
source_id,
config,
url,
token,
http: RefCell::new(None),
prefetch: Multi::new(),
multiplexing: false,
Expand Down Expand Up @@ -452,17 +459,22 @@ impl<'cfg> RegistryData for HttpRegistry<'cfg> {
try_old_curl!(handle.pipewait(true), "pipewait");

// Make sure we don't send data back if it's the same as we have in the index.
let mut list = List::new();
if let Some((ref etag, ref last_modified, _)) = was {
let mut list = List::new();
if !etag.is_empty() {
list.append(&format!("If-None-Match: {}", etag))?;
}
if !last_modified.is_empty() {
list.append(&format!("If-Modified-Since: {}", last_modified))?;
}
handle.http_headers(list)?;
}

// If an authorization token is set, add it to the headers.
if let Some(ref auth_token) = self.token {
list.append(&format!("Authorization: {}", auth_token))?;
}
handle.http_headers(list)?;

// We're going to have a bunch of downloads all happening "at the same time".
// So, we need some way to track what headers/data/responses are for which request.
// We do that through this token. Each request (and associated response) gets one.
Expand Down Expand Up @@ -732,6 +744,21 @@ impl<'cfg> RegistryData for HttpRegistry<'cfg> {
paths::remove_file(pkg)?;
}
}
401 => {
// Not Authorized
//
// If the registry supports authorization and the authorization
// fails for some reason, e.g. missing or wrong token.
eprintln!("error: authorization failed. Request needs to be authorized with a token."); // Not sure what the correct why of displaying a meaningful error message is

// Let cargo handle the "Not Authorized" case the same way, it handles a "Not Found", as in the end
// the package is not available to use.
let path = self.config.assert_package_cache_locked(&self.index_path);
let pkg = path.join(&fetched.path);
if pkg.exists() {
paths::remove_file(pkg)?;
}
}
code => {
anyhow::bail!(
"prefetch: server returned unexpected HTTP status code {} for {}{}: {}",
Expand Down Expand Up @@ -880,13 +907,18 @@ impl<'cfg> RegistryData for HttpRegistry<'cfg> {
debug!("fetch {}{}", self.url, path.display());
handle.url(&format!("{}{}", self.url, path.display()))?;

let mut list = List::new();
if let Some((ref etag, ref last_modified, _)) = was {
let mut list = List::new();
list.append(&format!("If-None-Match: {}", etag))?;
list.append(&format!("If-Modified-Since: {}", last_modified))?;
handle.http_headers(list)?;
}

// If an authorization token is set, add it to the headers.
if let Some(ref auth_token) = self.token {
list.append(&format!("Authorization: {}", auth_token))?;
}
handle.http_headers(list)?;

let mut contents = Vec::new();
let mut etag = None;
let mut last_modified = None;
Expand Down
2 changes: 1 addition & 1 deletion src/cargo/util/config/de.rs
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,7 @@ impl<'de, 'config> de::MapAccess<'de> for ValueDeserializer<'config> {
seed.deserialize(Tuple2Deserializer(0i32, path.to_string_lossy()))
}
Definition::Environment(env) => {
seed.deserialize(Tuple2Deserializer(1i32, env.as_ref()))
seed.deserialize(Tuple2Deserializer(1i32, env.as_ref() as &str))
}
Definition::Cli => seed.deserialize(Tuple2Deserializer(2i32, "")),
}
Expand Down
Loading