Twilio requires a few things that I didn’t know about when I started this project. One is connecting to an HTTP server in a C++ program, another is SSL for HTTP in said C++ program, and the third is website authentication, again in C++. What this all boils down to is that I just know nothing about making HTTPS requests from a C++ program.

I looked up cUrl and that third-party free software seemed like it would do the trick. Boy was I wrong. Apparently cUrl and the people that work on it don’t care about Microsoft systems. This is always strange to me since a majority of all computers systems run Microsoft Windows. So I get a slightly older cUrl library that had .lib files for linking and .dll files for actual executable code, and I fit it all into my project. As soon as I ran my program, it got an error that I didn’t have the right dll. So I did this same thing a few times until I had all of the dll files that I needed and then my program did what is essentially a crash. The operating system spits out a cryptic error that the program cannot start for some reason that was only specified by a number.

I could not find a cUrl distribution that had Visual Studio project files for building cUrl but I didn’t want to have six dll files stuck in my project anyhow.

Low and behold, Microsoft provides WinHTTP. This is a set of functions that are available in a dll that is part of Windows. The next problem was getting the calls right to access the HTTPS server and handle both the security certificate issue on the remote site and the login requirements of the remote site.

I found Visual Basic code that someone write to deal with HTTPS. I then found C++ code to handle regular HTTP connections as well as a few bits of documentation on Twilio that led me to find the few functions that were not obviously needed in the samples that I was using. In other words, one sample showed basic HTTP connectivity, one showed changes to make GHTTPS work, and a bit of documentation showed how to handle the authentication with a user name and password.

Here is the code. Sorry for the MFC strings but I’m too lazy to switch to not using MFC at this point in the project:

#include "Winhttp.h"

...

bool CHandler::SendSMSToTwilio( const char *pTo, const char *pMessage )
{
    //Variables 
    HINTERNET hSession = 0;
    HINTERNET hConnect = 0;
    HINTERNET hRequest = 0;

    CString Content = "From=";
    Content += urlencode( m_SMSFromNumber );
    Content += "&To=";
    Content += urlencode( pTo );
    Content += "&Body=";
    Content += urlencode( pMessage );

    CString RequestedPage;
    RequestedPage.Format( DEFAULT_TWILIO_SEND_PAGEFORMAT, (const char*)m_TwilioUser );
    AddLogEntry( LOG_TO_DEBUGLOG, CLogQueue::NONE, 0, "Accessing Twilio page %s", (const char*)RequestedPage );

    wchar_t *pServer = ConvertToWideChar( DEFAULT_TWILIO_SEND_SERVER );
    wchar_t *pPage = ConvertToWideChar( RequestedPage );
    wchar_t *pUser = ConvertToWideChar( m_TwilioUser );
    wchar_t *pPassword = ConvertToWideChar( m_TwilioPassword );

    // Use WinHttpOpen to obtain a session handle.
    hSession = WinHttpOpen( L"AutoResponder/1.0",  
                            WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
                            WINHTTP_NO_PROXY_NAME, 
                            WINHTTP_NO_PROXY_BYPASS, 0 );

    // Specify an HTTPS server.
    if (hSession)
        hConnect = WinHttpConnect( hSession, pServer,
                                   INTERNET_DEFAULT_HTTPS_PORT, 0);

    // Create an HTTPS request handle.
    if (hConnect)
        hRequest = WinHttpOpenRequest( hConnect, L"POST", pPage,
                                       NULL, WINHTTP_NO_REFERER, NULL, WINHTTP_FLAG_SECURE );

    BOOL Result = FALSE;
    if( hRequest )
    {
        WinHttpSetCredentials( hRequest, WINHTTP_AUTH_TARGET_SERVER,
                               WINHTTP_AUTH_SCHEME_BASIC,
                               pUser, pPassword, 0 );

        // Set to ignore cetificate stuff. We trust who we call.
        DWORD Flags = SECURITY_FLAG_IGNORE_UNKNOWN_CA | SECURITY_FLAG_IGNORE_CERT_CN_INVALID;
        WinHttpSetOption( hRequest, WINHTTP_OPTION_SECURITY_FLAGS, &Flags, sizeof( Flags ) );

        // Twilio uses url-encoding, not form-data.
        WinHttpAddRequestHeaders( hRequest, 
                                  L"Content-Type:application/x-www-form-urlencoded", 
                                  -1L,  WINHTTP_ADDREQ_FLAG_ADD );

        Result = WinHttpSendRequest( hRequest,
                                     WINHTTP_NO_ADDITIONAL_HEADERS,
                                     0, (char*)(const char*)Content, Content.GetLength(), 
                                     Content.GetLength(), 0 );
    }

    if( pServer != 0 ) delete [] pServer; pServer= 0;
    if( pPage != 0 ) delete [] pPage; pPage = 0;
    if( pUser != 0 ) delete [] pUser; pUser = 0;
    if( pPassword != 0 ) delete [] pPassword; pPassword = 0;

    if( Result )
        Result = WinHttpReceiveResponse( hRequest, 0 );

    static const int RESPONSE_BUFFER_SIZE = 1024;
    char ResponseBuffer[RESPONSE_BUFFER_SIZE];

    CString ResponseXML;

    // Keep checking for data until there is nothing left.
    if( Result )
    {
        while( true )
        {
            DWORD dwSize = 0;
            if( !WinHttpQueryDataAvailable( hRequest, &dwSize ) )
            {
                AddLogEntry( LOG_TO_DEBUGLOG, CLogQueue::NONE, 0, "Error %u in WinHttpQueryDataAvailable.", GetLastError() );
                break;
            }

            if( dwSize == 0 )
                break;

            while( dwSize > 0 )
            {
                DWORD Count = min( RESPONSE_BUFFER_SIZE, dwSize );
                DWORD BytesRead = 0;
                if( !WinHttpReadData( hRequest, (LPVOID)ResponseBuffer, dwSize, &BytesRead ) )
                {
                    AddLogEntry( LOG_TO_DEBUGLOG, CLogQueue::NONE, 0, "Error %u in WinHttpReadData.", GetLastError() );
                    break;
                }
                if( BytesRead == 0 )
                    break;

                ResponseXML.Append( ResponseBuffer, BytesRead );

                dwSize -= BytesRead;
            }
        }
    }

    if( hRequest ) WinHttpCloseHandle( hRequest );
    if( hConnect ) WinHttpCloseHandle( hConnect );
    if( hSession ) WinHttpCloseHandle( hSession );

    if( Result )
    {
        if( ResponseXML.IsEmpty() )
            AddLogEntry( LOG_TO_DEBUGLOG, CLogQueue::NONE, 0, "Empty response or corrupt response from Twilio server for outgoig SMS message request." );
        else
        {
            QuickXMLNode Message;
            if( !Message.Parse( ResponseXML ) )
            {
                AddLogEntry( LOG_TO_DEBUGLOG, CLogQueue::NONE, 0, "Unrecognized XML response to SMS request from Twilio server." );
                return false;
            }

            QuickXMLNode *pRoot = Message.FindChildByName( "TwilioResponse" );
            if( pRoot == 0 )
            {
                AddLogEntry( LOG_TO_DEBUGLOG, CLogQueue::NONE, 0, "Unrecognized response to SMS request from Twilio server." );
                return false;
            }

            QuickXMLNode *pRestException = pRoot->FindChildByName( "RestException" );
            if( pRestException == 0 )
            {
                /*
                 * This is the only place where we return true from this function!
                 * No exception means it must have worked.
                 */

                return true;
            }

            CString String = "unknown";
            QuickXMLNode *pStatus = pRestException->FindChildByName( "Status" );
            if( pStatus != 0 )
            {
                if( pStatus->GetFirstChild() != 0 )
                    String = pStatus->GetFirstChild()->GetText();
                String.TrimRight();
                String.TrimLeft();
            }

            AddLogEntry( LOG_TO_DEBUGLOG, CLogQueue::NONE, 0, "Twilio returned status %s when making the SMS text message request. The error message is...", (const char*)String );

            QuickXMLNode *pMessage = pRestException->FindChildByName( "Message" );
            if( pMessage != 0 )
            {
                if( pMessage->GetFirstChild() != 0 )
                    String = pMessage->GetFirstChild()->GetText();
                String.TrimRight();
                String.TrimLeft();
            }

            AddLogEntry( LOG_TO_DEBUGLOG, CLogQueue::NONE, 0, String );
        }
    }
    else
        AddLogEntry( LOG_TO_DEBUGLOG, CLogQueue::NONE, 0, "Failure trying to connect or send SMS request to Twilio server." );

    return false;
}

Comments are few in this bit of code. A lot of it needs no comment since function calls like WinHTTPOpen() need no comment. They are pretty clear as-is.

The logging and XML parsing are of my own making. The url encoding function is not shown but I got it from the internet anyhow. Error checking is minimal but there is some.

I wasn’t sure exactly how to handle reading back the server response. I changed the code that I started with because it would allocate a buffer for each read operation. I prefer to have a buffer on the stack and read in chunks until I have everything I need. I don’t know if the server would ever send back the response so that I ended up with multiple successful non-zero calls to WinHttpQueryDataAvailable() but it is handled anyhow.

I ignore remote site security errors although Twilio might have a real security certificate that is know to be good. I don’t really care since the I know that the site is secure and is safe to use.

I hope this helps someone out there. I had to figure out a few bits on my own but I was glad that a lot of this was already documented in various ways on the web.