Created
January 14, 2022 12:55
-
-
Save zhuker/ec11b828860bf1cf72614403a5f9bf2a to your computer and use it in GitHub Desktop.
AmazonS3Client GeneratePreSignedPost
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Globalization; | |
using System.IO; | |
using System.Linq; | |
using System.Reflection; | |
using System.Security.Cryptography; | |
using System.Text; | |
using Amazon.Runtime; | |
using Amazon.S3; | |
using Newtonsoft.Json; | |
namespace MyNameSpace | |
{ | |
public static class S3Ext | |
{ | |
public static PreSignedPost GeneratePreSignedPost(this AmazonS3Client s3, string bucket, | |
string key, List<List<string>> conditions, int expiresInSec) | |
{ | |
var p = new PostGenerator(s3, s3.GetCredentials()); | |
return p.GeneratePreSignedPost(bucket, key, null, conditions, expiresInSec); | |
} | |
public static AWSCredentials GetCredentials(this AmazonS3Client s3) | |
{ | |
var prop = s3.GetType().GetProperty("Credentials", BindingFlags.NonPublic | BindingFlags.Instance); | |
var getter = prop.GetGetMethod(nonPublic: true); | |
return (AWSCredentials) getter.Invoke(s3, null); | |
} | |
public static string Sign(string input, byte[] key) | |
{ | |
var myhmacsha1 = new HMACSHA1(key); | |
var byteArray = Encoding.UTF8.GetBytes(input); | |
var stream = new MemoryStream(byteArray); | |
var hash = myhmacsha1.ComputeHash(stream); | |
return Convert.ToBase64String(hash); | |
} | |
} | |
public record PreSignedPost(string Url, Dictionary<string, string> Fields) | |
{ | |
public Dictionary<string, string> Fields { get; } = Fields; | |
public string Url { get; } = Url; | |
} | |
internal class PostGenerator | |
{ | |
private readonly ImmutableCredentials credentials; | |
private string _region_name; | |
private string _signing_name = "s3"; | |
private string endpoint_url; | |
public PostGenerator(AmazonS3Client s3, AWSCredentials credentials) | |
{ | |
this.credentials = credentials.GetCredentials(); | |
_region_name = s3.Config.RegionEndpoint.SystemName; | |
_signing_name = "s3"; | |
endpoint_url = "https://" + s3.Config.RegionEndpoint.GetEndpointForService("s3").Hostname; | |
} | |
internal PreSignedPost GeneratePreSignedPost(string Bucket, string Key, | |
Dictionary<string, string>? Fields, | |
List<List<string>> Conditions, | |
int ExpiresIn) | |
{ | |
var bucket = Bucket; | |
var key = Key; | |
var fields = Fields; | |
var conditions = Conditions; | |
var expires_in = ExpiresIn; | |
if (fields == null) | |
fields = new Dictionary<string, string>(); | |
else | |
throw new NotImplementedException(); | |
if (conditions == null) | |
throw new NullReferenceException(); | |
// Create a request dict based on the params to serialize. | |
var request_dict = new Dictionary<string, object> | |
{ | |
["url_path"] = "/" + Bucket, | |
["query_string"] = new Dictionary<string, string>(), | |
["method"] = "PUT", | |
["headers"] = new Dictionary<string, string>(), | |
["body"] = Array.Empty<byte>() | |
}; | |
// Prepare the request dict by including the client's endpoint url. | |
prepare_request_dict(request_dict, endpoint_url, new Dictionary<string, object> | |
{ | |
["is_presign_request"] = true, | |
["use_global_endpoint"] = should_use_global_endpoint(), | |
}); | |
conditions.Add(new List<string> {"bucket", bucket}); | |
if (key.EndsWith("${filename}")) | |
throw new NotImplementedException(); | |
else | |
conditions.Add(new List<string> {"key", key}); | |
fields["key"] = key; | |
return generate_presigned_post(request_dict, fields, conditions, expires_in); | |
} | |
PreSignedPost generate_presigned_post(Dictionary<string, object> request_dict, | |
Dictionary<string, string> fields, | |
List<List<string>> conditions, int expires_in, | |
string? region_name = null) | |
{ | |
var policy = new Dictionary<string, object>(); | |
var expire_date = DateTime.UtcNow.AddSeconds(expires_in); | |
policy["expiration"] = expire_date.ToString("yyyy-MM-dd'T'HH:mm:ssK", CultureInfo.InvariantCulture); | |
policy["conditions"] = conditions.ToArray().ToList(); | |
var request = create_request_object(request_dict); | |
request.context["s3-presign-post-fields"] = fields; | |
request.context["s3-presign-post-policy"] = policy; | |
request_signer_sign("PutObject", request, region_name, "presign-post"); | |
return new PreSignedPost(request.url, fields); | |
} | |
private void request_signer_sign(string operation_name, AWSRequest request, string? region_name, | |
string signing_type = "standard", int? expires_in = null, string? signing_name = null) | |
{ | |
var self = this; | |
var explicit_region_name = region_name; | |
if (region_name == null) | |
region_name = self._region_name; | |
if (signing_name == null) | |
signing_name = self._signing_name; | |
var signature_version = self._choose_signer( | |
operation_name, signing_type, request.context); | |
var kwargs = new Dictionary<string, string>(); | |
kwargs["signing_name"] = signing_name; | |
kwargs["region_name"] = region_name; | |
kwargs["signature_version"] = signature_version; | |
//TODO: get auth from kwargs | |
auth_add_auth(request); | |
} | |
private void auth_add_auth(AWSRequest request) | |
{ | |
var self = this; | |
var fields = new Dictionary<string, string>(); | |
if (request.context.GetValueOrDefault("s3-presign-post-fields") != null) | |
fields = request.context["s3-presign-post-fields"] as Dictionary<string, string>; | |
var policy = new Dictionary<string, object>(); | |
var conditions = new List<List<string>>(); | |
if (request.context.GetValueOrDefault("s3-presign-post-policy") != null) | |
policy = request.context["s3-presign-post-policy"] as Dictionary<string, object>; | |
if (policy.GetValueOrDefault("conditions") != null) | |
conditions = policy["conditions"] as List<List<string>>; | |
policy["conditions"] = conditions; | |
fields["AWSAccessKeyId"] = self.credentials.AccessKey; | |
if (!string.IsNullOrWhiteSpace(self.credentials.Token)) | |
{ | |
fields["x-amz-security-token"] = self.credentials.Token; | |
conditions.Add(new List<string> {"x-amz-security-token", self.credentials.Token}); | |
} | |
// # Dump the base64 encoded policy into the fields dictionary. | |
var json = JsonConvert.SerializeObject(policy); | |
fields["policy"] = Convert.ToBase64String(Encoding.UTF8.GetBytes(json)); | |
fields["signature"] = self.sign_string(fields["policy"] as string); | |
request.context["s3-presign-post-fields"] = fields; | |
request.context["s3-presign-post-policy"] = policy; | |
} | |
private string sign_string(string string_to_sign) | |
{ | |
return S3Ext.Sign(string_to_sign, Encoding.UTF8.GetBytes(credentials.SecretKey)); | |
} | |
private string _choose_signer(string operation_name, string signing_type, Dictionary<string, object> context) | |
{ | |
return "s3-presign-post"; | |
} | |
record AWSRequest(string method, string url, byte[] data, Dictionary<string, string> headers) | |
{ | |
public Dictionary<string, object> context; | |
public string method { get; } = method; | |
public string url { get; } = url; | |
public byte[] data { get; } = data; | |
public Dictionary<string, string> headers { get; } = headers; | |
} | |
private AWSRequest create_request_object(Dictionary<string, object> request_dict) | |
{ | |
var r = request_dict; | |
var request_object = new AWSRequest(r["method"] as string, r["url"] as string, r["body"] as byte[], | |
r["headers"] as Dictionary<string, string>); | |
request_object.context = request_dict["context"] as Dictionary<string, object>; | |
return request_object; | |
} | |
private void prepare_request_dict(Dictionary<string, object> request_dict, string endpoint_url, | |
Dictionary<string, object> context, string? user_agent = null) | |
{ | |
var r = request_dict; | |
if (user_agent != null) | |
{ | |
var headers = r["headers"] as Dictionary<string, string>; | |
headers["User-Agent"] = user_agent; | |
} | |
var host_prefix = r.GetValueOrDefault("host_prefix") as string ?? ""; | |
if (!string.IsNullOrWhiteSpace(host_prefix)) | |
throw new NotImplementedException(); | |
var url = endpoint_url + r["url_path"]; | |
if ((r["query_string"] as Dictionary<string, string>)?.Count != 0) | |
{ | |
throw new NotImplementedException(); | |
// # NOTE: This is to avoid circular import with utils. This is being | |
// # done to avoid moving classes to different modules as to not cause | |
// # breaking chainges. | |
// percent_encode_sequence = botocore.utils.percent_encode_sequence | |
// encoded_query_string = percent_encode_sequence(r['query_string']) | |
// if '?' not in url: | |
// url += '?%s' % encoded_query_string | |
// else: | |
// url += '&%s' % encoded_query_string | |
} | |
r["url"] = url; | |
r["context"] = context; | |
if (context == null) | |
r["context"] = new Dictionary<string, bool>(); | |
} | |
private bool should_use_global_endpoint() | |
{ | |
return true; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This is a quick-hack rewrite of generate_presigned_post from boto3 python s3 client.
aws/aws-sdk-net#1901
Example usage: