Classic ASP amazon s3 rest authorisation
Asked Answered
H

3

13

I am confused on what I am doing wrong here...

<script language="javascript" runat="server">
  function GMTNow(){return new Date().toGMTString()}
</script>
<%

Const AWS_BUCKETNAME = "uk-bucketname"
Const AWS_ACCESSKEY = "GOES HERE"
Const AWS_SECRETKEY = "SECRET"
LocalFile = Server.Mappath("/test.jpg")

Dim sRemoteFilePath
    sRemoteFilePath = "/files/test.jpg" 'Remote Path, note that AWS paths (in fact they aren't real paths) are strictly case sensitive

Dim strNow
    strNow = GMTNow() ' GMT Date String

Dim StringToSign
    StringToSign = Replace("PUT\n\nimage/jpeg\n\nx-amz-date:" & strNow & "\n/"& AWS_BUCKETNAME & sRemoteFilePath, "\n", vbLf)

Dim Signature
    Signature = BytesToBase64(HMACSHA1(AWS_SECRETKEY, StringToSign))

Dim Authorization
    Authorization = "AWS " & AWS_ACCESSKEY & ":" & Signature

Dim AWSBucketUrl
    AWSBucketUrl = "http://s3.amazonaws.com/" & AWS_BUCKETNAME

With Server.CreateObject("Microsoft.XMLHTTP")
    .open "PUT", AWSBucketUrl & sRemoteFilePath, False
    .setRequestHeader "Authorization", Authorization
    .setRequestHeader "Content-Type", "image/jpeg"
    .setRequestHeader "Host", AWS_BUCKETNAME & ".s3.amazonaws.com"  
    .setRequestHeader "x-amz-date", strNow
    .send GetBytes(LocalFile) 'Get bytes of local file and send
    If .status = 200 Then ' successful
        Response.Write "<a href="& AWSBucketUrl & sRemoteFilePath &" target=_blank>Uploaded File</a>"
    Else ' an error ocurred, consider xml string of error details
        Response.ContentType = "text/xml"
        Response.Write .responseText
    End If
End With

Function GetBytes(sPath)
    dim fs,f
set fs=Server.CreateObject("Scripting.FileSystemObject")
set f=fs.GetFile(sPath)
GetBytes = f.Size
set f=nothing
set fs=nothing
End Function

Function BytesToBase64(varBytes)
    With Server.CreateObject("MSXML2.DomDocument").CreateElement("b64")
        .dataType = "bin.base64"
        .nodeTypedValue = varBytes
        BytesToBase64 = .Text
    End With
End Function

Function HMACSHA1(varKey, varValue)
    With Server.CreateObject("System.Security.Cryptography.HMACSHA1")
        .Key = UTF8Bytes(varKey)
        HMACSHA1 = .ComputeHash_2(UTF8Bytes(varValue))
    End With
End Function

Function UTF8Bytes(varStr)
    With Server.CreateObject("System.Text.UTF8Encoding")
        UTF8Bytes = .GetBytes_4(varStr)
    End With
End Function
%>

Now getting the error.

msxml3.dll error '800c0008'

The download of the specified resource has failed.

/s3.asp, line 39
Halo answered 13/6, 2012 at 10:17 Comment(7)
Did you manage to find your problem Chris?Educationist
Hello Chris, are you signed up for the Amazon S3 service as well as an AWS account? see this thread forums.aws.amazon.com/thread.jspa?threadID=45582Bruckner
Yes, I have a bucket set up and ready to go :)Halo
+1 for having the guts to combine cutting edge cloud tech with stone age ASP tech. Hope you find a good solution.Ninurta
@ChrisDowdeswell You should define full relative path in sRemoteFilePath as /files/test.jpg. GetBytes has been modified incorrect. It must return bytes of file not size of bytes. I'll update my answer with these improvements as you need current. As starting, just copy and use after modifying S3 credentials.Chichihaerh
I have updated my code, the problem is giving me. "The requested header was not found" Problem code is .send GetBytes(LocalFile)Halo
Thats with MSXML2.ServerXMLHTTP.3.0 if I used 6.0 I get The HTTP redirect request failed same line of code.Halo
C
10

I'd like to explain how S3 Rest Api works as far as I know.
First, you need to learn what should be the string to sign Amazon accepts.

Format :

StringToSign = HTTP-Verb + "\n" +
    Content-MD5 + "\n" +
    Content-Type + "\n" +
    Date + "\n" +
    CanonicalizedAmzHeaders +
    CanonicalizedResource;

Generating signed string :

Signature = Base64( HMAC-SHA1( YourSecretAccessKeyID, UTF-8-Encoding-Of( StringToSign ) ) );

Passing authorization header:

Authorization = "AWS" + " " + AWSAccessKeyId + ":" + Signature;

Unfortunately you'll play byte to byte since there is no any SDK released for classic asp. So, should understand by reading the entire page http://docs.amazonwebservices.com/AmazonS3/latest/dev/RESTAuthentication.html

For string to sign as you can see above in format, there are three native headers are reserved by the API. Content-Type, Content-MD5 and Date. These headers must be exists in the string to sign even your request hasn't them as empty without header name, just its value. There is an exception, Date header must be empty in string to sign if x-amz-date header is already exists in the request. Then, If request has canonical amazon headers, you should add them as key-value pairs like x-amz-headername:value. But, there is another exception need to be considered for multiple headers. Multiple headers should combine to one header with values comma separated.

Correct

x-amz-headername:value1,value2

Wrong

x-amz-headername:value1\n
x-amz-headername:value2

Most importantly, headers must be ascending order by its group in the string to sign. First, reserved headers with ascending order, then canonical headers with ascending order.

I'd recommend using DomDocument functionality to generate Base64 encoded strings. Additionally instead of a Windows Scripting Component (.wsc files), you could use .Net's interops such as System.Security.Cryptography to generating keyed hashes more effectively with power of System.Text. All of these interoperabilities are available in today's IIS web servers.
So, as an example I wrote the below script just sends a file to bucket you specified. Consider and test it.
Assumed local file name is myimage.jpg and will be uploaded with same name to root of the bucket.

<script language="javascript" runat="server">
function GMTNow(){return new Date().toGMTString()}
</script>
<%
Const AWS_BUCKETNAME = "uk-bucketname"
Const AWS_ACCESSKEY = "GOES HERE"
Const AWS_SECRETKEY = "SECRET"

LocalFile = Server.Mappath("/test.jpg")

Dim sRemoteFilePath
    sRemoteFilePath = "/files/test.jpg" 'Remote Path, note that AWS paths (in fact they aren't real paths) are strictly case sensitive

Dim strNow
    strNow = GMTNow() ' GMT Date String

Dim StringToSign
    StringToSign = Replace("PUT\n\nimage/jpeg\n\nx-amz-date:" & strNow & "\n/"& AWS_BUCKETNAME & sRemoteFilePath, "\n", vbLf)

Dim Signature
    Signature = BytesToBase64(HMACSHA1(AWS_SECRETKEY, StringToSign))

Dim Authorization
    Authorization = "AWS " & AWS_ACCESSKEY & ":" & Signature

Dim AWSBucketUrl
    AWSBucketUrl = "https://" & AWS_BUCKETNAME & ".s3.amazonaws.com"

With Server.CreateObject("MSXML2.ServerXMLHTTP.6.0")
    .open "PUT", AWSBucketUrl & sRemoteFilePath, False
    .setRequestHeader "Authorization", Authorization
    .setRequestHeader "Content-Type", "image/jpeg"
    .setRequestHeader "Host", AWS_BUCKETNAME & ".s3.amazonaws.com"  
    .setRequestHeader "x-amz-date", strNow
    .send GetBytes(LocalFile) 'Get bytes of local file and send
    If .status = 200 Then ' successful
        Response.Write "<a href="& AWSBucketUrl & sRemoteFilePath &" target=_blank>Uploaded File</a>"
    Else ' an error ocurred, consider xml string of error details
        Response.ContentType = "text/xml"
        Response.Write .responseText
    End If
End With

Function GetBytes(sPath)
    With Server.CreateObject("Adodb.Stream")
        .Type = 1 ' adTypeBinary
        .Open
        .LoadFromFile sPath
        .Position = 0
        GetBytes = .Read
        .Close
    End With
End Function

Function BytesToBase64(varBytes)
    With Server.CreateObject("MSXML2.DomDocument").CreateElement("b64")
        .dataType = "bin.base64"
        .nodeTypedValue = varBytes
        BytesToBase64 = .Text
    End With
End Function

Function HMACSHA1(varKey, varValue)
    With Server.CreateObject("System.Security.Cryptography.HMACSHA1")
        .Key = UTF8Bytes(varKey)
        HMACSHA1 = .ComputeHash_2(UTF8Bytes(varValue))
    End With
End Function

Function UTF8Bytes(varStr)
    With Server.CreateObject("System.Text.UTF8Encoding")
        UTF8Bytes = .GetBytes_4(varStr)
    End With
End Function
%>


Chichihaerh answered 20/9, 2012 at 20:49 Comment(9)
I'm getting the error "msxml6.dll error '80072f7c' The HTTP redirect request failed" any ideas what could be causing it?Halo
@ChrisDowdeswell What changes have you made in the code except for bucket name, access key and secret key?Chichihaerh
Nothing except the variables you put... :(Halo
@ChrisDowdeswell I really don't know what is causing this. On my computer it's works like a charm, and also with many online web pages currently. So, If you able to sniff http requests using a utility such HttpSniffer, can figure it out. In addition, maybe you should try older versions of MSXML e.g. MSXML2.ServerXMLHTTP.3.0Chichihaerh
Ok I changed the version to 3.0 and got "The requested header was not found"Halo
@ChrisDowdeswell Unfortunately to help through comments is so hard. Would be better if you update the question with latest state and I'd like to see complete code you're trying even if you've changed a letter.Chichihaerh
@Chichihaerh - so helpful, thanks! This worked perfectly for adding objects. Further, I'm trying to modify this to perform a copy operation, but can't get the authorization working, specifically the string to sign. Could you possibly give me an example of copying an object from one bucket to another. Totally pulling my hair out using classic ASP. Sigh.Delectable
Change the Bucket URL to the following, otherwise it only works in us-east-1 AWSBucketUrl = "https://" & AWS_BUCKETNAME & ".s3.amazonaws.com" - that solves the redirect error.Advisee
@RichardBenson In fact, I don't remember how I tested the code. If you're sure about it, please feel free to edit. Thanks.Chichihaerh
B
2

The Amazon Signature must be url encoded in a slightly different way to what VBSCript encodes. The following function will encode the result correctly:

JScript Version:

function amazonEncode(s)
{
    return Server.UrlEncode(s).replace(/\+/g,"%20").replace(/\%2E/g,".").replace(/\%2D/g,"-").replace(/\%7E/g,"~").replace(/\%5F/g,"_");
}

VBScript Version:

function amazonEncode(s)
    dim retval
    retval = Server.UrlEncode(s)
    retval = replace(retval,"+","%20")
    retval = replace(retval,"%2E",".")
    retval = replace(retval,"%2D","-")
    retval = replace(retval,"%7E","~")
    retval = replace(retval,"%5F","_")
    amazonEncode = retval
end function

As for base64, I used .NET's already built functionality for it. I had to create a DLL to wrap it, so that I could use it from JScript (or VBScript).

Here's how to create that dll:

Download the free C# 2010 Express and install it.
You also need to use two other tools that you won’t have a path to, so you will need to add the path to your PATH environment variable, so at a cmd prompt search for regasm.exe, guidgen.exe and sn.exe (you might find several versions – select the one with the latest date).
•   cd\
•   dir/s regasm.exe
•   dir/s sn.exe
•   dir/s guidgen.exe


So as an example, a COM object that has just one method which just returns “Hello”:
Our eventual aim is to use it like this:
<%@Language=JScript%>
<%
var x = Server.CreateObject("blah.whatever");
Response.Write(x.someMethod());
%>

or 

<%@Language=VBScript%>
<%
dim x
set x = Server.CreateObject("blah.whatever")
Response.Write x.someMethod()
%>

•   Start C# and create a new project
•   Select “Empty Project”
•   Give it a name – this becomes the namespace by default (the blah in the sample above)
•   Next save the project (so you know where to go for the next bit).  This will create a folder structure like so:
o   blah    this contains your solution files that the editor needs (blah.sln etc)
   blah    this contains your source code and project files
•   bin
o   Debug           the compiled output ends up here
•   Next, using the cmd console, navigate to the root blah folder and create a key pair file:
   sn –k key.snk
•   Next you need a unique guid (enter guidgen at the cmd prompt)
o   Select registry format
o   Click “New Guid”
o   Click “Copy”
•   Back to C# editor – from the menu, select Project – Add Class
•   Give it a name – this is the whatever in the sample above
•   After the opening brace just after the namespace line type:
   [GuidAttribute(“paste your guid here”)]
   remove the curly brackets from your pasted guid
•   You will need to add another “using” at the top
  using System.Runtime.InteropServices;
•   Finally you need to create someMethod

The final C# code looks like this (the bits in red may be different in your version):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;

namespace blah
{
    [GuidAttribute("AEF4F27F-9E97-4189-9AD5-64386A1699A7")]
    public class whatever
    {
        public string someMethod()
        {
            return "Hello";
        }
    }
}

•   Next, from the menu, select Project – Properties
o   On the left, select Application and, for the Output type dropdown, select “Class Library”
o   On the left, select Signing and tick the “Sign the assembly” box, then browse to the key.snk file you made earlier
o   Save the properties (CTRL-S)
•   Next build the dll (Press F6) – This will create a dll in the Debug folder
•   Open a cmd window as administrator (right click cmd.exe and select “Run as Administrator”)
•   Navigate to the Debug folder and enter the following to register the assembly:
  regasm blah.dll /tlb:blah.tlb /codebase blah

That’s it – the above is a genuine COM component and will work in other applications, the example below allows for event handling and only really works in ASP due to the default property mechanism of ASP:

The code for the base64 stuff would be:

    // returns a base 64 encoded string that has been encrypted with SHA256
    // parameters:
    //  s   string to encrypt
    //  k   key to use during encryption
    public string getBase64SHA256(string s, string k)
    {
        HMACSHA256 sha = new HMACSHA256();
        System.Text.UTF8Encoding encoding = new System.Text.UTF8Encoding();
        sha.Key = encoding.GetBytes(k);
        byte[] hashBytes = sha.ComputeHash(encoding.GetBytes(s));
        return System.Convert.ToBase64String(hashBytes);
    }

    // returns a base 64 encoded string that has been encrypted with SHA1
    // parameters:
    //  s   string to encrypt
    //  k   key to use during encryption
    public string getBase64SHA1(string s, string k)
    {
        HMACSHA1 sha = new HMACSHA1();
        System.Text.UTF8Encoding encoding = new System.Text.UTF8Encoding();
        sha.Key = encoding.GetBytes(k);
        byte[] hashBytes = sha.ComputeHash(encoding.GetBytes(s));
        return System.Convert.ToBase64String(hashBytes);
    }

You would need the relevant usings:

using System.Security.Cryptography;

The signature in full must have all the query string name-value pairs in alphabetical order before computing the SHA and base64. Here is my version of the signature creator function:

function buildAmazonSignature(host,req,qstring)
{
    var str="", i, arr = String(qstring).split("&");

    for (i=0; i<arr.length; i++)
        arr[i] = arr[i].split("=");
    arr.sort(amazonSortFunc);

    for (i=0; i<arr.length; i++)
    {
        if (str != "")
            str += "&";

        str += arr[i][0] + "=" + arr[i][1];
    }

    str = "GET\n"+host+"\n"+req+"\n"+str;

    var utils = Server.CreateObject("FMAG.Utils");
    var b64 = utils.getBase64SHA256(str, "xxxxxxxxxx");
    utils = null;

    return amazonEncode(b64);
}

function amazonSortFunc(a,b)
{
    return (a[0]<b[0])?-1:((a[0]>b[0])?1:0);
}

VBScript doesn't have a very good array sort facility, so you'll have to work that one out yourself - sorry

Also I have the timestamp in this format:

YYYY-MM-DDTHH:MM:SSZ

Also the stuff in the query string included the following:

AWSAccessKeyId
SignatureMethod
SignatureVersion
Version
Expires
Action

Hope that helps

Babiche answered 19/9, 2012 at 10:57 Comment(0)
I
0

Thank you so much for this question, it has been such a great help to start my WSH/VBScript for my S3 backup service ;-)

I do not have much time, so I will not go through the details of the things I have changed from Chris' code, but please find below my little prototype script which works perfectly ;-)

This is just a WSH/VBScript, so you do not need IIS to run it, you just need to paste the content in a file with the ".vbs" extension, and you can then directly execute it ;-)

Option Explicit
'-- Amazon Web Services > My Account > Access Credentials > Access Keys --'
Dim strAccessKeyID: strAccessKeyID = "..."
Dim strSecretAccessKey: strSecretAccessKey = "..."
'-- Parameters: --'
Dim strLocalFile: strLocalFile = "..."
Dim strRemoteFile: strRemoteFile = "..."
Dim strBucket: strBucket = "..."
'-- Authentication: --'
Dim strNowInGMT: strNowInGMT = NowInGMT()
Dim strStringToSign: strStringToSign = _
  "PUT" & vbLf & _
  "" & vbLf & _
  "text/xml" & vbLf & _
  strNowInGMT & vbLf & _
  "/" & strBucket + "/" & strRemoteFile
Dim strSignature: strSignature = ConvertBytesToBase64(HMACSHA1(strSecretAccessKey, strStringToSign))
Dim strAuthorization: strAuthorization = "AWS " & strAccessKeyID & ":" & strSignature
'-- Upload: --'
Dim xhttp: Set xhttp = CreateObject("MSXML2.ServerXMLHTTP")
xhttp.open "PUT", "http://" & strBucket & ".s3.amazonaws.com/" & strRemoteFile, False
xhttp.setRequestHeader "Content-Type", "text/xml"
xhttp.setRequestHeader "Date", strNowInGMT 'Yes, this line is mandatory ;-) --'
xhttp.setRequestHeader "Authorization", strAuthorization
xhttp.send GetBytesFromFile(strLocalFile)
If xhttp.status = "200" Then
  WScript.Echo "The file has been successfully uploaded ;-)"
Else
  WScript.Echo "There was an error :-(" & vbCrLf & vbCrLf & _
  xhttp.responseText
End If
Set xhttp = Nothing
'-- NowInGMT ------------------------------------------------------------------'
Function NowInGMT()
  'This is probably not the best implementation, but it works ;-) --'
  Dim sh: Set sh = WScript.CreateObject("WScript.Shell")
  Dim iOffset: iOffset = sh.RegRead("HKLM\System\CurrentControlSet\Control\TimeZoneInformation\ActiveTimeBias")
  Dim dtNowGMT: dtNowGMT = DateAdd("n", iOffset, Now())
  Dim strDay: strDay = "NA"
  Select Case Weekday(dtNowGMT)
    Case 1 strDay = "Sun"
    Case 2 strDay = "Mon"
    Case 3 strDay = "Tue"
    Case 4 strDay = "Wed"
    Case 5 strDay = "Thu"
    Case 6 strDay = "Fri"
    Case 7 strDay = "Sat"
    Case Else strDay = "Error"
  End Select
  Dim strMonth: strMonth = "NA"
  Select Case Month(dtNowGMT)
    Case 1 strMonth = "Jan"
    Case 2 strMonth = "Feb"
    Case 3 strMonth = "Mar"
    Case 4 strMonth = "Apr"
    Case 5 strMonth = "May"
    Case 6 strMonth = "Jun"
    Case 7 strMonth = "Jul"
    Case 8 strMonth = "Aug"
    Case 9 strMonth = "Sep"
    Case 10 strMonth = "Oct"
    Case 11 strMonth = "Nov"
    Case 12 strMonth = "Dec"
    Case Else strMonth = "Error"
  End Select
  Dim strHour: strHour = CStr(Hour(dtNowGMT))
  If Len(strHour) = 1 Then strHour = "0" & strHour End If
  Dim strMinute: strMinute = CStr(Minute(dtNowGMT))
  If Len(strMinute) = 1 Then strMinute = "0" & strMinute End If
  Dim strSecond: strSecond = CStr(Second(dtNowGMT))
  If Len(strSecond) = 1 Then strSecond = "0" & strSecond End If
  Dim strNowInGMT: strNowInGMT = _
    strDay & _
    ", " & _
    Day(dtNowGMT) & _
    " " & _
    strMonth & _
    " " & _
    Year(dtNowGMT) & _
    " " & _
    strHour & _
    ":" & _
    strMinute & _
    ":" & _
    strSecond & _
    " +0000"
  NowInGMT = strNowInGMT
End Function
'-- GetBytesFromString --------------------------------------------------------'
Function GetBytesFromString(strValue)
  Dim stm: Set stm = CreateObject("ADODB.Stream")
  stm.Open
  stm.Type = 2
  stm.Charset = "ascii"
  stm.WriteText strValue
  stm.Position = 0
  stm.Type = 1
  GetBytesFromString = stm.Read
  Set stm = Nothing
End Function
'-- HMACSHA1 ------------------------------------------------------------------'
Function HMACSHA1(strKey, strValue)
  Dim sha1: Set sha1 = CreateObject("System.Security.Cryptography.HMACSHA1")
  sha1.key = GetBytesFromString(strKey)
  HMACSHA1 = sha1.ComputeHash_2(GetBytesFromString(strValue))
  Set sha1 = Nothing
End Function
'-- ConvertBytesToBase64 ------------------------------------------------------'
Function ConvertBytesToBase64(byteValue)
  Dim dom: Set dom = CreateObject("MSXML2.DomDocument")
  Dim elm: Set elm = dom.CreateElement("b64")
  elm.dataType = "bin.base64"
  elm.nodeTypedValue = byteValue
  ConvertBytesToBase64 = elm.Text
  Set elm = Nothing
  Set dom = Nothing
End Function
'-- GetBytesFromFile ----------------------------------------------------------'
Function GetBytesFromFile(strFileName)
  Dim stm: Set stm = CreateObject("ADODB.Stream")
  stm.Type = 1 'adTypeBinary --'
  stm.Open
  stm.LoadFromFile strFileName
  stm.Position = 0
  GetBytesFromFile = stm.Read
  stm.Close
  Set stm = Nothing
End Function

Dear stone-edge-technology-VBScript-mates (*), let me know if it is working for you as well ;-)

(*) This is a reference to the comment from Spudley, see above ;-)

Idealistic answered 12/1, 2013 at 21:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.