Last Updated: November 19, 2020
·
6.381K
· thatsamorais

Amazon S3 Interface in a C# Unity Monobehavior

Below is my working method for sending an Amazon AWS S3 GET request for a bucket item. This can also be modified to communicate in other ways with S3 using GET.

static readonly string BUCKET = "bucket-name";

static readonly string AWS_ACCESS_KEY_ID ="1A1A1A1A1A1A1A1A1A1A";
static readonly string AWS_SECRET_ACCESS_KEY ="1Ab1Ab1Ab1Ab1Ab1Ab1Ab1Ab1Ab1Ab1Ab1Ab1Ab1";

static readonly string AWS_S3_URL_BASE_VIRTUAL_HOSTED = "https://"+BUCKET+".s3.amazonaws.com/";
static readonly string AWS_S3_URL_BASE_PATH_HOSTED = "https://s3.amazonaws.com/"+BUCKET+"/";
static readonly string AWS_S3_URL = AWS_S3_URL_BASE_VIRTUAL_HOSTED;

void SendAmasonS3Request(string itemName)
{
    Hashtable headers = new Hashtable();

    string dateString =
        System.DateTime.UtcNow.ToString("ddd, dd MMM yyyy HH:mm:ss ") + "GMT";
    headers.Add("x-amz-date", dateString);
    Debug.Log("Date: " + dateString);

    string canonicalString = "GET\n\n\n\nx-amz-date:" + dateString + "\n/" + BUCKET + "/" + itemName;

    // now encode the canonical string
    var ae = new System.Text.UTF8Encoding();
    // create a hashing object
    HMACSHA1 signature = new HMACSHA1();
    // secretId is the hash key
    signature.Key = ae.GetBytes(AWS_SECRET_ACCESS_KEY);
    byte[] bytes  = ae.GetBytes(canonicalString);
    byte[] moreBytes = signature.ComputeHash(bytes);
    // convert the hash byte array into a base64 encoding
    string encodedCanonical = System.Convert.ToBase64String(moreBytes);

    // finally, this is the Authorization header.
    headers.Add("Authorization", "AWS " + AWS_ACCESS_KEY_ID + ":" + encodedCanonical);

    // The URL, either PATH_HOSTED or VIRTUAL_HOSTED, plus the item path in the bucket 
    string url = AWS_S3_URL + itemName;

    // Setup the request url to be sent to Amazon
    WWW www = new WWW(url, null, headers);

    // Send the request in this coroutine so as not to wait busily
    StartCoroutine(WaitForRequest(www));
}



IEnumerator WaitForRequest(WWW www)
{
    yield return www;

    // Check for errors
    if(www.error == null)
    {
        ParseResponse(www.text, www.url);
    }
    else
    {
        Debug.Log("WWW Error: "+ www.error+" for URL: "+www.url);
        ProcessAmazonS3Error(www);
    }
}

Firstly, "itemName" is the full item ~path~ within my bucket (excluding the bucket name itself), in my case something like "ItemCategory/123".

Communicating with Amazon Web Services from Unity requires the use of the WWW class to send the URL, specifically the constructor that takes a Hashtable, here called "headers".

The URL can be either "path-hosted" or "virtual-hosted", both of which I have included in the constants for you to see the difference. path-hosted has the bucket appended to the end, and virtual-hosted has the bucket as part of the domain.

The headers, in the case of GETting an Object consist of (1) the specifically formatted DateTime string, and (2) the REST Authorization Signature.

The Date time format is described above, and uses the ToString override in UtcNow. It is added to "headers" as "x-amz-date"

The REST Authorization Signature is much more complex, and each word, space, and "/" is absolutely critical. The way this works is that based on the URL of your request, AWS will calculate the signature it expects from your ACCESSKEY and SECRETKEY, then compare that with the signature you send it in the request header. To understand what is happening, first, look at "canonicalString", which describes the request using the REST Authorization Signature syntax described in the AWS S3 REST documentation. There is the verb, GET, followed by several new-lines to skip over the Content MD5, Content-type, and "Date". The next field is the Amz-headers, where I am assigning the "x-amz-date" header. Next is a new-line to go to the next ~argument~ in the signature, but also a "/" because the "Canonicalized-Resource" must begin with a "/". Next, regardless of whether you are using PATHHOSTED or VIRTUALHOSTED, you MUST phrase the request with the bucket's name, "/", then the object-path in the bucket. This string is encoded based on the SECRET_KEY using HMACSHA1 and computing hashes. The encoded string is added to "headers" as "Authorization".

The final URL should, then, have the object-path inside the bucket appended to it. The URL should request the same Object that the Signature was encoded.

the constructor of WWW should be the 3-argument form taking (1) the url, (2) a null for the "bytes" since we are not POSTing, and (3) the headers containing the Date-string and REST Authorization Signature.

The Coroutine is a Unity feature which, in short, allows you to check the state of the request where you can react to its completion, without polling. Find more about Coroutines and "yield" in the Unity documentation.

Enjoy!

Related protips:

Improved PlayerPrefs for Unity

2 Responses
Add your response

Alex, how can I do a HEAD request? I want to check the file last modification date before downloading, can you help me out with this? Thanks in advantage.

over 1 year ago ·

Hello,
First, thankyou very much for your code. I've make a little modification to get the ACCESSKEY and SECRETKEY from Cognito AWS object.

CognitoAWSCredentials caw = new CognitoAWSCredentials(YOUR_IdentityPoolId, YOUR_S3Region_End_Point);
caw.GetCredentialsAsync((IdenCallback) =>
{
ImmutableCredentials awsc = IdenCallback.Response;
AWS_ACCESS_KEY_ID = awsc.AccessKey;
AWS_SECRET_ACCESS_KEY = awsc.SecretKey;
}

(this code is woking fine).

But WWW returns this error: SSL peer certificate or SSH remote key was not OK.
Could you test again your code to verify thar is still working?

Thanks!

over 1 year ago ·