Twitter OAuth in C++ for Win32 – Part 3 – Updating Twitter Status

Part 1 covered the OAuth process at a high level, and Part 2 went over the example code in detail.  Here in Part 3, we’ll finish things off by adding support for doing OAuth POST requests, enabling you to update your Twitter status.

The example project and source files are available on Google Code; C++ code for Win32, with a Visual Studio 2008 project file.

OAuth POST Requests

Doing a POST request isn’t much different from doing a GET request, but there are still a few important things that you need to get right and, like all things OAuth, if there is a single character out of place somewhere it won’t work.

First one simple cosmetic change for clarity, I renamed the OAuthParameters type to HTTPParameters to reflect its general nature. I’m using it for holding GET and POST query parameters as well as those used for OAuth.

typedef std::map<wstring, wstring> HTTPParameters;

This name change is reflected throughout the code. Next we add a new URL constant for accessing the status update API on twitter:

const wstring UserStatusUpdateUrl = L"http://twitter.com/statuses/update.xml";

I added a quick interface to prompt the user for their status update after retrieving the user’s timeline:

 wprintf(L"Enter your status update (Enter to skip): ");

    wchar_t buf[500] = {L""};
    _getws_s(buf, SIZEOF(buf));

    wstring input = buf;
    if(input.size() > 0)
    {
        wprintf(L"\r\nUpdating status: %s\r\n", input.c_str());

        HTTPParameters postParams;
        postParams[L"status"] = UrlEncode(input);

        wstring submitResult = OAuthWebRequestSubmit(UserStatusUpdateUrl, L"POST", &postParams, ConsumerKey, ConsumerSecret,
            oauthAccessToken, oauthAccessTokenSecret);

        wprintf(L"\r\nUpdate Result:\r\n%s\r\n", submitResult.c_str());
    }

After checking if the user has entered an update and not an empty string, the first step is to build an HTTPParameters object and assign the user’s text to the 'status' parameter. There are a number of other possible parameters which could also be added to postParams, as documented on the Twitter API statuses/update reference page.

Note that the status text is URL Encoded immediately, before it is put into the parameters object. I originally had made the mistake of leaving this until later, when I build the POST query string to send over HTTP. This is incorrect (as I’ve implemented things), because when we generate the OAuth signature later, we need to sign the POST query values as they are sent to the server. In this case, that means URL Encoded, and since I was only URL encoding the data as I sent it in the POST request, the signed data didn’t match (it wasn’t URL encoded). One alternative would have been to do the URL encoding in multiple places, when I generate the OAuth signature, and when generating the POST form data. Doing it once removes decreases your chances of forgetting or doing things differently in multiple places later.

There are two other changes of note here, one is that we are passing "POST" as the HTTP request method, indicating that we are passing parameters as the request body, and not as part of the URL as in GET requests. The second is I’ve added a new parameter to OAuthWebRequestSubmit, following the request method is an optional pointer to an HTTPParameters list, here we provide postParams which contains the status value.

This second change is also reflected throughout the rest of the example code, where all of the other calls are using the "GET" HTTP request method I am simply passing NULL for the unused postParameters value.

The updated OAuthWebRequestSubmit:

wstring OAuthWebRequestSubmit(
    const wstring& url,
    const wstring& httpMethod,
    const HTTPParameters* postParameters,
    const wstring& consumerKey,
    const wstring& consumerSecret,
    const wstring& oauthToken = L"",
    const wstring& oauthTokenSecret = L"",
    const wstring& pin = L""
    )
{
    wstring query = UrlGetQuery(url);
    HTTPParameters getParameters = ParseQueryString(query);

    HTTPParameters oauthSignedParameters = BuildSignedOAuthParameters(
        getParameters,
        url,
        httpMethod,
        postParameters,
        consumerKey, consumerSecret,
        oauthToken, oauthTokenSecret,
        pin );
    return OAuthWebRequestSignedSubmit(oauthSignedParameters, url, httpMethod, postParameters);
}

The only difference is that we are now passing the new postParameters value to BuildSignedOAuthParameters and OAuthWebRequestSignedSubmit (formerly also named OAuthWebRequestSubmit, which wasn’t a great choice for clarity), as well as passing the httpMethod to OAuthWebRequestSignedSubmit which now takes the HTTP request method as an argument instead of hard coding it to "GET" internally.

BuildSignedOAuthParameters actually does something useful with the new postParameters value:

 // create a parameter list containing both oauth and original parameters
    // this will be used to create the parameter signature
    HTTPParameters allParameters = requestParameters;
    if(Compare(httpMethod, L"POST", false) && postParameters)
    {
        allParameters.insert(postParameters->begin(), postParameters->end());
    }
    allParameters.insert(oauthParameters.begin(), oauthParameters.end());

    // prepare a signature base, a carefully formatted string containing
    // all of the necessary information needed to generate a valid signature
    wstring normalUrl = OAuthNormalizeUrl(url);
    wstring normalizedParameters = OAuthNormalizeRequestParameters(allParameters);
    wstring signatureBase = OAuthConcatenateRequestElements(httpMethod, normalUrl, normalizedParameters);

    // obtain a signature and add it to header requestParameters
    wstring signature = OAuthCreateSignature(signatureBase, consumerSecret, requestTokenSecret);
    oauthParameters[L"oauth_signature"] = signature;

    return oauthParameters;

Where before we were taking the request parameters (parsed from the URL) and adding in the OAuth parameters (that we just generated) and combining them into the same list, we are now adding the postParameters into the mix as well, but only if it’s a "POST" request, and there are any postParameters available to be added. As you can see, this master list is then sent to OAuthNormalizeRequestParameters whose return value is then concatenated and ultimately included in the OAuthCreateSignature process.

Finally, after the signature is generated we pass our two new parameters to OAuthWebRequestSignedSubmit where the actual working of doing the POSTing happens:

wstring OAuthWebRequestSignedSubmit(
    const HTTPParameters& oauthParameters,
    const wstring& url,
    const wstring& httpMethod,
    const HTTPParameters* postParameters
    )
{
    _TRACE("OAuthWebRequestSignedSubmit(%s)", url.c_str());

    wstring oauthHeader = L"Authorization: OAuth ";
    oauthHeader += OAuthBuildHeader(oauthParameters);
    oauthHeader += L"\r\n";

    _TRACE("%s", oauthHeader.c_str());

    wchar_t host[1024*4] = {};
    wchar_t path[1024*4] = {};

    URL_COMPONENTS components = { sizeof(URL_COMPONENTS) };

    components.lpszHostName = host;
    components.dwHostNameLength = SIZEOF(host);

    components.lpszUrlPath = path;
    components.dwUrlPathLength = SIZEOF(path);

    wstring normalUrl = url;

    BOOL crackUrlOk = InternetCrackUrl(url.c_str(), url.size(), 0, &components);
    _ASSERTE(crackUrlOk);

    wstring result;

    // TODO you'd probably want to InternetOpen only once at app initialization
    HINTERNET hINet = InternetOpen(L"tc2/1.0",
        INTERNET_OPEN_TYPE_PRECONFIG,
        NULL,
        NULL,
        0 );
    _ASSERTE( hINet != NULL );
    if ( hINet != NULL )
    {
        // TODO add support for HTTPS requests
        HINTERNET hConnection = InternetConnect(
            hINet,
            host,
            components.nPort,
            NULL,
            NULL,
            INTERNET_SERVICE_HTTP,
            0, 0 );
        _ASSERTE(hConnection != NULL);
        if ( hConnection != NULL)
        {
            HINTERNET hData = HttpOpenRequest( hConnection,
                httpMethod.c_str(),
                path,
                NULL,
                NULL,
                NULL,
                INTERNET_FLAG_KEEP_CONNECTION | INTERNET_FLAG_NO_CACHE_WRITE | INTERNET_FLAG_NO_AUTH | INTERNET_FLAG_RELOAD,
                0 );
            _ASSERTE(hData != NULL);
            if ( hData != NULL )
            {
                BOOL oauthHeaderOk = HttpAddRequestHeaders(hData,
                    oauthHeader.c_str(),
                    oauthHeader.size(),
                    0);
                _ASSERTE(oauthHeaderOk);

                // NOTE POST requests are supported, but the MIME type is hardcoded to application/x-www-form-urlencoded (aka. form data)
                // TODO implement support for posting image, raw or other data types
                string postDataUTF8;
                if(Compare(httpMethod, L"POST", false) && postParameters)
                {
                    wstring contentHeader = L"Content-Type: application/x-www-form-urlencoded\r\n";
                    BOOL contentHeaderOk = HttpAddRequestHeaders(hData,
                        contentHeader.c_str(),
                        contentHeader.size(),
                        0);
                    _ASSERTE(contentHeaderOk);

                    postDataUTF8 = WideToUTF8(BuildQueryString(*postParameters));
                    _TRACE("POST DATA: %S", postDataUTF8.c_str());
                }

                BOOL sendOk = HttpSendRequest( hData, NULL, 0, (LPVOID)(postDataUTF8.size() > 0 ? postDataUTF8.c_str() : NULL), postDataUTF8.size());
                _ASSERTE(sendOk);

                // TODO dynamically allocate return buffer
                BYTE buffer[1024*32] = {};
                DWORD dwRead = 0;
                while(
                    InternetReadFile( hData, buffer, SIZEOF(buffer) - 1, &dwRead ) &&
                    dwRead > 0
                    )
                {
                    buffer[dwRead] = 0;
                    result += UTF8ToWide((char*)buffer);
                }

                _TRACE("%s", result.c_str());

                InternetCloseHandle(hData);
            }
            InternetCloseHandle(hConnection);
        }
        InternetCloseHandle(hINet);
    }

    return result;
}

Another minor cosmetic change, I extracted the OAuth header parameter formatting out into its own function OAuthBuildHeader.

Farther down you can see in the call to HttpOpenRequest I am no longer blindly passing "GET", and instead am passing in the new httpMethod parameter value. Currently "GET" and "POST" are the only values likely to work.

And finally, just before calling HttpSendRequest, we again check to see if we’re processing a "POST" request, and if so, we take care of a couple of tasks.

First, we need to add a header indicating the MIME type of the data that we’ll be posting, in this case application/x-www-form-urlencoded which is the data format used for submitting forms on websites.

Next we need to format the POST parameters into the application/x-www-form-urlencoded data format, which is simple. It’s essentially the same as the query part of an URL used in a GET request (var1=blah&var2=foo&var3=john). This is taken care of by the helper function BuildQueryString:

// parameters must already be URL encoded before calling BuildQueryString
wstring BuildQueryString( const HTTPParameters &parameters )
{
    wstring query;

    for(HTTPParameters::const_iterator it = parameters.begin();
        it != parameters.end();
        ++it)
    {
        _TRACE("%s = %s", it->first.c_str(), it->second.c_str());

        if(it != parameters.begin())
        {
            query += L"&";
        }

        wstring pair;
        pair += it->first + L"=" + it->second + L"";
        query += pair;
    }
    return query;
}

The resulting query string must then be UTF8 encoded. When we send the query string as part of a GET request, the WinInet APIs convert the entire URL to UTF8 for us, parameters and all. Not so with POST data, which WinInet treats as raw binary and will pass along unchanged.

I forgot to do this the first time, and just sent my wide string data buffer as the POST data. This resulted in an invalid signature error from Twitter since the data was UTF8 encoded as part of the signing process and therefor the signature didn’t match the raw wide string data that I sent with the POST (and which wouldn’t have worked anyway since the twitter API would almost certainly only accept UTF8 encoded POST data).

Last but not least, we pass the UTF8 encoded form data string to HttpSendRequest, or pass NULL if there is no data to send.

Tweeted

And that should be it. Assuming everything has gone right so far, you have just sent a tweet out into the twitterverse. The return value from the call (if it succeeds) should be a copy of the full tweet in XML format. One thing to note while testing is that (according to the Twitter API docs) multiple tweets in a row with exactly the same text will be ignored, so make sure to change it up each time.

Since UrlEncode converts to UTF8 as an intermediate format as part of the encoding process (when we first added the status text to the HTTPParameters object), Unicode status text should work fine. I tested it with Japanese and had no problems, but I had to hard code the string as I wasn’t able to enter Japanese into my console window. A utility I found in order to do the test without saving my source file in Unicode is Richard Ishida’s Unicode Code Converter, which looks super handy for a variety of Unicode related tasks.

Advertisements
This entry was posted in C++, Twitter, Win32 and tagged , , , , , , . Bookmark the permalink.

19 Responses to Twitter OAuth in C++ for Win32 – Part 3 – Updating Twitter Status

  1. Pingback: Twitter OAuth in C++ for Win32 – Part 2 | code:brook

  2. tony says:

    Well done !
    Do you know if it can work on facebook plateform ?

    • Brook Miles says:

      No, I don’t believe it will work on Facebook. I’m not that familiar with Facebook’s implementation, however my understanding is that it’s based on OAuth 2.0, whereas Twitter’s is based on OAuth 1.0a.

      That being said, OAuth 2.0 appears to be quite a bit simpler and uses HTTPS for security instead of the complex message signing involved in OAuth 1.0a.

  3. zoltan says:

    Hi,
    can you send a contact e-mail for me? I have to ask something about this code.

    Thanks.
    zoltan

  4. zoltan says:

    I want to use your code in my project and I need your help in c++. So “personal” question 🙂

  5. Nick says:

    Hi Brook,
    Just wanted to drop you a line saying THANKYOU for posting this whole Part1 -> Part3 saga. I have been converting a twitter plugin for miranda IM from basic auth to OAuth, and without your articles i can definitely say it wouldn’t have happened. I hope you don’t mind but I used a lot of your code to make my plugin work 🙂
    Nick

    • Brook Miles says:

      Great, glad it was helpful. I hope you reviewed the code you used for buffer overflows and proper error handling, etc… because I didn’t 🙂

      • Nick says:

        yeah most of your
        wstring OAuthWebRequestSignedSubmit(
        const HTTPParameters& oauthParameters,
        const wstring& url,
        const wstring& httpMethod,
        const HTTPParameters* postParameters
        )

        function i butchered to work with the miranda API, so i hope that should eliminate most of the potential problems… right? 😉

  6. Ryan says:

    Hi,
    I can’t find your code download anywhere on your google code page 😦

    • Brook Miles says:

      Ryan,

      The code is accessible via a Subversion (SVN) repository. On the Google Code page, click the Source tab, there you can see the command line to check out the code using SVN, or you can switch to the Browse sub-tab under Source to see the structure of the repository or view individual files.

      If you’d prefer a GUI interface, you might try Tortoise SVN, which integrates with windows explorer.

  7. kaushik says:

    Hi,

    I am planning to used this source code for my project, but my server is HTTPs, what are changes i have to do to implement HTTPs instead of HTTP?

    Thanks,
    kaushik

    • Brook Miles says:

      Under this comment…
      // TODO add support for HTTPS requests

      You’ll find a call to InternetConnect. Basically I think the only change you’ll need to make is to pass the value components.nScheme (which results from InternetCrackUrl) to InternetConnect instead of the hard coded value of INTERNET_SERVICE_HTTP.

      I haven’t tried it but it should be enough, barring bugs in other areas.

  8. Andy says:

    Hi,
    Where can I download source code of this project (with libraries, examples, …)?

  9. Jagadish says:

    Hi,
    How to authenticate twitter for getting access tokens with out having browser option?

  10. Pingback: Twitter OAuth in C++ for Win32 – Part 2 | Brook Miles

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s