Skip to content

Instantly share code, notes, and snippets.

@AxelJunker
Last active March 8, 2022 07:51
Show Gist options
  • Save AxelJunker/07811000f239abcfc4e785f4df93c96b to your computer and use it in GitHub Desktop.
Save AxelJunker/07811000f239abcfc4e785f4df93c96b to your computer and use it in GitHub Desktop.
AWS SDK for dotnet, presigned S3 POST URL
module S3PreSignedPost
open System.Text
open Amazon.Runtime
open Amazon.Runtime.Internal.Auth
open Thoth.Json.Net
type PresignedPostOptions =
{
credentials: ImmutableCredentials
region: string
bucket: string
key: string
conditions: List<Condition>
expires: System.TimeSpan
}
and Condition =
| EqualCondition of key: string * value: string
| StartsWithCondition of key: string * value: string
| ContentLengthRangeCondition of min: uint64 * max: uint64
type Field = string * string
type ConditionType =
| Field of Field
| Condition of Condition
and PresignedPost =
{
url: string
fields: Map<string, string>
}
let private encodeFieldsAndConditions: ConditionType -> JsonValue =
function
| Field (key, value) -> Encode.object [ key, Encode.string value ]
| Condition condition ->
match condition with
| EqualCondition (key, value) ->
[ "eq"; key; value ]
|> List.map Encode.string
|> Encode.list
| StartsWithCondition (key, value) ->
[ "starts-with"; key; value ]
|> List.map Encode.string
|> Encode.list
| ContentLengthRangeCondition (min, max) ->
[
Encode.string "content-length-range"
Encode.uint64 min
Encode.uint64 max
]
|> Encode.list
// Presigned POST is not supported in AWSSDK.S3. See https://github.com/aws/aws-sdk-net/issues/1901.
// This is our own implementation.
let createPresignedPost: PresignedPostOptions -> PresignedPost =
fun options ->
let url = $"https://s3.{options.region}.amazonaws.com/{options.bucket}"
let signingDate = System.DateTime.Now.ToString("yyyyMMddTHHmmssZ")
let shortDate = signingDate.Substring(0, 8)
let credentialScope = $"{shortDate}/{options.region}/s3/aws4_request"
let credential = $"{options.credentials.AccessKey}/{credentialScope}"
let fields =
[
("key", options.key)
("bucket", options.bucket)
("X-Amz-Algorithm", "AWS4-HMAC-SHA256")
("X-Amz-Credential", credential)
("X-Amz-Date", signingDate)
("X-Amz-Security-Token", options.credentials.Token)
]
let expirationJson: JsonValue =
System
.DateTime
.Now
.Add(options.expires)
.ToString("yyyy-MM-ddTHH:mm:ssZ")
|> Encode.string
let fieldsAndConditionsJson: JsonValue =
(fields |> List.map Field)
@ (options.conditions |> List.map Condition)
|> List.map encodeFieldsAndConditions
|> Encode.list
let encodedPolicy: string =
Encode.object [ ("expiration", expirationJson)
("conditions", fieldsAndConditionsJson) ]
|> Encode.toString 2
|> Encoding.UTF8.GetBytes
|> System.Convert.ToBase64String
let signingKey =
AWS4Signer.ComposeSigningKey(options.credentials.SecretKey, options.region, shortDate, "s3")
let signature =
AWS4Signer.ComputeKeyedHash(SigningAlgorithm.HmacSHA256, signingKey, encodedPolicy)
let byteToHex bytes =
bytes
|> Array.map (fun (x: byte) -> System.String.Format("{0:x2}", x))
|> String.concat System.String.Empty
let signatureHex = byteToHex signature
let fields =
Map.ofList (
fields
@ [
("Policy", encodedPolicy)
("X-Amz-Signature", signatureHex)
]
)
{ url = url; fields = fields }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment