Skip to content

Instantly share code, notes, and snippets.

@zhuker
Created January 14, 2022 12:55
Show Gist options
  • Save zhuker/ec11b828860bf1cf72614403a5f9bf2a to your computer and use it in GitHub Desktop.
Save zhuker/ec11b828860bf1cf72614403a5f9bf2a to your computer and use it in GitHub Desktop.
AmazonS3Client GeneratePreSignedPost
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;
}
}
}
@zhuker
Copy link
Author

zhuker commented Jan 14, 2022

This is a quick-hack rewrite of generate_presigned_post from boto3 python s3 client.
aws/aws-sdk-net#1901

Example usage:

[Test]
public void TestPost()
{
    var s3 = new AmazonS3Client(new BasicAWSCredentials("MyAccessKey", "MySecretKey"), RegionEndpoint.USWest1);

    var post = s3.GeneratePreSignedPost("mybucket", "testkey",
        new List<List<string>> {new() {"content-length-range", "1", "42000"}},
        4200);

    Console.WriteLine(post.Url);
    foreach (var (key, value) in post.Fields)
    {
        Console.WriteLine(key + ": " + value);
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment