Twitter OAuth in C++ for Win32 - Part 2

Part 1 covered the OAuth process at a high level, and Part 2 goes over the example code in what will likely turn out to be too much detail.

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

Update: Part 3 builds on the code in this section.

Making OAuth Requests

Now lets take a look at what OAuthWebRequest actually does. The steps are essentially the same for any request, however not all of the parameters are used all the time.

wstring OAuthWebRequest(
    const wstring& url,
    const wstring& httpMethod,
    const wstring& consumerKey,
    const wstring& consumerSecret,
    const wstring& oauthToken = L"",
    const wstring& oauthTokenSecret = L"",
    const wstring& pin = L""
    )
{
    wstring query = UrlGetQuery(url);
    OAuthParameters originalParameters = ParseQueryString(query);

    OAuthParameters oauthSignedParameters = BuildSignedOAuthParameters(
        originalParameters,
        url, httpMethod,
        consumerKey, consumerSecret,
        oauthToken, oauthTokenSecret,
        pin );
    return OAuthWebRequestSubmit(oauthSignedParameters, url);
}

The first step is pretty basic, we call UrlGetQuery which simply extracts the “query” part from the provided URL, if any. None of the URLs we are using for this example actually need any parameters, but I stuck some extras in one of them just to prove this code works:

twitter.com/oauth/request_token*?some_other_parameter=hello&another_one=goodbye**#meep*

The unused parameters will just be ignored by the API, but since they’re there, they need to be properly included in the OAuth signature process.

ParseQueryString splits the query section of the URL up into invidual key/value pairs and stores them in an OAuthParameters, which is simply a typedef for a map of strings:

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

We pass these parameters (if any) to BuildSignedOAuthParameters which generates all of the additional information that makes an OAuth request OAuth and not just another HTTP call.

OAuthParameters BuildSignedOAuthParameters(
    const OAuthParameters& requestParameters,
    const wstring& url,
    const wstring& httpMethod,
    const wstring& consumerKey,
    const wstring& consumerSecret,
    const wstring& requestToken = L"",
    const wstring& requestTokenSecret = L"",
    const wstring& pin = L"" )
{
    wstring timestamp = OAuthCreateTimestamp();
    wstring nonce = OAuthCreateNonce();

    // create oauth requestParameters
    OAuthParameters oauthParameters;

    oauthParameters[L"oauth_timestamp"] = timestamp;
    oauthParameters[L"oauth_nonce"] = nonce;
    oauthParameters[L"oauth_version"] = L"1.0";
    oauthParameters[L"oauth_signature_method"] = L"HMAC-SHA1";
    oauthParameters[L"oauth_consumer_key"] = consumerKey;

    // add the request token if found
    if (!requestToken.empty())
    {
        oauthParameters[L"oauth_token"] = requestToken;
    }

    // add the authorization pin if found
    if (!pin.empty())
    {
        oauthParameters[L"oauth_verifier"] = pin;
    }

    // create a parameter list containing both oauth and original parameters
    // this will be used to create the parameter signature
    OAuthParameters allParameters = requestParameters;
    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;
}

The first couple of steps are pretty straightforward, OAuthCreateTimestamp simply formats the current time value in seconds as a string, and OAuthCreateNonce creates a nonce value, a random string of characters used only for this request.

wstring OAuthCreateNonce()
{
    wchar_t ALPHANUMERIC[] = L"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
    wstring nonce;

    for(int i = 0; i <= 16; ++i)
    {
        nonce += ALPHANUMERIC[rand() % (SIZEOF(ALPHANUMERIC) - 1)]; // don't count null terminator in array
    }
    return nonce;
}

wstring OAuthCreateTimestamp()
{
    __time64_t utcNow;
    __time64_t ret = _time64(&utcNow);
    _ASSERTE(ret != -1);

    wchar_t buf[100] = {};
    swprintf_s(buf, SIZEOF(buf), L"%I64u", utcNow);

    return buf;
}

Next, BuildSignedOAuthParameters collects the nonce and timestamp value, along with the OAuth version we’re using, the signature method (Twitter only accepts the HMAC-SHA1 signature method), and our Consumer Key. In addition, if requestToken is set (may contain either a Request Token or Access Token) we add that into the list; and if a PIN is provided for authorization, we add that as well.

We currently have two lists of parameters, one that contains anything that was included in the original URL, and one that contains all of the needed OAuth values. We create a new set of parameters called allParameters, which includes both lists.

Now that we have a master list of parameters, we begin constructing a Signature Base String, according to the OAuth specification. Basically this is a single string that combines the URL, the query and OAuth parameters, and the HTTP method (“GET” or “POST”) in a known and re-producable format. The server is going to perform all of the same steps in an attempt to create exactly the same string, and validate our signature. If even a single character is the wrong case, missing, or out of place, then the whole signature will be invalidated, and the request will be rejected by the server.

We pass our master parameter list to OAuthNormalizeRequestParameters which creates a single string containing all of the key/value pairs, in sorted order.

wstring OAuthNormalizeRequestParameters( const OAuthParameters& requestParameters )
{
    list<wstring> sorted;
    for(OAuthParameters::const_iterator it = requestParameters.begin();
        it != requestParameters.end();
        ++it)
    {
        wstring param = it->first + L"=" + it->second;
        sorted.push_back(param);
    }
    sorted.sort();

    wstring params;
    for(list<wstring>::iterator it = sorted.begin(); it != sorted.end(); ++it)
    {
        if(params.size() > 0)
        {
            params += L"&";
        }
        params += *it;
    }

    return params;
}

We also need to normalize the URL, which consists of stripping off the query section, and making sure the port number is included if it’s non-standard (eg. not 80 for HTTP), and NOT included if it IS standard.

wstring OAuthNormalizeUrl( const wstring& url )
{
    wchar_t scheme[1024*4] = {};
    wchar_t host[1024*4] = {};
    wchar_t path[1024*4] = {};

    URL_COMPONENTS components = { sizeof(URL_COMPONENTS) };

    components.lpszScheme = scheme;
    components.dwSchemeLength = SIZEOF(scheme);

    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);
    if(crackUrlOk)
    {
        wchar_t port[10] = {};

        // The port number must only be included if it is non-standard
        if((Compare(scheme, L"http", false) && components.nPort != 80) ||
            (Compare(scheme, L"https", false) && components.nPort != 443))
        {
            swprintf_s(port, SIZEOF(port), L":%u", components.nPort);
        }

        // InternetCrackUrl includes ? and # elements in the path,
        // which we need to strip off
        wstring pathOnly = path;
        wstring::size_type q = pathOnly.find_first_of(L"#?");
        if(q != wstring::npos)
        {
            pathOnly = pathOnly.substr(0, q);
        }

        normalUrl = wstring(scheme) + L"://" + host + port + pathOnly;
    }
    return normalUrl;
}

OAuthNormalizeUrl splits apart the URL using InternetCrackUrl, and then recombines only the parts we need.

We’ve got all our normalized information ready, so we can stick it all together in a single string using OAuthConcatenateRequestElements, first the HTTP method, then the normalized URL, then the normalized parameters.

wstring OAuthConcatenateRequestElements( const wstring& httpMethod, wstring url, const wstring& parameters )
{
    wstring escapedUrl = UrlEncode(url);
    wstring escapedParameters = UrlEncode(parameters);

    wstring ret = httpMethod + L"&" + escapedUrl + L"&" + escapedParameters;
    return ret;
}

The URL and parameters need to be URL encoded before they are concatenated. OAuth has specific requirements about how this should be done, namely:

  1. Alphanumeric characters (letters and digits), as well as - . _ ~ (dash, period, underscore, and tilde), MUST NOT be encoded. They must remain as they are. These are confusingly named the Unreserved Characters.
  2. All other characters MUST be encoded.
  3. The resulting encoded hexadecimal values must be in UPPERCASE, %1A, not %1a.
// char2hex and urlencode from http://www.zedwood.com/article/111/cpp-urlencode-function
// modified according to http://oauth.net/core/1.0a/#encoding_parameters
//
//5.1.  Parameter Encoding
//
//All parameter names and values are escaped using the [RFC3986]
//percent-encoding (%xx) mechanism. Characters not in the unreserved character set
//MUST be encoded. Characters in the unreserved character set MUST NOT be encoded.
//Hexadecimal characters in encodings MUST be upper case.
//Text names and values MUST be encoded as UTF-8
// octets before percent-encoding them per [RFC3629].
//
//  unreserved = ALPHA, DIGIT, '-', '.', '_', '~'

string char2hex( char dec )
{
    char dig1 = (dec&0xF0)>>4;
    char dig2 = (dec&0x0F);
    if ( 0<= dig1 && dig1<= 9) dig1+=48;    //0,48 in ascii
    if (10<= dig1 && dig1<=15) dig1+=65-10; //A,65 in ascii
    if ( 0<= dig2 && dig2<= 9) dig2+=48;
    if (10<= dig2 && dig2<=15) dig2+=65-10;

    string r;
    r.append( &dig1, 1);
    r.append( &dig2, 1);
    return r;
}

string urlencode(const string &c)
{

    string escaped;
    int max = c.length();
    for(int i=0; i<max; i++)
    {
        if ( (48 <= c[i] && c[i] <= 57) ||//0-9
            (65 <= c[i] && c[i] <= 90) ||//ABC...XYZ
            (97 <= c[i] && c[i] <= 122) || //abc...xyz
            (c[i]=='~' || c[i]=='-' || c[i]=='_' || c[i]=='.')
            )
        {
            escaped.append( &c[i], 1);
        }
        else
        {
            escaped.append("%");
            escaped.append( char2hex(c[i]) );//converts char 255 to string "FF"
        }
    }
    return escaped;
}

wstring UrlEncode( const wstring& url )
{
    return UTF8ToWide(urlencode(WideToUTF8(url)));
}

We’re almost there. We have our signature base, and we need to sign it, and add the signature to our list of OAuth parameters. To do this we call OAuthCreateSignature.

wstring OAuthCreateSignature( const wstring& signatureBase, const wstring& consumerSecret, const wstring& requestTokenSecret )
{
    // URL encode key elements http://oauth.net/core/1.0/#anchor16
    wstring escapedConsumerSecret = UrlEncode(consumerSecret);
    wstring escapedTokenSecret = UrlEncode(requestTokenSecret);

    wstring key = escapedConsumerSecret + L"&" + escapedTokenSecret;
    string keyBytes = WideToUTF8(key);

    string data = WideToUTF8(signatureBase);
    string hash = HMACSHA1(keyBytes, data);
    wstring signature = Base64String(hash);

    // You must encode the URI for safe net travel
    signature = UrlEncode(signature);
    return signature;
}

To create the key for signing, we URL encode the Consumer Secret, and the Access Token Secret, and then concatenate them with an &. Then we pass this key data, and our signature base that we want to sign, to HMACSHA1 which will create a signature in the form of a binary hash value. The hash value is then Base64 encoded and then URL encoded, before being returned. In the interest of keeping this article from being even longer than it already is, I won’t include the Base64 code here as it is nothing out of the ordinary, you can check it out on the Google Code project in Base64Coder.cpp or the original file provided by Microsoft.

HMACSHA1 gave me the most trouble, as I am not particularly well versed in cryptographic subjects, and don’t recall ever using the Crypto API before. Ultimately, my solution was based on this delightful example on MSDN, Example C Program: Creating an HMAC, with a key change (oh, a pun!) to how the key is created, based on a NetBSD support source file of all places, crypto_cryptoapi.c

Presented for you in all it’s terrible glory, HMACSHA1

string HMACSHA1( const string& keyBytes, const string& data )
{
    // based on http://msdn.microsoft.com/en-us/library/aa382379%28v=VS.85%29.aspx

    string hash;

    //--------------------------------------------------------------------
    // Declare variables.
    //
    // hProv:           Handle to a cryptographic service provider (CSP).
    //                  This example retrieves the default provider for
    //                  the PROV_RSA_FULL provider type.
    // hHash:           Handle to the hash object needed to create a hash.
    // hKey:            Handle to a symmetric key. This example creates a
    //                  key for the RC4 algorithm.
    // hHmacHash:       Handle to an HMAC hash.
    // pbHash:          Pointer to the hash.
    // dwDataLen:       Length, in bytes, of the hash.
    // Data1:           Password string used to create a symmetric key.
    // Data2:           Message string to be hashed.
    // HmacInfo:        Instance of an HMAC_INFO structure that contains
    //                  information about the HMAC hash.
    //
    HCRYPTPROV  hProv       = NULL;
    HCRYPTHASH  hHash       = NULL;
    HCRYPTKEY   hKey        = NULL;
    HCRYPTHASH  hHmacHash   = NULL;
    PBYTE       pbHash      = NULL;
    DWORD       dwDataLen   = 0;
    //BYTE        Data1[]     = {0x70,0x61,0x73,0x73,0x77,0x6F,0x72,0x64};
    //BYTE        Data2[]     = {0x6D,0x65,0x73,0x73,0x61,0x67,0x65};
    HMAC_INFO   HmacInfo;

    //--------------------------------------------------------------------
    // Zero the HMAC_INFO structure and use the SHA1 algorithm for
    // hashing.

    ZeroMemory(&HmacInfo, sizeof(HmacInfo));
    HmacInfo.HashAlgid = CALG_SHA1;

    //--------------------------------------------------------------------
    // Acquire a handle to the default RSA cryptographic service provider.

    if (!CryptAcquireContext(
        &hProv,                   // handle of the CSP
        NULL,                     // key container name
        NULL,                     // CSP name
        PROV_RSA_FULL,            // provider type
        CRYPT_VERIFYCONTEXT))     // no key access is requested
    {
        _TRACE(" Error in AcquireContext 0x%08x \n",
            GetLastError());
        goto ErrorExit;
    }

    //--------------------------------------------------------------------
    // Derive a symmetric key from a hash object by performing the
    // following steps:
    //    1. Call CryptCreateHash to retrieve a handle to a hash object.
    //    2. Call CryptHashData to add a text string (password) to the
    //       hash object.
    //    3. Call CryptDeriveKey to create the symmetric key from the
    //       hashed password derived in step 2.
    // You will use the key later to create an HMAC hash object.

    if (!CryptCreateHash(
        hProv,                    // handle of the CSP
        CALG_SHA1,                // hash algorithm to use
        0,                        // hash key
        0,                        // reserved
        &hHash))                  // address of hash object handle
    {
        _TRACE("Error in CryptCreateHash 0x%08x \n",
            GetLastError());
        goto ErrorExit;
    }

    if (!CryptHashData(
        hHash,                    // handle of the hash object
        (BYTE*)keyBytes.c_str(),                    // password to hash
        keyBytes.size(),            // number of bytes of data to add
        0))                       // flags
    {
        _TRACE("Error in CryptHashData 0x%08x \n",
            GetLastError());
        goto ErrorExit;
    }

    // key creation based on
    // http://mirror.leaseweb.com/NetBSD/NetBSD-release-5-0/src/dist/wpa/src/crypto/crypto_cryptoapi.c
    struct {
        BLOBHEADER hdr;
        DWORD len;
        BYTE key[1024]; // TODO might want to dynamically allocate this, Should Be Fine though
    } key_blob;

    key_blob.hdr.bType = PLAINTEXTKEYBLOB;
    key_blob.hdr.bVersion = CUR_BLOB_VERSION;
    key_blob.hdr.reserved = 0;
    /*
    * Note: RC2 is not really used, but that can be used to
    * import HMAC keys of up to 16 byte long.
    * CRYPT_IPSEC_HMAC_KEY flag for CryptImportKey() is needed to
    * be able to import longer keys (HMAC-SHA1 uses 20-byte key).
    */
    key_blob.hdr.aiKeyAlg = CALG_RC2;
    key_blob.len = keyBytes.size();
    ZeroMemory(key_blob.key, sizeof(key_blob.key));

    _ASSERTE(keyBytes.size() <= SIZEOF(key_blob.key));
    CopyMemory(key_blob.key, keyBytes.c_str(), min(keyBytes.size(), SIZEOF(key_blob.key)));

    if (!CryptImportKey(
        hProv,
        (BYTE *)&key_blob,
        sizeof(key_blob),
        0,
        CRYPT_IPSEC_HMAC_KEY,
        &hKey))
    {
        _TRACE("Error in CryptImportKey 0x%08x \n", GetLastError());
        goto ErrorExit;
    }

    //--------------------------------------------------------------------
    // Create an HMAC by performing the following steps:
    //    1. Call CryptCreateHash to create a hash object and retrieve
    //       a handle to it.
    //    2. Call CryptSetHashParam to set the instance of the HMAC_INFO
    //       structure into the hash object.
    //    3. Call CryptHashData to compute a hash of the message.
    //    4. Call CryptGetHashParam to retrieve the size, in bytes, of
    //       the hash.
    //    5. Call malloc to allocate memory for the hash.
    //    6. Call CryptGetHashParam again to retrieve the HMAC hash.

    if (!CryptCreateHash(
        hProv,                    // handle of the CSP.
        CALG_HMAC,                // HMAC hash algorithm ID
        hKey,                     // key for the hash (see above)
        0,                        // reserved
        &hHmacHash))              // address of the hash handle
    {
        _TRACE("Error in CryptCreateHash 0x%08x \n",
            GetLastError());
        goto ErrorExit;
    }

    if (!CryptSetHashParam(
        hHmacHash,                // handle of the HMAC hash object
        HP_HMAC_INFO,             // setting an HMAC_INFO object
        (BYTE*)&HmacInfo,         // the HMAC_INFO object
        0))                       // reserved
    {
        _TRACE("Error in CryptSetHashParam 0x%08x \n",
            GetLastError());
        goto ErrorExit;
    }

    if (!CryptHashData(
        hHmacHash,                // handle of the HMAC hash object
        (BYTE*)data.c_str(),                    // message to hash
        data.size(),            // number of bytes of data to add
        0))                       // flags
    {
        _TRACE("Error in CryptHashData 0x%08x \n",
            GetLastError());
        goto ErrorExit;
    }

    //--------------------------------------------------------------------
    // Call CryptGetHashParam twice. Call it the first time to retrieve
    // the size, in bytes, of the hash. Allocate memory. Then call
    // CryptGetHashParam again to retrieve the hash value.

    if (!CryptGetHashParam(
        hHmacHash,                // handle of the HMAC hash object
        HP_HASHVAL,               // query on the hash value
        NULL,                     // filled on second call
        &dwDataLen,               // length, in bytes, of the hash
        0))
    {
        _TRACE("Error in CryptGetHashParam 0x%08x \n",
            GetLastError());
        goto ErrorExit;
    }

    pbHash = (BYTE*)malloc(dwDataLen);
    if(NULL == pbHash)
    {
        _TRACE("unable to allocate memory\n");
        goto ErrorExit;
    }

    if (!CryptGetHashParam(
        hHmacHash,                 // handle of the HMAC hash object
        HP_HASHVAL,                // query on the hash value
        pbHash,                    // pointer to the HMAC hash value
        &dwDataLen,                // length, in bytes, of the hash
        0))
    {
        _TRACE("Error in CryptGetHashParam 0x%08x \n", GetLastError());
        goto ErrorExit;
    }

    for(DWORD i = 0 ; i < dwDataLen ; i++)
    {
        hash.push_back((char)pbHash[i]);
    }

    // Free resources.
    // lol goto
ErrorExit:
    if(hHmacHash)
        CryptDestroyHash(hHmacHash);
    if(hKey)
        CryptDestroyKey(hKey);
    if(hHash)
        CryptDestroyHash(hHash);
    if(hProv)
        CryptReleaseContext(hProv, 0);
    if(pbHash)
        free(pbHash);

    return hash;
}

Note in particular the use of CryptImportKey to use the key data we provide, instead of using CryptDeriveKey to create one as is done in the MSDN example.

Phew! The hard stuff is all done. As mentioned earlier, the signed hash is Base64 encoded, then URL encoded, and passed back to BuildSignedOAuthParameters which adds it to the list of OAuth parameters, and returns the final signed list.

All we need to do now is pass the signed list of OAuth parameters, and the original URL to OAuthWebRequestSubmit to actually make the HTTP connection, and get the reply.

wstring OAuthWebRequestSubmit(
                              const OAuthParameters& parameters,
                              const wstring& url
                              )
{
    _TRACE("OAuthWebRequestSubmit(%s)", url.c_str());

    wstring oauthHeader = L"Authorization: OAuth ";

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

        if(it != parameters.begin())
        {
            oauthHeader += L",";
        }

        wstring pair;
        pair += it->first + L"=\"" + it->second + L"\"";
        oauthHeader += pair;
    }
    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)
        {
            // TODO add support for handling POST requests
            HINTERNET hData = HttpOpenRequest( hConnection,
                L"GET",
                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 addHeadersOk = HttpAddRequestHeaders(hData,
                    oauthHeader.c_str(),
                    oauthHeader.size(),
                    0);
                _ASSERTE(addHeadersOk);

                BOOL sendOk = HttpSendRequest( hData, NULL, 0, NULL, 0);
                _ASSERTE(sendOk);

                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;
}

Notice that the OAuth parameters are all provided as part of a custom header named “Authorization: “. The query parameters, while they were included in the OAuth signing process, are not sent as part of the OAuth custom header, they are sent as usual in the path of the “GET” request (or in the body of the request if using “POST”).

The result is read into a buffer, and then returned. The result will be truncated at 32KB because I was lazy. You’ll want to allocate it dynamically. The example code also doesn’t deal with any of the possible HTTP responses, or other WinInet error codes.

The End

And that’s all there is to it! Ok, it was a bit longer than I expected. The response you get back from the web request, obviously depends on what URL you requested. You might get back a Request Token, or an Access Token, the user’s feed, or possibly an error code or other result.

The Twitter API Documentation awaits.

Update: Part 3 covers POST requests and updating your twitter status.