Showing posts with label Amazon web service. Show all posts
Showing posts with label Amazon web service. Show all posts

Thursday, April 4, 2013

Accessing Amazon AWS service from QML/Javascript

Sometime back I published a post that shows how to access Amazon Web Service using Qt. You can find original post here. Now that trend is to code everything from QML and Javascript ( at-least Ubuntu Touch is following that path to make it as device independent as possible), I tired to access AWS service from using pure QML and Javascript.

I required to use external Crypto library for HMACSHA1 algorithm but everything else can easily be done with pure QML/Javascript.

Following is code i used for downloading Book cover image from Amazon AWS.

First all to communicate with AWS you required to have AWS access key id and Secret Access key. You can get it from here. I am also importing the Crypto library, for using HmacSHA1 implementation. I am using crypto-js Library. You can download required the same from here.

.import "JSLib/rollups/hmac-sha1.js" as Crypto

//key and password, required to sign the request using HMACSHA1
var AWS_KEY = "KEY";
var AWS_PASS = "PASSWORD";
var END_POINT = "http://ecs.amazonaws.com/onca/xml";
var AWS_TAG = "TAG"

Now we have required information, we can are ready to create request. In following code, I am putting all required parameter in a map and then using it to create signature and URL. We also need to encode parameter, I am using encodeURIComponent for encoding required parameter.
function downloadCover( author,bookName,callback) {

    var queryItems = {};
    queryItems["AWSAccessKeyId"] = AWS_KEY;
    queryItems["AssociateTag"] = AWS_TAG;
    queryItems["Author"] = encodeURIComponent(author);
    queryItems["Keywords"] = encodeURIComponent(bookName);
    queryItems["Operation"] = "ItemSearch";
    queryItems["ResponseGroup"] = "Images";       
    queryItems["SearchIndex"] = "Books";
    queryItems["Service"] = "AWSECommerceService";
    queryItems["SignatureMethod"] = "HmacSHA1";
    queryItems["Timestamp"] = encodeURIComponent( new Date().toISOString());
    queryItems["Signature"] = createSignature(queryItems);


    var downloadUrl = createUrl(queryItems);
    sendNetworkRequest(downloadUrl,callback);
}
Following is code for creating signature. Creating signature is the only tricky part to make code works as expected. I am using HmacSHA1 from Crypto-JS lib. This function return WordArray object, which can be converted to HEX string, binary string or base64. Once we have HmacSHA1 hash of request, we need to convert it to Base64. I was not able to use base64 function from Crypto-JS library due to some error. I decided to copy base64 function from library directly to my js file and use it. Then finally we need to encode this base64 output, which we can be used as signature while sending request.
function createSignature(queryItems) {
    var strToSign = "GET\n";
    strToSign += "ecs.amazonaws.com\n";
    strToSign += "/onca/xml\n"

    for( var prop in queryItems ) {
        if( prop === "Signature") {
            continue;
        }

        strToSign += ( prop+"="+ queryItems[prop]);
        strToSign += "&"
    }
    //removing last &
    strToSign = strToSign.slice(0,strToSign.length-1)


    var signature = Crypto.CryptoJS.HmacSHA1(strToSign, AWS_PASS);
    signature = base64(signature);

    return encodeURIComponent(signature);
}

Now we are almost done, all we need to do is to use created signature and make HTTP request. Following code shows how. There is nothing new here. I am creating complete URL from all required parameter and making request using this URL by XMLHttpRequest object.

function createUrl(queryItems )
{
    var url = END_POINT+"?";

    for( var prop in queryItems ) {
        url += ( prop+"="+ queryItems[prop]);
        url += "&"
    }
    //removing last &
    url = url.slice(0,url.length-1);
    return url;
}

function sendNetworkRequest(url,callback) {
    var http = new XMLHttpRequest();
    http.onreadystatechange = function() {
        if (http.readyState === XMLHttpRequest.HEADERS_RECEIVED) {
            console.log("Headers -->");
            console.log(http.getAllResponseHeaders ());
            console.log("Last modified -->");
            console.log(http.getResponseHeader ("Last-Modified"));

        } else if (http.readyState === XMLHttpRequest.DONE) {
            console.log(http.responseText);
            callback(http.responseText);
        }
    }

    http.open("GET", url);
    http.send();
}

Saturday, September 10, 2011

Accessing Amazon Web service from Qt

Currently I am trying to port my Audiobook reader application to Harmattan. I decided to improve its UI and as a change I decided to show book cover page in background.

Initially I thought to use taglib to fetch album art from audio file, but then I decided to download cover art from internet as some audio file might not contain album art information.

To download cover art from internet, I decided to use Amazon web service. Initially I struggled to successfully communicate with AWS.

AWS required to send signature with every request you send and there is certain procedure to create that signature. Creating signature with Qt is quite easy, but still I was facing problem with retrieving data from AWS because I did not read QUrl documentation carefully.

While generating QUrl, it expect human redable URL string with no percentage encoding. But I was trying to generate QUrl from already encoded parameters.

And as a result I was getting such error message.
"Value 2011-09-11T05%3A18%3A42 for parameter Timestamp is invalid. Reason: Must be in ISO8601 format".

Double encoding was causing above problem.

I am posting my code here, in hope that will be helpful to someone.

First all to communicate with AWS you required to have AWS access key id and Secret Access key. You can get it from here.

Here is AWS doc that explain how to generate signature. And here utility which you can use to verify if you generated correct signature.

Now we are ready for implementation. Following is my code.

Here in below code, I am trying to create AWS request to get all image for mentioned book. In request I am also specifying SignatureMetod to HmacSHA1, by default AWS use HmacSHA256 signature method for comparison.

const char AWS_KEY[] = "YOU ACCESS KEY";
const char AWS_PASS[] = "YOUR SECRET KEY";
const char END_POINT[] = "http://ecs.amazonaws.com/onca/xml";

AwsAlbumArtHelper::AwsAlbumArtHelper(QObject *parent) :
    QObject(parent)
{
    _netManager = new QNetworkAccessManager(this);    
}

QByteArray AwsAlbumArtHelper::getTimeStamp()
{
    QDateTime dateTime = QDateTime::currentDateTimeUtc();
    return dateTime.toString(Qt::ISODate).toUtf8();
}

void AwsAlbumArtHelper::downloadAlbumArt(const QString& bookname)
{
    _bookName = bookname;
    QMap< QString,QString > queryItems;
    queryItems["AWSAccessKeyId"] = AWS_KEY;
    queryItems["ResponseGroup"] = "Images";
    queryItems["Keywords"] = QUrl::toPercentEncoding(bookname);
    queryItems["Operation"] = "ItemSearch";
    queryItems["SearchIndex"] = "Books";
    queryItems["Service"] = "AWSECommerceService";
    queryItems["SignatureMethod"] = "HmacSHA1";
    queryItems["Timestamp"] = QUrl::toPercentEncoding(getTimeStamp());
    queryItems["Signature"] = createSignature(queryItems);

    QUrl downloadUrl = createUrl(queryItems);
    QNetworkRequest request(downloadUrl);

    disconnect(_netManager,SIGNAL(finished(QNetworkReply*)),this,SLOT(imageDownloaded(QNetworkReply*)));
    connect(_netManager,SIGNAL(finished(QNetworkReply*)),this,SLOT(xmlDownloaded(QNetworkReply*)));
    _netManager->get(request);
}
Following code create signature from query parameters. I used code pasted here for signing string with hmacSha1.
QByteArray AwsAlbumArtHelper::createSignature(const QMap< QString,QString >& queryItems)
{
    QUrl url(END_POINT);
    QString stringToSign = "GET\n";
    stringToSign.append(url.host() + "\n");
    stringToSign.append(url.path() + "\n");

    QList<qstring> keys = queryItems.keys();
    for( int i=0; i < keys.count() ; ++i ) {
        stringToSign.append(keys[i]+"="+queryItems[keys[i]]);
        if( i != keys.count() -1  ) {
            stringToSign.append("&");
        }
    }
    QString signature = hmacSha1(AWS_PASS,stringToSign.toUtf8());
    return QUrl::toPercentEncoding(signature);
}

QUrl AwsAlbumArtHelper::createUrl( const QMap< QString,QString >& queryItems )
{
    QUrl url(END_POINT);
    QMapIterator<QString, QString> it(queryItems);
    while (it.hasNext()) {            
        it.next();
        //everything is already URL encoded
        url.addEncodedQueryItem(it.key().toUtf8(), it.value().toUtf8());
    }
    return url;
}
Now we are ready to submit request using downloadAlbumArt method with required book name and AWS should return xml data with various image link. We need to parse xml and use mentioned link to download image.