Sign avatar download URLs against a browser-reachable endpoint

This commit is contained in:
Aaron Po
2026-10-11 22:20:25 -04:00
parent dfe248ea5f
commit 9afab64f67
10 changed files with 55 additions and 4 deletions

View File

@@ -23,6 +23,14 @@ public static class ConfigurationKeys
public const string SmtpFromName = "SMTP_FROM_NAME";
public const string SeaweedFsServiceUrl = "SEAWEEDFS_SERVICE_URL";
/// <summary>
/// The browser-reachable base URL of the object store, used to sign download URLs. Falls back to
/// <see cref="SeaweedFsServiceUrl" /> when unset: SigV4 covers the host, so a URL signed against
/// the container-internal endpoint is only usable by callers that can resolve that host.
/// </summary>
public const string SeaweedFsPublicUrl = "SEAWEEDFS_PUBLIC_URL";
public const string SeaweedFsAccessKeyId = "SEAWEEDFS_ACCESS_KEY_ID";
public const string SeaweedFsSecretAccessKey = "SEAWEEDFS_SECRET_ACCESS_KEY";
public const string SeaweedFsBucket = "SEAWEEDFS_BUCKET";

View File

@@ -13,11 +13,13 @@ public class S3FileStorageProvider : IFileStorageProvider, IDisposable
{
private readonly string _bucket;
private readonly IAmazonS3 _client;
private readonly IAmazonS3 _presignClient;
/// <summary>
/// Initializes a new instance of the <see cref="S3FileStorageProvider" /> class, reading storage
/// configuration (<c>SEAWEEDFS_SERVICE_URL</c>, <c>SEAWEEDFS_ACCESS_KEY_ID</c>,
/// <c>SEAWEEDFS_SECRET_ACCESS_KEY</c>, <c>SEAWEEDFS_BUCKET</c>) from <paramref name="configuration" />.
/// <c>SEAWEEDFS_SECRET_ACCESS_KEY</c>, <c>SEAWEEDFS_BUCKET</c>, and the optional
/// <c>SEAWEEDFS_PUBLIC_URL</c>) from <paramref name="configuration" />.
/// </summary>
/// <exception cref="InvalidOperationException">
/// Thrown when any of the required configuration values is not set.
@@ -45,6 +47,23 @@ public class S3FileStorageProvider : IFileStorageProvider, IDisposable
AmazonS3Config config = new() { ServiceURL = serviceUrl, ForcePathStyle = true };
_client = new AmazonS3Client(accessKeyId, secretAccessKey, config);
// A presigned URL is signed over its host, so one signed against the container-internal
// endpoint is only usable from inside the network. Sign downloads against the public
// endpoint instead, so a browser can fetch the object.
string? configuredPublicUrl = configuration[ConfigurationKeys.SeaweedFsPublicUrl];
string publicUrl = string.IsNullOrWhiteSpace(configuredPublicUrl)
? serviceUrl
: configuredPublicUrl;
_presignClient =
publicUrl == serviceUrl
? _client
: new AmazonS3Client(
accessKeyId,
secretAccessKey,
new AmazonS3Config { ServiceURL = publicUrl, ForcePathStyle = true }
);
}
/// <inheritdoc/>
@@ -84,12 +103,15 @@ public class S3FileStorageProvider : IFileStorageProvider, IDisposable
Expires = DateTime.UtcNow.Add(expiresIn),
};
return _client.GetPreSignedURL(request);
return _presignClient.GetPreSignedURL(request);
}
/// <inheritdoc/>
public void Dispose()
{
if (!ReferenceEquals(_presignClient, _client))
_presignClient.Dispose();
_client.Dispose();
GC.SuppressFinalize(this);
}