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.

2 comments:

  1. It doesn't work for me ... there's a problem about signature .... appeared:
    "The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details."

    ReplyDelete
  2. I am not sure why this is happening to you. If you can send you code then I might be able to debug it.

    ReplyDelete