Skip to content

Implement Presigned Post URLs #3902

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 6 commits into
base: gcbeatty/presignedpostbcl
Choose a base branch
from
Draft
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
246 changes: 246 additions & 0 deletions sdk/src/Services/S3/Custom/AmazonS3Client.Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,22 @@
* permissions and limitations under the License.
*/
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Net;
using System.Text;
using System.Text.Json;

#if AWS_ASYNC_API
using System.Threading.Tasks;
#endif

#if !NETFRAMEWORK
using ThirdParty.RuntimeBackports;
#endif

using Amazon.Runtime;
using Amazon.Runtime.Internal;
using Amazon.Runtime.Internal.Auth;
Expand Down Expand Up @@ -570,6 +577,245 @@ public async Task<string> GetPreSignedURLAsync(GetPreSignedUrlRequest request)
#endif
#endregion

#region CreatePresignedPost

/// <summary>
/// Create a presigned POST request that can be used to upload a file directly to S3 from a web browser.
/// </summary>
/// <param name="request">The CreatePresignedPostRequest that defines the parameters of the operation.</param>
/// <returns>A CreatePresignedPostResponse containing the URL and form fields for the POST request.</returns>
/// <exception cref="T:System.ArgumentException" />
/// <exception cref="T:System.ArgumentNullException" />
public CreatePresignedPostResponse CreatePresignedPost(CreatePresignedPostRequest request)
{
return CreatePresignedPostInternal(request);
}

#if AWS_ASYNC_API
/// <summary>
/// Asynchronously create a presigned POST request that can be used to upload a file directly to S3 from a web browser.
/// </summary>
/// <param name="request">The CreatePresignedPostRequest that defines the parameters of the operation.</param>
/// <returns>A CreatePresignedPostResponse containing the URL and form fields for the POST request.</returns>
/// <exception cref="T:System.ArgumentException" />
/// <exception cref="T:System.ArgumentNullException" />
public async Task<CreatePresignedPostResponse> CreatePresignedPostAsync(CreatePresignedPostRequest request)
{
return await CreatePresignedPostInternalAsync(request).ConfigureAwait(false);
}
#endif

/// <summary>
/// Validates the CreatePresignedPostRequest parameters.
/// </summary>
/// <param name="request">The request to validate.</param>
/// <exception cref="T:System.ArgumentException" />
/// <exception cref="T:System.ArgumentNullException" />
private static void ValidateCreatePresignedPostRequest(CreatePresignedPostRequest request)
{
if (request == null)
throw new ArgumentNullException(nameof(request), "The CreatePresignedPostRequest specified is null!");

if (string.IsNullOrEmpty(request.BucketName))
throw new ArgumentException("BucketName is required", nameof(request));

if (!request.Expires.HasValue)
throw new ArgumentException("Expires is required", nameof(request));

// Check for access point ARNs and reject them - S3 presigned POST doesn't support access points
if (Arn.TryParse(request.BucketName, out var arn))
{
throw new AmazonS3Exception("S3 presigned POST does not support access points or multi-region access points. " +
"Use the underlying bucket name instead, or consider using presigned PUT URLs as an alternative.");
}
}

/// <summary>
/// Creates and processes the internal request for endpoint resolution.
/// </summary>
/// <param name="request">The CreatePresignedPostRequest.</param>
/// <returns>The processed IRequest object.</returns>
private IRequest CreateAndProcessRequest(CreatePresignedPostRequest request)
{
// Marshall the request to create a proper IRequest object
var irequest = MarshallCreatePresignedPost(request);

// Use the same endpoint resolution pipeline as GetPreSignedURL
var context = new Amazon.Runtime.Internal.ExecutionContext(
new RequestContext(true, new NullSigner())
{
Request = irequest,
ClientConfig = this.Config,
OriginalRequest = request,
},
null
);
new AmazonS3EndpointResolver().ProcessRequestHandlers(context);

return irequest;
}

/// <summary>
/// Builds the CreatePresignedPostResponse with URL and form fields.
/// </summary>
/// <param name="request">The CreatePresignedPostRequest.</param>
/// <param name="irequest">The processed IRequest object.</param>
/// <param name="credentials">The AWS credentials.</param>
/// <returns>A CreatePresignedPostResponse containing the URL and form fields for the POST request.</returns>
private CreatePresignedPostResponse BuildPresignedPostResponse(CreatePresignedPostRequest request, IRequest irequest, AWSCredentials credentials)
{
// Build the policy document
var policyDocument = BuildPolicyDocument(request);

// Use S3PostUploadSignedPolicy to sign the policy
var signedPolicy = S3PostUploadSignedPolicy.GetSignedPolicy(policyDocument, credentials, Config.RegionEndpoint);

// Build the response
var response = new CreatePresignedPostResponse();

// Determine the endpoint URL using the processed request context
var parameters = new ServiceOperationEndpointParameters(irequest.OriginalRequest);
var endpoint = Config.DetermineServiceOperationEndpoint(parameters);
response.Url = $"{endpoint.URL}/{request.BucketName}";

// Add all the required form fields
response.Fields = new Dictionary<string, string>(request.Fields);

// Add the AWS signature fields
response.Fields[S3Constants.PostFormDataObjectKey] = request.Key ?? "";
response.Fields[S3Constants.PostFormDataPolicy] = signedPolicy.Policy;
response.Fields[S3Constants.PostFormDataXAmzCredential] = signedPolicy.Credential;
response.Fields[S3Constants.PostFormDataXAmzAlgorithm] = signedPolicy.Algorithm;
response.Fields[S3Constants.PostFormDataXAmzDate] = signedPolicy.Date;
response.Fields[S3Constants.PostFormDataXAmzSignature] = signedPolicy.Signature;

if (!string.IsNullOrEmpty(signedPolicy.SecurityToken))
{
response.Fields[S3Constants.PostFormDataSecurityToken] = signedPolicy.SecurityToken;
}

return response;
}

/// <summary>
/// Internal implementation for creating presigned POST requests.
/// </summary>
/// <param name="request">The CreatePresignedPostRequest that defines the parameters of the operation.</param>
/// <returns>A CreatePresignedPostResponse containing the URL and form fields for the POST request.</returns>
/// <exception cref="T:System.ArgumentException" />
/// <exception cref="T:System.ArgumentNullException" />
internal CreatePresignedPostResponse CreatePresignedPostInternal(CreatePresignedPostRequest request)
{
ValidateCreatePresignedPostRequest(request);

var credentials = Config.DefaultAWSCredentials ?? DefaultIdentityResolverConfiguration.ResolveDefaultIdentity<AWSCredentials>();
if (credentials == null)
throw new AmazonS3Exception("Credentials must be specified, cannot call method anonymously");

var irequest = CreateAndProcessRequest(request);
return BuildPresignedPostResponse(request, irequest, credentials);
}

/// <summary>
/// Internal implementation for creating presigned POST requests.
/// </summary>
/// <param name="request">The CreatePresignedPostRequest that defines the parameters of the operation.</param>
/// <returns>A CreatePresignedPostResponse containing the URL and form fields for the POST request.</returns>
/// <exception cref="T:System.ArgumentException" />
/// <exception cref="T:System.ArgumentNullException" />
[SuppressMessage("AWSSDKRules", "CR1004")]
internal async Task<CreatePresignedPostResponse> CreatePresignedPostInternalAsync(CreatePresignedPostRequest request)
{
ValidateCreatePresignedPostRequest(request);

var credentials = Config.DefaultAWSCredentials ?? DefaultIdentityResolverConfiguration.ResolveDefaultIdentity<AWSCredentials>();
if (credentials == null)
throw new AmazonS3Exception("Credentials must be specified, cannot call method anonymously");

// Resolve credentials asynchronously
var immutableCredentials = await credentials.GetCredentialsAsync().ConfigureAwait(false);

var irequest = CreateAndProcessRequest(request);
return BuildPresignedPostResponse(request, irequest, credentials);
}

/// <summary>
/// Marshalls the parameters for a presigned POST request to create a proper IRequest object.
/// </summary>
/// <param name="createPresignedPostRequest">The presigned POST request</param>
/// <returns>Internal request object</returns>
private static IRequest MarshallCreatePresignedPost(CreatePresignedPostRequest createPresignedPostRequest)
{
IRequest request = new DefaultRequest(createPresignedPostRequest, "AmazonS3");
request.HttpMethod = "POST";

// POST requests go to the bucket root, not to a specific key
request.ResourcePath = "/";
request.UseQueryString = false; // POST uses form data, not query string

return request;
}

/// <summary>
/// Builds the policy document JSON string from the request using Utf8JsonWriter.
/// This approach follows AWS SDK patterns and is Native AOT compatible.
/// </summary>
/// <param name="request">The CreatePresignedPostRequest containing the policy conditions.</param>
/// <returns>A JSON string representing the policy document.</returns>
private string BuildPolicyDocument(CreatePresignedPostRequest request)
{
#if !NETFRAMEWORK
using var arrayPoolBufferWriter = new ArrayPoolBufferWriter<byte>();
using var writer = new Utf8JsonWriter(arrayPoolBufferWriter);
#else
using var memoryStream = new MemoryStream();
using var writer = new Utf8JsonWriter(memoryStream);
#endif

writer.WriteStartObject();
writer.WriteString("expiration", request.Expires.Value.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"));
writer.WriteStartArray("conditions");

// Add bucket condition (required)
writer.WriteStartObject();
writer.WriteString("bucket", request.BucketName);
writer.WriteEndObject();

// Add key condition if specified
if (!string.IsNullOrEmpty(request.Key))
{
writer.WriteStartObject();
writer.WriteString("key", request.Key);
writer.WriteEndObject();
}

// Add field conditions
foreach (var field in request.Fields)
{
writer.WriteStartObject();
writer.WriteString(field.Key, field.Value);
writer.WriteEndObject();
}

// Add custom conditions using WriteToJsonWriter
foreach (var condition in request.Conditions)
{
condition.WriteToJsonWriter(writer);
}

writer.WriteEndArray();
writer.WriteEndObject();
writer.Flush();

#if !NETFRAMEWORK
return Encoding.UTF8.GetString(arrayPoolBufferWriter.WrittenMemory.ToArray());
#else
return Encoding.UTF8.GetString(memoryStream.ToArray());
#endif
}

#endregion

#region ICoreAmazonS3 Implementation

string ICoreAmazonS3.GeneratePreSignedURL(string bucketName, string objectKey, DateTime expiration, IDictionary<string, object> additionalProperties)
Expand Down
51 changes: 51 additions & 0 deletions sdk/src/Services/S3/Custom/Util/CreatePresignedPostRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Amazon.Runtime;
using Amazon.Util;

namespace Amazon.S3.Util
{
/// <summary>
/// Container for the parameters to create a presigned POST request for S3.
/// </summary>
public class CreatePresignedPostRequest : AmazonWebServiceRequest
{
/// <summary>
/// Gets or sets the name of the S3 bucket for the presigned POST.
/// </summary>
public string BucketName { get; set; }

/// <summary>
/// Gets or sets the key (name) of the object for the presigned POST.
/// </summary>
public string Key { get; set; }

/// <summary>
/// Gets or sets the expiration time for the presigned POST.
/// </summary>
public DateTime? Expires { get; set; }

/// <summary>
/// Gets or sets additional form fields to include in the presigned POST.
/// </summary>
public Dictionary<string, string> Fields { get; set; }

/// <summary>
/// Gets or sets the policy conditions for the presigned POST.
/// </summary>
public List<S3PostCondition> Conditions { get; set; }

/// <summary>
/// Initializes a new instance of the CreatePresignedPostRequest class.
/// </summary>
public CreatePresignedPostRequest()
{
Expires = AWSSDKUtils.CorrectedUtcNow.AddHours(1);
Fields = new Dictionary<string, string>();
Conditions = new List<S3PostCondition>();
}
}
}
35 changes: 35 additions & 0 deletions sdk/src/Services/S3/Custom/Util/CreatePresignedPostResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Amazon.S3.Util
{
/// <summary>
/// Response from creating a presigned POST request for S3.
/// Contains the URL and form fields needed for a browser-based file upload directly to S3.
/// </summary>
public class CreatePresignedPostResponse
{
/// <summary>
/// Gets the URL where the POST request should be submitted.
/// </summary>
public string Url { get; set; }

/// <summary>
/// Gets the form fields that must be included in the POST request.
/// These fields contain the policy, signature, and other AWS-required parameters.
/// </summary>
public Dictionary<string, string> Fields { get; set; }

/// <summary>
/// Initializes a new instance of the CreatePresignedPostResponse class.
/// </summary>
public CreatePresignedPostResponse()
{
Fields = new Dictionary<string, string>();
}
}

}
Loading