Fix bug in GithubSource when an access token is not provided

This commit is contained in:
Caelan Sayler
2025-06-07 07:14:41 +01:00
committed by Caelan
parent b18fac7d96
commit ed8600eee5
5 changed files with 67 additions and 35 deletions

View File

@@ -37,7 +37,7 @@ namespace Velopack.Sources
/// <summary> /// <summary>
/// The Bearer or other type of Authorization header used to authenticate against the Api. /// The Bearer or other type of Authorization header used to authenticate against the Api.
/// </summary> /// </summary>
protected abstract (string Name, string Value) Authorization { get; } protected abstract (string Name, string Value)? Authorization { get; }
/// <summary> /// <summary>
/// Base constructor. /// Base constructor.
@@ -51,17 +51,18 @@ namespace Velopack.Sources
} }
/// <inheritdoc /> /// <inheritdoc />
public virtual Task DownloadReleaseEntry(IVelopackLogger logger, VelopackAsset releaseEntry, string localFile, Action<int> progress, CancellationToken cancelToken) public virtual Task DownloadReleaseEntry(IVelopackLogger logger, VelopackAsset releaseEntry, string localFile, Action<int> progress,
CancellationToken cancelToken)
{ {
if (releaseEntry is GitBaseAsset githubEntry) { if (releaseEntry is GitBaseAsset githubEntry) {
// this might be a browser url or an api url (depending on whether we have a AccessToken or not) // this might be a browser url or an api url (depending on whether we have a AccessToken or not)
// https://docs.github.com/en/rest/reference/releases#get-a-release-asset // https://docs.github.com/en/rest/reference/releases#get-a-release-asset
var assetUrl = GetAssetUrlFromName(githubEntry.Release, releaseEntry.FileName); var assetUrl = GetAssetUrlFromName(githubEntry.Release, releaseEntry.FileName);
return Downloader.DownloadFile(assetUrl, localFile, progress, return Downloader.DownloadFile(
new Dictionary<string, string> { assetUrl,
[Authorization.Name] = Authorization.Value, localFile,
["Accept"] = "application/octet-stream" progress,
}, GetRequestHeaders("application/octet-stream"),
cancelToken: cancelToken); cancelToken: cancelToken);
} }
@@ -91,11 +92,10 @@ namespace Velopack.Sources
logger.Trace(ex.ToString()); logger.Trace(ex.ToString());
continue; continue;
} }
var releaseBytes = await Downloader.DownloadBytes(assetUrl,
new Dictionary<string, string> { var releaseBytes = await Downloader.DownloadBytes(
[Authorization.Name] = Authorization.Value, assetUrl,
["Accept"] = "application/octet-stream" GetRequestHeaders("application/octet-stream")
}
).ConfigureAwait(false); ).ConfigureAwait(false);
var txt = CoreUtil.RemoveByteOrderMarkerIfPresent(releaseBytes); var txt = CoreUtil.RemoveByteOrderMarkerIfPresent(releaseBytes);
var feed = VelopackAssetFeed.FromJson(txt); var feed = VelopackAssetFeed.FromJson(txt);
@@ -122,6 +122,24 @@ namespace Velopack.Sources
/// </summary> /// </summary>
protected abstract string GetAssetUrlFromName(T release, string assetName); protected abstract string GetAssetUrlFromName(T release, string assetName);
/// <summary>
/// Constructs a dictionary containing HTTP request headers.
/// </summary>
/// <param name="accept">The value for the "Accept" header; defaults to "application/json".</param>
/// <returns>A dictionary of headers including "Accept" and, if available, authorization headers.</returns>
protected virtual Dictionary<string, string> GetRequestHeaders(string accept = "application/json")
{
var headers = new Dictionary<string, string> {
["Accept"] = accept,
};
if (Authorization.HasValue) {
headers.Add(Authorization.Value.Name, Authorization.Value.Value);
}
return headers;
}
/// <summary> /// <summary>
/// Provides a wrapper around <see cref="VelopackAsset"/> which also contains a Git Release. /// Provides a wrapper around <see cref="VelopackAsset"/> which also contains a Git Release.
/// </summary> /// </summary>
@@ -146,4 +164,4 @@ namespace Velopack.Sources
} }
} }
} }
} }

View File

@@ -83,7 +83,8 @@ namespace Velopack.Sources
} }
/// <inheritdoc cref="Authorization"/> /// <inheritdoc cref="Authorization"/>
protected override (string Name, string Value) Authorization => ("Authorization", $"token {AccessToken}"); protected override (string Name, string Value)? Authorization =>
string.IsNullOrEmpty(AccessToken) ? null : ("Authorization", $"token {AccessToken}");
/// <inheritdoc /> /// <inheritdoc />
protected override async Task<GiteaRelease[]> GetReleases(bool includePrereleases) protected override async Task<GiteaRelease[]> GetReleases(bool includePrereleases)
@@ -96,14 +97,9 @@ namespace Velopack.Sources
var releasesPath = $"repos{RepoUri.AbsolutePath}/releases?limit={perPage}&page={page}&draft=false"; var releasesPath = $"repos{RepoUri.AbsolutePath}/releases?limit={perPage}&page={page}&draft=false";
var baseUri = GetApiBaseUrl(RepoUri); var baseUri = GetApiBaseUrl(RepoUri);
var getReleasesUri = new Uri(baseUri, releasesPath); var getReleasesUri = new Uri(baseUri, releasesPath);
var response = await Downloader.DownloadString(getReleasesUri.ToString(), var response = await Downloader.DownloadString(getReleasesUri.ToString(), GetRequestHeaders()).ConfigureAwait(false);
new Dictionary<string, string> {
[Authorization.Name] = Authorization.Value,
["Accept"] = "application/json"
}
).ConfigureAwait(false);
var releases = CompiledJson.DeserializeGiteaReleaseList(response); var releases = CompiledJson.DeserializeGiteaReleaseList(response);
if (releases == null) return new GiteaRelease[0]; if (releases == null) return Array.Empty<GiteaRelease>();
return releases.OrderByDescending(d => d.PublishedAt).Where(x => includePrereleases || !x.Prerelease).ToArray(); return releases.OrderByDescending(d => d.PublishedAt).Where(x => includePrereleases || !x.Prerelease).ToArray();
} }

View File

@@ -88,7 +88,8 @@ namespace Velopack.Sources
} }
/// <inheritdoc cref="Authorization"/> /// <inheritdoc cref="Authorization"/>
protected override (string Name, string Value) Authorization => ("Authorization", $"Bearer {AccessToken}"); protected override (string Name, string Value)? Authorization =>
string.IsNullOrEmpty(AccessToken) ? null : ("Authorization", $"Bearer {AccessToken}");
/// <inheritdoc /> /// <inheritdoc />
protected override async Task<GithubRelease[]> GetReleases(bool includePrereleases) protected override async Task<GithubRelease[]> GetReleases(bool includePrereleases)
@@ -99,11 +100,9 @@ namespace Velopack.Sources
var releasesPath = $"repos{RepoUri.AbsolutePath}/releases?per_page={perPage}&page={page}"; var releasesPath = $"repos{RepoUri.AbsolutePath}/releases?per_page={perPage}&page={page}";
var baseUri = GetApiBaseUrl(RepoUri); var baseUri = GetApiBaseUrl(RepoUri);
var getReleasesUri = new Uri(baseUri, releasesPath); var getReleasesUri = new Uri(baseUri, releasesPath);
var response = await Downloader.DownloadString(getReleasesUri.ToString(), var response = await Downloader.DownloadString(
new Dictionary<string, string> { getReleasesUri.ToString(),
[Authorization.Name] = Authorization.Value, GetRequestHeaders("application/vnd.github.v3+json")
["Accept"] = "application/vnd.github.v3+json"
}
).ConfigureAwait(false); ).ConfigureAwait(false);
var releases = CompiledJson.DeserializeGithubReleaseList(response); var releases = CompiledJson.DeserializeGithubReleaseList(response);
if (releases == null) return Array.Empty<GithubRelease>(); if (releases == null) return Array.Empty<GithubRelease>();
@@ -117,7 +116,8 @@ namespace Velopack.Sources
throw new ArgumentException($"No assets found in GitHub Release '{release.Name}'."); throw new ArgumentException($"No assets found in GitHub Release '{release.Name}'.");
} }
IEnumerable<GithubReleaseAsset> allReleasesFiles = release.Assets.Where(a => a.Name?.Equals(assetName, StringComparison.InvariantCultureIgnoreCase) == true); IEnumerable<GithubReleaseAsset> allReleasesFiles =
release.Assets.Where(a => a.Name?.Equals(assetName, StringComparison.InvariantCultureIgnoreCase) == true);
if (!allReleasesFiles.Any()) { if (!allReleasesFiles.Any()) {
throw new ArgumentException($"Could not find asset called '{assetName}' in GitHub Release '{release.Name}'."); throw new ArgumentException($"Could not find asset called '{assetName}' in GitHub Release '{release.Name}'.");
} }
@@ -157,8 +157,9 @@ namespace Velopack.Sources
// API location is http://internal.github.server.local/api/v3 // API location is http://internal.github.server.local/api/v3
baseAddress = new Uri(string.Format("{0}{1}{2}/api/v3/", repoUrl.Scheme, Uri.SchemeDelimiter, repoUrl.Host)); baseAddress = new Uri(string.Format("{0}{1}{2}/api/v3/", repoUrl.Scheme, Uri.SchemeDelimiter, repoUrl.Host));
} }
// above ^^ notice the end slashes for the baseAddress, explained here: http://stackoverflow.com/a/23438417/162694 // above ^^ notice the end slashes for the baseAddress, explained here: http://stackoverflow.com/a/23438417/162694
return baseAddress; return baseAddress;
} }
} }
} }

View File

@@ -100,7 +100,8 @@ namespace Velopack.Sources
public class GitlabSource : GitBase<GitlabRelease> public class GitlabSource : GitBase<GitlabRelease>
{ {
/// <inheritdoc cref="Authorization"/> /// <inheritdoc cref="Authorization"/>
protected override (string Name, string Value) Authorization => ("PRIVATE-TOKEN", AccessToken ?? string.Empty); protected override (string Name, string Value)? Authorization =>
string.IsNullOrEmpty(AccessToken) ? null : ("PRIVATE-TOKEN", AccessToken ?? string.Empty);
/// <inheritdoc cref="GitlabSource" /> /// <inheritdoc cref="GitlabSource" />
/// <param name="repoUrl"> /// <param name="repoUrl">
@@ -159,11 +160,7 @@ namespace Velopack.Sources
// https://docs.gitlab.com/ee/api/releases/ // https://docs.gitlab.com/ee/api/releases/
var releasesPath = $"releases?per_page={perPage}&page={page}"; var releasesPath = $"releases?per_page={perPage}&page={page}";
var getReleasesUri = CombineUri(RepoUri, releasesPath); var getReleasesUri = CombineUri(RepoUri, releasesPath);
var response = await Downloader.DownloadString(getReleasesUri.ToString(), var response = await Downloader.DownloadString(getReleasesUri.ToString(), GetRequestHeaders()).ConfigureAwait(false);
new Dictionary<string, string> {
[Authorization.Name] = Authorization.Value,
["Accept"] = "application/json"
}).ConfigureAwait(false);
var releases = CompiledJson.DeserializeGitlabReleaseList(response); var releases = CompiledJson.DeserializeGitlabReleaseList(response);
if (releases == null) return Array.Empty<GitlabRelease>(); if (releases == null) return Array.Empty<GitlabRelease>();
return releases.OrderByDescending(d => d.ReleasedAt).Where(x => includePrereleases || !x.UpcomingRelease).ToArray(); return releases.OrderByDescending(d => d.ReleasedAt).Where(x => includePrereleases || !x.UpcomingRelease).ToArray();

View File

@@ -20,6 +20,26 @@ public class GithubDeploymentTests
_output = output; _output = output;
} }
[Fact(Skip = "Need to create a repo to test with")]
public async Task TestUnauthenticatedDownload()
{
using var logger = _output.BuildLoggerFor<GithubDeploymentTests>();
using var _1 = TempUtil.GetTempDirectory(out var releaseDir);
var repo = new GitHubRepository(logger);
var options = new GitHubDownloadOptions {
TargetOs = RuntimeOs.Linux,
Channel = "linux-x64",
ReleaseDir = new DirectoryInfo(releaseDir),
Timeout = 60,
Prerelease = false,
RepoUrl = "TODO",
Token = null,
};
await repo.DownloadLatestFullPackageAsync(options);
}
[SkippableFact] [SkippableFact]
public void WillRefuseToUploadMultipleWithoutMergeArg() public void WillRefuseToUploadMultipleWithoutMergeArg()
{ {