diff --git a/src/lib-csharp/Sources/GitBase.cs b/src/lib-csharp/Sources/GitBase.cs index 5641dc61..6f701422 100644 --- a/src/lib-csharp/Sources/GitBase.cs +++ b/src/lib-csharp/Sources/GitBase.cs @@ -35,12 +35,14 @@ namespace Velopack.Sources protected virtual string? AccessToken { get; } /// - /// The Bearer token used in the request. + /// The Bearer or other type of Authorization header used to authenticate against the Api. /// - protected virtual string? Authorization => string.IsNullOrWhiteSpace(AccessToken) ? null : "Bearer " + AccessToken; + protected abstract (string Name, string Value) Authorization { get; } - /// - public GitBase(string repoUrl, string? accessToken, bool prerelease, IFileDownloader? downloader = null) + /// + /// Base constructor. + /// + protected GitBase(string repoUrl, string? accessToken, bool prerelease, IFileDownloader? downloader = null) { RepoUri = new Uri(repoUrl.TrimEnd('/')); AccessToken = accessToken; @@ -55,7 +57,12 @@ namespace Velopack.Sources // 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 var assetUrl = GetAssetUrlFromName(githubEntry.Release, releaseEntry.FileName); - return Downloader.DownloadFile(assetUrl, localFile, progress, Authorization, "application/octet-stream", cancelToken: cancelToken); + return Downloader.DownloadFile(assetUrl, localFile, progress, + new Dictionary { + [Authorization.Name] = Authorization.Value, + ["Accept"] = "application/octet-stream" + }, + cancelToken: cancelToken); } throw new ArgumentException($"Expected releaseEntry to be {nameof(GitBaseAsset)} but got {releaseEntry.GetType().Name}."); @@ -84,7 +91,12 @@ namespace Velopack.Sources logger.Trace(ex.ToString()); continue; } - var releaseBytes = await Downloader.DownloadBytes(assetUrl, Authorization, "application/octet-stream").ConfigureAwait(false); + var releaseBytes = await Downloader.DownloadBytes(assetUrl, + new Dictionary { + [Authorization.Name] = Authorization.Value, + ["Accept"] = "application/octet-stream" + } + ).ConfigureAwait(false); var txt = CoreUtil.RemoveByteOrderMarkerIfPresent(releaseBytes); var feed = VelopackAssetFeed.FromJson(txt); foreach (var f in feed.Assets) { diff --git a/src/lib-csharp/Sources/GiteaSource.cs b/src/lib-csharp/Sources/GiteaSource.cs index 720ea1f9..c519864d 100644 --- a/src/lib-csharp/Sources/GiteaSource.cs +++ b/src/lib-csharp/Sources/GiteaSource.cs @@ -81,11 +81,10 @@ namespace Velopack.Sources : base(repoUrl, accessToken, prerelease, downloader) { } - /// - /// The authorization token used in the request. - /// Overwrite it to token - /// - protected override string? Authorization => string.IsNullOrWhiteSpace(AccessToken) ? null : "token " + AccessToken; + + /// + protected override (string Name, string Value) Authorization => ("Authorization", $"token {AccessToken}"); + /// protected override async Task GetReleases(bool includePrereleases) { @@ -97,7 +96,12 @@ namespace Velopack.Sources var releasesPath = $"repos{RepoUri.AbsolutePath}/releases?limit={perPage}&page={page}&draft=false"; var baseUri = GetApiBaseUrl(RepoUri); var getReleasesUri = new Uri(baseUri, releasesPath); - var response = await Downloader.DownloadString(getReleasesUri.ToString(), Authorization, "application/json").ConfigureAwait(false); + var response = await Downloader.DownloadString(getReleasesUri.ToString(), + new Dictionary { + [Authorization.Name] = Authorization.Value, + ["Accept"] = "application/json" + } + ).ConfigureAwait(false); var releases = CompiledJson.DeserializeGiteaReleaseList(response); if (releases == null) return new GiteaRelease[0]; return releases.OrderByDescending(d => d.PublishedAt).Where(x => includePrereleases || !x.Prerelease).ToArray(); diff --git a/src/lib-csharp/Sources/GithubSource.cs b/src/lib-csharp/Sources/GithubSource.cs index 611d9063..1cb5942b 100644 --- a/src/lib-csharp/Sources/GithubSource.cs +++ b/src/lib-csharp/Sources/GithubSource.cs @@ -87,6 +87,9 @@ namespace Velopack.Sources { } + /// + protected override (string Name, string Value) Authorization => ("Authorization", $"Bearer {AccessToken}"); + /// protected override async Task GetReleases(bool includePrereleases) { @@ -96,7 +99,12 @@ namespace Velopack.Sources var releasesPath = $"repos{RepoUri.AbsolutePath}/releases?per_page={perPage}&page={page}"; var baseUri = GetApiBaseUrl(RepoUri); var getReleasesUri = new Uri(baseUri, releasesPath); - var response = await Downloader.DownloadString(getReleasesUri.ToString(), Authorization, "application/vnd.github.v3+json").ConfigureAwait(false); + var response = await Downloader.DownloadString(getReleasesUri.ToString(), + new Dictionary { + [Authorization.Name] = Authorization.Value, + ["Accept"] = "application/vnd.github.v3+json" + } + ).ConfigureAwait(false); var releases = CompiledJson.DeserializeGithubReleaseList(response); if (releases == null) return Array.Empty(); return releases.OrderByDescending(d => d.PublishedAt).Where(x => includePrereleases || !x.Prerelease).ToArray(); diff --git a/src/lib-csharp/Sources/GitlabSource.cs b/src/lib-csharp/Sources/GitlabSource.cs index a51b469a..6c70fea5 100644 --- a/src/lib-csharp/Sources/GitlabSource.cs +++ b/src/lib-csharp/Sources/GitlabSource.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Velopack.Util; @@ -98,6 +99,9 @@ namespace Velopack.Sources /// public class GitlabSource : GitBase { + /// + protected override (string Name, string Value) Authorization => ("PRIVATE-TOKEN", AccessToken ?? string.Empty); + /// /// /// The URL of the GitLab repository to download releases from @@ -156,7 +160,11 @@ namespace Velopack.Sources var releasesPath = $"{RepoUri.AbsolutePath}/releases?per_page={perPage}&page={page}"; var baseUri = new Uri("https://gitlab.com"); var getReleasesUri = new Uri(baseUri, releasesPath); - var response = await Downloader.DownloadString(getReleasesUri.ToString(), Authorization).ConfigureAwait(false); + var response = await Downloader.DownloadString(getReleasesUri.ToString(), + new Dictionary { + [Authorization.Name] = Authorization.Value, + ["Accept"] = "application/json" + }).ConfigureAwait(false); var releases = CompiledJson.DeserializeGitlabReleaseList(response); if (releases == null) return new GitlabRelease[0]; return releases.OrderByDescending(d => d.ReleasedAt).Where(x => includePrereleases || !x.UpcomingRelease).ToArray(); diff --git a/src/lib-csharp/Sources/HttpClientFileDownloader.cs b/src/lib-csharp/Sources/HttpClientFileDownloader.cs index c1e1dd30..183b66cd 100644 --- a/src/lib-csharp/Sources/HttpClientFileDownloader.cs +++ b/src/lib-csharp/Sources/HttpClientFileDownloader.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Http; @@ -18,9 +19,9 @@ namespace Velopack.Sources public static ProductInfoHeaderValue UserAgent => new("Velopack", VelopackRuntimeInfo.VelopackNugetVersion.ToFullString()); /// - public virtual async Task DownloadFile(string url, string targetFile, Action progress, string? authorization, string? accept, double timeout, CancellationToken cancelToken = default) + public virtual async Task DownloadFile(string url, string targetFile, Action progress, IDictionary? headers, double timeout, CancellationToken cancelToken = default) { - using var client = CreateHttpClient(authorization, accept, timeout); + using var client = CreateHttpClient(headers, timeout); try { using (var fs = File.Open(targetFile, FileMode.Create)) { @@ -36,9 +37,9 @@ namespace Velopack.Sources } /// - public virtual async Task DownloadBytes(string url, string? authorization, string? accept, double timeout) + public virtual async Task DownloadBytes(string url, IDictionary? headers, double timeout) { - using var client = CreateHttpClient(authorization, accept, timeout); + using var client = CreateHttpClient(headers, timeout); try { return await client.GetByteArrayAsync(url).ConfigureAwait(false); @@ -50,9 +51,9 @@ namespace Velopack.Sources } /// - public virtual async Task DownloadString(string url, string? authorization, string? accept, double timeout) + public virtual async Task DownloadString(string url, IDictionary? headers, double timeout) { - using var client = CreateHttpClient(authorization, accept, timeout); + using var client = CreateHttpClient(headers, timeout); try { return await client.GetStringAsync(url).ConfigureAwait(false); @@ -123,16 +124,15 @@ namespace Velopack.Sources /// /// Creates a new for every request. /// - protected virtual HttpClient CreateHttpClient(string? authorization, string? accept, double timeout = 30) + protected virtual HttpClient CreateHttpClient(IDictionary? headers, double timeout) { var client = new HttpClient(CreateHttpClientHandler()); client.DefaultRequestHeaders.UserAgent.Add(UserAgent); - if (authorization != null) - client.DefaultRequestHeaders.Add("Authorization", authorization); - - if (accept != null) - client.DefaultRequestHeaders.Add("Accept", accept); + foreach (var header in headers ?? new Dictionary()) + { + client.DefaultRequestHeaders.Add(header.Key, header.Value); + } client.Timeout = TimeSpan.FromMinutes(timeout); return client; diff --git a/src/lib-csharp/Sources/IFileDownloader.cs b/src/lib-csharp/Sources/IFileDownloader.cs index e03ca440..67343c46 100644 --- a/src/lib-csharp/Sources/IFileDownloader.cs +++ b/src/lib-csharp/Sources/IFileDownloader.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -14,31 +15,26 @@ namespace Velopack.Sources /// /// The url which will be downloaded. /// - /// The local path where the file will be stored - /// If a file exists at this path, it will be overwritten. + /// The local path where the file will be stored + /// If a file exists at this path, it will be overwritten. /// - /// A delegate for reporting download progress, with expected values from 0-100. - /// - /// - /// Text to be sent in the 'Authorization' header of the request. - /// - /// - /// Text to be sent in the 'Accept' header of the request. + /// A delegate for reporting download progress, with expected values from 0-100. /// + /// Headers that can be passed to Http Downloader, e.g. Accept or Authorization. /// - /// The maximum time in minutes to wait for the download to complete. + /// The maximum time in minutes to wait for the download to complete. /// /// Optional token to cancel the request. - Task DownloadFile(string url, string targetFile, Action progress, string? authorization = null, string? accept = null, double timeout = 30, CancellationToken cancelToken = default); + Task DownloadFile(string url, string targetFile, Action progress, IDictionary? headers = null, double timeout = 30, CancellationToken cancelToken = default); /// /// Returns a byte array containing the contents of the file at the specified url /// - Task DownloadBytes(string url, string? authorization = null, string? accept = null, double timeout = 30); + Task DownloadBytes(string url, IDictionary? headers = null, double timeout = 30); /// /// Returns a string containing the contents of the specified url /// - Task DownloadString(string url, string? authorization = null, string? accept = null, double timeout = 30); + Task DownloadString(string url, IDictionary? headers = null, double timeout = 30); } } diff --git a/test/Velopack.Tests/TestHelpers/FakeDownloader.cs b/test/Velopack.Tests/TestHelpers/FakeDownloader.cs index fc089845..c929d8f6 100644 --- a/test/Velopack.Tests/TestHelpers/FakeDownloader.cs +++ b/test/Velopack.Tests/TestHelpers/FakeDownloader.cs @@ -7,23 +7,21 @@ public class FakeDownloader : IFileDownloader { public string LastUrl { get; private set; } public string LastLocalFile { get; private set; } - public string LastAuthHeader { get; private set; } - public string LastAcceptHeader { get; private set; } + public IDictionary? LastHeaders { get; private set; } public byte[] MockedResponseBytes { get; set; } = []; public bool WriteMockLocalFile { get; set; } = false; - public Task DownloadBytes(string url, string auth, string acc, double timeout = 30) + public Task DownloadBytes(string url, IDictionary headers, double timeout = 30) { LastUrl = url; - LastAuthHeader = auth; - LastAcceptHeader = acc; + LastHeaders = headers; return Task.FromResult(MockedResponseBytes); } - public async Task DownloadFile(string url, string targetFile, Action progress, string auth, string acc, double timeout, CancellationToken token) + public async Task DownloadFile(string url, string targetFile, Action progress, IDictionary headers, double timeout = 30, CancellationToken token = default) { LastLocalFile = targetFile; - var resp = await DownloadBytes(url, auth, acc); + var resp = await DownloadBytes(url, headers); progress?.Invoke(25); progress?.Invoke(50); progress?.Invoke(75); @@ -32,8 +30,8 @@ public class FakeDownloader : IFileDownloader File.WriteAllBytes(targetFile, resp); } - public async Task DownloadString(string url, string auth, string acc, double timeout = 30) + public async Task DownloadString(string url, IDictionary headers, double timeout = 30) { - return Encoding.UTF8.GetString(await DownloadBytes(url, auth, acc)); + return Encoding.UTF8.GetString(await DownloadBytes(url, headers)); } } diff --git a/test/Velopack.Tests/TestHelpers/FakeFixtureRepository.cs b/test/Velopack.Tests/TestHelpers/FakeFixtureRepository.cs index 786a3f80..8577bf6c 100644 --- a/test/Velopack.Tests/TestHelpers/FakeFixtureRepository.cs +++ b/test/Velopack.Tests/TestHelpers/FakeFixtureRepository.cs @@ -57,7 +57,7 @@ internal class FakeFixtureRepository : IFileDownloader _releases = releases; } - public Task DownloadBytes(string url, string authorization = null, string accept = null, double timeout = 30) + public Task DownloadBytes(string url, IDictionary headers = null, double timeout = 30) { if (url.Contains($"/{_releasesName}?")) { MemoryStream ms = new MemoryStream(); @@ -82,7 +82,7 @@ internal class FakeFixtureRepository : IFileDownloader return Task.FromResult(File.ReadAllBytes(filePath)); } - public Task DownloadFile(string url, string targetFile, Action progress, string authorization = null, string accept = null, double timeout = 30, + public Task DownloadFile(string url, string targetFile, Action progress, IDictionary headers = null, double timeout = 30, CancellationToken token = default) { var rel = _releases.FirstOrDefault(r => url.EndsWith(r.OriginalFilename)); @@ -99,7 +99,7 @@ internal class FakeFixtureRepository : IFileDownloader return Task.CompletedTask; } - public Task DownloadString(string url, string authorization = null, string accept = null, double timeout = 30) + public Task DownloadString(string url, IDictionary headers = null, double timeout = 30) { if (url.Contains($"/{_releasesName}?")) { MemoryStream ms = new MemoryStream();