Handle local loopback plugin downloads
Add an explicit local artifact download proxy flag for development and tests. Keep the public download endpoint as a redirect while routing enabled loopback artifacts through the internal proxy.
This commit is contained in:
parent
645f28c7d8
commit
04a64d9178
@ -45,6 +45,8 @@ public class ServerTester : IAsyncDisposable
|
||||
public WebApplication WebApp => _WebApp ?? throw new InvalidOperationException("Webapp not initialized");
|
||||
|
||||
public bool ReuseDatabase { get; set; } = true;
|
||||
public bool CheatMode { get; set; }
|
||||
public bool EnableLocalArtifactDownloadProxy { get; set; }
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
@ -108,6 +110,8 @@ public class ServerTester : IAsyncDisposable
|
||||
"--urls=http://127.0.0.1:0",
|
||||
$"--postgres={connStr}",
|
||||
$"--storage_connection_string={StorageConnectionString}",
|
||||
$"--cheat_mode={CheatMode.ToString().ToLowerInvariant()}",
|
||||
$"--enable_local_artifact_download_proxy={EnableLocalArtifactDownloadProxy.ToString().ToLowerInvariant()}",
|
||||
]
|
||||
});
|
||||
|
||||
|
||||
@ -167,4 +167,55 @@ public class UnitTest1 : UnitTestBase
|
||||
res = await client.GetPublishedVersions("2.1.0.0", false, searchPluginName: "rockstar");
|
||||
Assert.DoesNotContain(res, p => p.ProjectSlug == "rockstar-stylist");
|
||||
}
|
||||
[Fact]
|
||||
public async Task DownloadEndpoint_UsesInternalLoopbackRedirectWhenLocalArtifactProxyEnabled()
|
||||
{
|
||||
await using var tester = Create();
|
||||
tester.ReuseDatabase = false;
|
||||
tester.EnableLocalArtifactDownloadProxy = true;
|
||||
await tester.Start();
|
||||
|
||||
var ownerId = await tester.CreateFakeUserAsync();
|
||||
await tester.CreateAndBuildPluginAsync(ownerId);
|
||||
|
||||
using var client = new HttpClient(new HttpClientHandler { AllowAutoRedirect = false });
|
||||
client.BaseAddress = new Uri(tester.WebApp.Urls.First(), UriKind.Absolute);
|
||||
|
||||
using var response = await client.GetAsync("api/v1/plugins/rockstar-stylist/versions/1.0.2.0/download");
|
||||
|
||||
Assert.Equal(System.Net.HttpStatusCode.Found, response.StatusCode);
|
||||
Assert.NotNull(response.Headers.Location);
|
||||
Assert.Equal(
|
||||
"/api/v1/plugins/rockstar-stylist/versions/1.0.2.0/download-loopback",
|
||||
response.Headers.Location!.OriginalString);
|
||||
|
||||
using var proxiedResponse = await client.GetAsync(response.Headers.Location);
|
||||
|
||||
Assert.Equal(System.Net.HttpStatusCode.OK, proxiedResponse.StatusCode);
|
||||
Assert.Equal("application/zip", proxiedResponse.Content.Headers.ContentType?.MediaType);
|
||||
Assert.True((await proxiedResponse.Content.ReadAsByteArrayAsync()).Length > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DownloadEndpoint_DoesNotUseInternalLoopbackRedirectWhenLocalArtifactProxyDisabled()
|
||||
{
|
||||
await using var tester = Create();
|
||||
tester.ReuseDatabase = false;
|
||||
await tester.Start();
|
||||
|
||||
var ownerId = await tester.CreateFakeUserAsync();
|
||||
await tester.CreateAndBuildPluginAsync(ownerId);
|
||||
|
||||
using var client = new HttpClient(new HttpClientHandler { AllowAutoRedirect = false });
|
||||
client.BaseAddress = new Uri(tester.WebApp.Urls.First(), UriKind.Absolute);
|
||||
|
||||
using var response = await client.GetAsync("api/v1/plugins/rockstar-stylist/versions/1.0.2.0/download");
|
||||
|
||||
Assert.Equal(System.Net.HttpStatusCode.Found, response.StatusCode);
|
||||
Assert.NotNull(response.Headers.Location);
|
||||
Assert.DoesNotContain("download-loopback", response.Headers.Location!.OriginalString, StringComparison.Ordinal);
|
||||
Assert.True(response.Headers.Location.IsAbsoluteUri);
|
||||
Assert.True(response.Headers.Location.IsLoopback);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -26,7 +26,9 @@ public class ApiController(
|
||||
BuildService buildService,
|
||||
VersionLifecycleService versionLifecycleService,
|
||||
UserManager<IdentityUser> userManager,
|
||||
UserVerifiedLogic userVerifiedLogic)
|
||||
UserVerifiedLogic userVerifiedLogic,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
ServerEnvironment serverEnvironment)
|
||||
: ControllerBase
|
||||
{
|
||||
private sealed class BuildRow
|
||||
@ -241,17 +243,57 @@ public class ApiController(
|
||||
[ModelBinder(typeof(PluginVersionModelBinder))]
|
||||
PluginVersion version)
|
||||
{
|
||||
var url = await GetArtifactUrl(pluginSlug, version);
|
||||
if (url is null)
|
||||
return NotFound();
|
||||
|
||||
await using var conn = await connectionFactory.Open();
|
||||
var url = await conn.ExecuteScalarAsync<string?>(
|
||||
await conn.InsertEvent("Download", new JObject { ["pluginSlug"] = pluginSlug.ToString(), ["version"] = version.ToString() });
|
||||
if (serverEnvironment.EnableLocalArtifactDownloadProxy && Uri.TryCreate(url, UriKind.Absolute, out var artifactUri) && artifactUri.IsLoopback)
|
||||
{
|
||||
return RedirectToAction(
|
||||
nameof(DownloadLoopbackArtifact),
|
||||
new { pluginSlug = pluginSlug.ToString(), version = version.ToString() });
|
||||
}
|
||||
|
||||
return Redirect(url);
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
[HttpGet("plugins/{pluginSlug}/versions/{version}/download-loopback")]
|
||||
[EnableRateLimiting(Policies.PublicApiRateLimit)]
|
||||
public async Task<IActionResult> DownloadLoopbackArtifact(
|
||||
[ModelBinder(typeof(PluginSlugModelBinder))]
|
||||
PluginSlug pluginSlug,
|
||||
[ModelBinder(typeof(PluginVersionModelBinder))]
|
||||
PluginVersion version)
|
||||
{
|
||||
if (!serverEnvironment.EnableLocalArtifactDownloadProxy)
|
||||
return NotFound();
|
||||
|
||||
var url = await GetArtifactUrl(pluginSlug, version);
|
||||
if (url is null || !Uri.TryCreate(url, UriKind.Absolute, out var artifactUri) || !artifactUri.IsLoopback)
|
||||
return NotFound();
|
||||
|
||||
using var response = await httpClientFactory.CreateClient().GetAsync(artifactUri, HttpContext.RequestAborted);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return StatusCode((int)response.StatusCode);
|
||||
|
||||
var package = await response.Content.ReadAsByteArrayAsync(HttpContext.RequestAborted);
|
||||
var contentType = response.Content.Headers.ContentType?.ToString() ?? "application/zip";
|
||||
var fileName = Path.GetFileName(artifactUri.LocalPath);
|
||||
return File(package, contentType, fileName);
|
||||
}
|
||||
|
||||
private async Task<string?> GetArtifactUrl(PluginSlug pluginSlug, PluginVersion version)
|
||||
{
|
||||
await using var conn = await connectionFactory.Open();
|
||||
return await conn.ExecuteScalarAsync<string?>(
|
||||
"SELECT b.build_info->>'url' FROM versions v " +
|
||||
"JOIN builds b ON b.plugin_slug = v.plugin_slug AND b.id = v.build_id " +
|
||||
"WHERE v.plugin_slug=@plugin_slug AND v.ver=@version",
|
||||
new { plugin_slug = pluginSlug.ToString(), version = version.VersionParts });
|
||||
if (url is null)
|
||||
return NotFound();
|
||||
|
||||
await conn.InsertEvent("Download", new JObject { ["pluginSlug"] = pluginSlug.ToString(), ["version"] = version.ToString() });
|
||||
return Redirect(url);
|
||||
}
|
||||
|
||||
[HttpPost("plugins/{pluginSlug}/builds")]
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
"PB_POSTGRES": "User ID=postgres;Include Error Detail=true;Host=127.0.0.1;Port=61932;Database=btcpayplugin",
|
||||
"PB_STORAGE_CONNECTION_STRING": "BlobEndpoint=http://127.0.0.1:32827/satoshi;AccountName=satoshi;AccountKey=Rxb41pUHRe+ibX5XS311tjXpjvu7mVi2xYJvtmq1j2jlUpN+fY/gkzyBMjqwzgj42geXGdYSbPEcu5i5wjSjPw==",
|
||||
"PB_CHEAT_MODE": "true",
|
||||
"PB_ENABLE_LOCAL_ARTIFACT_DOWNLOAD_PROXY": "true",
|
||||
"PB_DEBUGLOG": "debug.log"
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,7 +5,9 @@ public class ServerEnvironment
|
||||
public ServerEnvironment(IConfiguration configuration)
|
||||
{
|
||||
CheatMode = configuration.GetValue<bool?>("CHEAT_MODE") ?? false;
|
||||
EnableLocalArtifactDownloadProxy = configuration.GetValue<bool?>("ENABLE_LOCAL_ARTIFACT_DOWNLOAD_PROXY") ?? false;
|
||||
}
|
||||
|
||||
public bool CheatMode { get; set; }
|
||||
public bool EnableLocalArtifactDownloadProxy { get; set; }
|
||||
}
|
||||
|
||||
@ -15,6 +15,7 @@ All parameters are configured via environment variables.
|
||||
* `PB_POSTGRES`: Connection to a postgres database (example: `User ID=postgres;Include Error Detail=true;Host=127.0.0.1;Port=61932;Database=btcpayplugin`)
|
||||
* `PB_STORAGE_CONNECTION_STRING`: Connection string to azure storage to store build results (example: `BlobEndpoint=http://127.0.0.1:32827/satoshi;AccountName=satoshi;AccountKey=Rxb41pUHRe+ibX5XS311tjXpjvu7mVi2xYJvtmq1j2jlUpN+fY/gkzyBMjqwzgj42geXGdYSbPEcu5i5wjSjPw==`)
|
||||
* `PB_CHEAT_MODE`: If set to `true`, it's considered that the server is running in a development environment and will allow to bypass some security checks (right now only registering admin account).
|
||||
* `PB_ENABLE_LOCAL_ARTIFACT_DOWNLOAD_PROXY`: If set to `true`, loopback artifact URLs can be proxied through the API download endpoint for local development.
|
||||
* `ASPNETCORE_URLS`: The url the web server will be listening (example: `http://127.0.0.1:8080`)
|
||||
* `PB_DATADIR`: Where some persistent data get saved (example: `/datadir`)
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user