Skip to content
Merged
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
4 changes: 2 additions & 2 deletions src/backend/github.rs
Original file line number Diff line number Diff line change
Expand Up @@ -869,7 +869,7 @@ impl UnifiedGitBackend {
} else if self.is_forgejo() {
forgejo::get_headers(&download_url)
} else {
github::get_headers(&download_url)
github::get_headers(&download_url)?
};
HTTP.download_file_with_headers(&download_url, &artifact_path, &headers, None)
.await?;
Expand Down Expand Up @@ -1127,7 +1127,7 @@ impl UnifiedGitBackend {
} else if self.is_forgejo() {
forgejo::get_headers(&url)
} else {
github::get_headers(&url)
github::get_headers(&url)?
};

ctx.pr.set_message(format!("download {filename}"));
Expand Down
2 changes: 1 addition & 1 deletion src/backend/spm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,7 @@ impl SPMBackend {
let download_path = tv.download_path().join(&asset.name);
let headers = match provider.kind {
GitProviderKind::GitLab => gitlab::get_headers(&asset.url),
GitProviderKind::GitHub => github::get_headers(&asset.url),
GitProviderKind::GitHub => github::get_headers(&asset.url)?,
};
ctx.pr.set_message(format!("download {}", asset.name));
HTTP.download_file_with_headers(
Expand Down
100 changes: 74 additions & 26 deletions src/github.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::cache::{CacheManager, CacheManagerBuilder};
use crate::config::Settings;
use crate::tokens;
use crate::{dirs, env};
use eyre::Result;
use eyre::{Result, WrapErr};
use heck::ToKebabCase;
use reqwest::IntoUrl;
use reqwest::header::{HeaderMap, HeaderValue};
Expand Down Expand Up @@ -178,10 +178,10 @@ pub async fn list_releases_including_prereleases_from_url(
}

async fn list_releases_(api_url: &str, repo: &str) -> Result<Vec<GithubRelease>> {
let url = format!("{api_url}/repos/{repo}/releases?per_page=100");
let headers = get_headers(&url);
let mut url = format!("{api_url}/repos/{repo}/releases?per_page=100");
let headers = get_headers(&url)?;
let (mut releases, mut headers) = crate::http::HTTP_FETCH
.json_headers_with_headers::<Vec<GithubRelease>, _>(url, &headers)
.json_headers_with_headers::<Vec<GithubRelease>, _>(&url, &headers)
.await?;

// Fetch additional pages when MISE_LIST_ALL_VERSIONS is set, or (bounded) while
Expand All @@ -197,9 +197,10 @@ async fn list_releases_(api_url: &str, repo: &str) -> Result<Vec<GithubRelease>>
{
break;
}
headers = get_headers(&next);
url = resolve_pagination_url(&url, &next)?;
headers = get_headers(&url)?;
let (more, h) = crate::http::HTTP_FETCH
.json_headers_with_headers::<Vec<GithubRelease>, _>(next, &headers)
.json_headers_with_headers::<Vec<GithubRelease>, _>(&url, &headers)
.await?;
releases.extend(more);
headers = h;
Expand Down Expand Up @@ -231,17 +232,18 @@ pub async fn list_tags_from_url(api_url: &str, repo: &str) -> Result<Vec<String>
}

async fn list_tags_(api_url: &str, repo: &str) -> Result<Vec<String>> {
let url = format!("{api_url}/repos/{repo}/tags?per_page=100");
let headers = get_headers(&url);
let mut url = format!("{api_url}/repos/{repo}/tags?per_page=100");
let headers = get_headers(&url)?;
let (mut tags, mut headers) = crate::http::HTTP_FETCH
.json_headers_with_headers::<Vec<GithubTag>, _>(url, &headers)
.json_headers_with_headers::<Vec<GithubTag>, _>(&url, &headers)
.await?;

if *env::MISE_LIST_ALL_VERSIONS {
while let Some(next) = next_page(&headers) {
headers = get_headers(&next);
url = resolve_pagination_url(&url, &next)?;
headers = get_headers(&url)?;
let (more, h) = crate::http::HTTP_FETCH
.json_headers_with_headers::<Vec<GithubTag>, _>(next, &headers)
.json_headers_with_headers::<Vec<GithubTag>, _>(&url, &headers)
.await?;
tags.extend(more);
headers = h;
Expand All @@ -258,17 +260,18 @@ pub async fn list_tags_with_dates(repo: &str) -> Result<Vec<GithubTagWithDate>>
}

async fn list_tags_with_dates_(api_url: &str, repo: &str) -> Result<Vec<GithubTagWithDate>> {
let url = format!("{api_url}/repos/{repo}/tags?per_page=100");
let headers = get_headers(&url);
let mut url = format!("{api_url}/repos/{repo}/tags?per_page=100");
let headers = get_headers(&url)?;
let (mut tags, mut response_headers) = crate::http::HTTP_FETCH
.json_headers_with_headers::<Vec<GithubTag>, _>(url, &headers)
.json_headers_with_headers::<Vec<GithubTag>, _>(&url, &headers)
.await?;

// Fetch all pages when MISE_LIST_ALL_VERSIONS is set
while let Some(next) = next_page(&response_headers) {
response_headers = get_headers(&next);
url = resolve_pagination_url(&url, &next)?;
response_headers = get_headers(&url)?;
let (more, h) = crate::http::HTTP_FETCH
.json_headers_with_headers::<Vec<GithubTag>, _>(next, &response_headers)
.json_headers_with_headers::<Vec<GithubTag>, _>(&url, &response_headers)
.await?;
tags.extend(more);
response_headers = h;
Expand All @@ -277,7 +280,7 @@ async fn list_tags_with_dates_(api_url: &str, repo: &str) -> Result<Vec<GithubTa
// Fetch commit dates in parallel using the parallel utility
let results = crate::parallel::parallel(tags, |tag| async move {
let date = if let Some(commit) = tag.commit {
let headers = get_headers(&commit.url);
let headers = get_headers(&commit.url)?;
match crate::http::HTTP_FETCH
.json_with_headers::<GithubCommit, _>(&commit.url, &headers)
.await
Expand Down Expand Up @@ -437,7 +440,7 @@ async fn get_release_with_options(
} else {
format!("{api_url}/repos/{repo}/releases/tags/{tag}")
};
let headers = get_headers(&url);
let headers = get_headers(&url)?;
crate::http::HTTP_FETCH
.json_with_headers(url, &headers)
.await
Expand All @@ -457,6 +460,20 @@ fn next_page(headers: &HeaderMap) -> Option<String> {
.map(|c| c.get(1).unwrap().as_str().to_string())
}

fn resolve_pagination_url(current: &str, next: &str) -> Result<String> {
if next.starts_with("http://") || next.starts_with("https://") {
return Ok(next.to_string());
}
let base = url::Url::parse(current)
.wrap_err_with(|| format!("invalid pagination base URL: {current}"))?;
if next.starts_with('/') {
return Ok(format!("{}{next}", base.origin().ascii_serialization()));
}
base.join(next)
.map(|u| u.to_string())
.wrap_err_with(|| format!("invalid pagination URL: {next}"))
}

fn cache_dir() -> PathBuf {
dirs::CACHE.join("github")
}
Expand Down Expand Up @@ -646,9 +663,11 @@ pub fn resolve_token_for_api_url(api_url: &str) -> Option<String> {
resolve_token(host).map(|(t, _)| t)
}

pub fn get_headers<U: IntoUrl>(url: U) -> HeaderMap {
pub fn get_headers<U: IntoUrl>(url: U) -> Result<HeaderMap> {
let mut headers = HeaderMap::new();
let url = url.into_url().unwrap();
let url = url
.into_url()
.wrap_err("invalid request URL for GitHub auth headers")?;

if is_github_api_url(&url)
&& let Some((token, _source)) = resolve_token(url.host_str().unwrap_or("github.com"))
Expand All @@ -670,7 +689,7 @@ pub fn get_headers<U: IntoUrl>(url: U) -> HeaderMap {
);
}

headers
Ok(headers)
}

// ── github_tokens.toml ──────────────────────────────────────────────
Expand Down Expand Up @@ -889,7 +908,7 @@ something_else = "value"
"https://octocorp.ghe.com/api/v3/repos/owner/repo/releases",
"https://octocorp.ghe.com/owner/repo/releases/download/v1.0.0/file.tar.gz",
] {
let headers = get_headers(url);
let headers = get_headers(url).unwrap();
assert!(
!headers.contains_key(reqwest::header::AUTHORIZATION),
"{url} should not use GitHub auth"
Expand All @@ -900,25 +919,54 @@ something_else = "value"
);
}

let headers = get_headers("https://api.github.com/repos/owner/repo/releases");
let headers = get_headers("https://api.github.com/repos/owner/repo/releases").unwrap();
assert!(headers.contains_key(reqwest::header::AUTHORIZATION));
assert!(headers.contains_key("x-github-api-version"));

let headers = get_headers("https://api.github.com/repos/owner/repo/releases/assets/1");
let headers =
get_headers("https://api.github.com/repos/owner/repo/releases/assets/1").unwrap();
assert!(headers.contains_key(reqwest::header::AUTHORIZATION));
assert_eq!(headers.get("accept").unwrap(), "application/octet-stream");

let headers =
get_headers("https://github.example.com/api/v3/repos/owner/repo/releases");
get_headers("https://github.example.com/api/v3/repos/owner/repo/releases").unwrap();
assert!(headers.contains_key(reqwest::header::AUTHORIZATION));
assert!(headers.contains_key("x-github-api-version"));

let headers = get_headers("https://api.octocorp.ghe.com/repos/owner/repo/releases");
let headers =
get_headers("https://api.octocorp.ghe.com/repos/owner/repo/releases").unwrap();
assert!(headers.contains_key(reqwest::header::AUTHORIZATION));
assert!(headers.contains_key("x-github-api-version"));
});
}

#[test]
fn test_get_headers_rejects_relative_url() {
let err = get_headers("/repos/jdx/aube/releases").unwrap_err();
assert!(
err.to_string()
.contains("invalid request URL for GitHub auth headers"),
"unexpected error: {err}"
);
}

#[test]
fn test_resolve_pagination_url() {
let base = "https://api.github.com/repos/jdx/aube/releases?per_page=100";
assert_eq!(
resolve_pagination_url(base, "/repos/jdx/aube/releases?page=2").unwrap(),
"https://api.github.com/repos/jdx/aube/releases?page=2"
);
assert_eq!(
resolve_pagination_url(
base,
"https://api.github.com/repos/jdx/aube/releases?page=2"
)
.unwrap(),
"https://api.github.com/repos/jdx/aube/releases?page=2"
);
}

fn make_release(tag: &str) -> GithubRelease {
GithubRelease {
tag_name: tag.to_string(),
Expand Down
22 changes: 11 additions & 11 deletions src/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,8 @@ impl Client {
}

pub async fn get_async<U: IntoUrl>(&self, url: U) -> Result<Response> {
let url = url.into_url().unwrap();
let headers = host_auth_headers(&url);
let url = url.into_url()?;
let headers = host_auth_headers(&url)?;
self.get_async_with_headers(url, &headers).await
}

Expand Down Expand Up @@ -178,8 +178,8 @@ impl Client {
}

pub async fn head<U: IntoUrl>(&self, url: U) -> Result<Response> {
let url = url.into_url().unwrap();
let headers = host_auth_headers(&url);
let url = url.into_url()?;
let headers = host_auth_headers(&url)?;
self.head_async_with_headers(url, &headers).await
}

Expand Down Expand Up @@ -358,7 +358,7 @@ impl Client {
pr: Option<&dyn SingleReport>,
) -> Result<()> {
let url = url.into_url()?;
let headers = host_auth_headers(&url);
let headers = host_auth_headers(&url)?;
self.download_file_with_headers(url, path, &headers, pr)
.await
}
Expand Down Expand Up @@ -699,7 +699,7 @@ impl TextRequest<'_> {
pub async fn send(mut self) -> Result<String> {
ensure!(!Settings::get().offline(), "offline mode is enabled");
// Merge GitHub headers with any extra headers provided
let mut headers = host_auth_headers(&self.url);
let mut headers = host_auth_headers(&self.url)?;
headers.extend(self.extra_headers.clone());
let resp = self
.client
Expand Down Expand Up @@ -822,26 +822,26 @@ pub fn error_code(e: &Report) -> Option<u16> {
}
}

fn host_auth_headers(url: &Url) -> HeaderMap {
fn host_auth_headers(url: &Url) -> Result<HeaderMap> {
if crate::github::is_github_api_url(url) {
return crate::github::get_headers(url.as_str());
}

let Some(host) = url.host_str() else {
return HeaderMap::new();
return Ok(HeaderMap::new());
};

let is_gitlab = host == "gitlab.com" || crate::gitlab::is_gitlab_host(host);
if is_gitlab {
return crate::gitlab::get_headers(url.as_str());
return Ok(crate::gitlab::get_headers(url.as_str()));
}

let is_forgejo = host == "codeberg.org" || crate::forgejo::is_forgejo_host(host);
if is_forgejo {
return crate::forgejo::get_headers(url.as_str());
return Ok(crate::forgejo::get_headers(url.as_str()));
}

HeaderMap::new()
Ok(HeaderMap::new())
}

/// Get HTTP Basic authentication headers from netrc file for the given URL
Expand Down
Loading