5
votes

I'm using a nice Twitter API wrapper: codebird-js; and it includes a proxy in the event that you need to make CORS AJAX requests (I do) because Twitter's API does not allow CORS.

Aside: Would Twitter be ok with this proxying? I have to assume so, since Codebird is listed among their recommended libraries.

The proxy goes through one of the developer's servers, which is nice enough of them, but I've noticed that it occasionally goes down for hours, sometimes a full day. This won't be acceptable once my app goes to production, so I need to self-host the proxy to have more control.

Fortunately, they provide the source for the proxy, as well. Unfortunately, PHP isn't an option. So I'm attempting to port it to CFML, which is the best of my current options (I could also consider Node.js and Ruby, though I'm less familiar with both, which is why I've opted for CFML for now)

Essentially what it boils down to is that I'm attempting to port this script to CFML. Below is what I have so far, but I'm having issues that I'll describe below the code.

<cfscript>
    try{
        header(name="Access-Control-Allow-Origin", value="*");
        header(name="Access-Control-Allow-Headers", value="Origin, X-Authorization");
        header(name="Access-Control-Allow-Methods", value="POST, GET, OPTIONS");

        method = cgi.request_method;
        if (method == 'OPTIONS'){
            abort;
        }

        path = 'https://api.twitter.com' & cgi.path_info;
        headers = [{name="Expect", value=""}];
        req_headers = getHTTPRequestData().headers;
        req_body = getHTTPRequestData().content;

        if (isBinary(req_body)){
            req_body = charsetEncode(req_body, "UTF-8");
        }

        if (structKeyExists(req_headers, 'X-Authorization')){
            arrayAppend(headers, { name='Authorization', value=req_headers['X-Authorization'] });
        }

        response = http_wrapper(method, path, headers, req_body);
        code = val( response.statusCode );
        msg = trim( replace(response.statusCode, code, '') );

        twitter_headers = listToArray(structKeyList( response.responseHeader ));
        for (i = 1; i <= arrayLen(twitter_headers); i++){
            if (twitter_headers[i] == 'set-cookie'){ continue; }
            header(name=twitter_headers[i], value=response.responseHeader[twitter_headers[i]]);
        }
        header(statusCode=code, statusText=msg);
        respond(response.filecontent);
    }catch(any e){
        application.bugService.notifyService(
            message = "Error in Twitter Proxy"
            ,severityCode = "ERROR"
            ,exception = e
        );
        header(statusCode=500,statusText="Proxy Error");
        writeDump(var={error=e,twitter_response=response}, format='text');
    }
</cfscript>

<cffunction name="http_wrapper">
    <cfargument name="method" />
    <cfargument name="path" />
    <cfargument name="headers" />
    <cfargument name="body" />

    <cfset var local = {} />

    <cfhttp method="#arguments.method#" url="#arguments.path#" result="local.result">
        <cfloop from="1" to="#arrayLen(arguments.headers)#" index="local.i">
            <cfhttpparam type="header" name="#arguments.headers[i].name#" value="#arguments.headers[i].value#" />
        </cfloop>
        <cfhttpparam type="body" value="#arguments.body#" />
    </cfhttp>

    <cfreturn local.result />
</cffunction>

<cffunction name="header">
    <cfheader attributeCollection="#arguments#" />
</cffunction>

<cffunction name="respond">
    <cfargument name="body" />
    <cfcontent reset="true" /><cfoutput>#body#</cfoutput><cfabort/>
</cffunction>

Problems

  1. The original source appears (maybe? my php is rusty...) to include a header named Expect when forwarding the request to Twitter, with an empty value as far as I can tell. If I include this header, I get this response: 417 Expectation Failed.
  2. If I instead leave off the Expect header, I'm getting a 401 response for my proxied request.

It's important to note that I'm using client code that has been tested against the provided PHP proxy and works fine. In order to use my own proxy, I've set up my Codebird instance as follows:

var cb = new Codebird;
cb.setProxy('https://mydomain.com/api/v1/proxy/twitter.cfm/');
cb.setConsumerKey(key, secret);

The network request looks like this in chrome debug tools:

Request URL: https://mydomain.com/api/v1/proxy/twitter.cfm/oauth/request_token
Request Method: POST
Status Code: 401 Unauthorized
Request Headers
Accept: */*
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8
Connection: keep-alive
Content-Length: 61
Content-Type: application/x-www-form-urlencoded
Host: mydomain.com
Origin: null
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.95 Safari/537.36
X-Authorization: OAuth oauth_consumer_key="..........", oauth_nonce="PqK4KPCc", oauth_signature="B%2BQ..............b08%3D", oauth_signature_method="HMAC-SHA1", oauth_timestamp="1376507615", oauth_version="1.0"
Form Data:
oauth_callback: http://mydomain.com/twitter-login

You can see that the X-Authorization header is included in the request and, to the best of my knowledge, is complete and correct, and in the first code sample you can see that this request header is then forwarded to Twitter as the Authorization header.

I can't figure out why I'm getting 401 responses. The documentation doesn't seem to indicate that an Expect header is necessary, so I'm assuming I can ignore it. (Indeed, to the best of my admittedly rusty PHP knowledge, it's not actually being sent in the PHP version either...)

But if that's the case, why am I getting 401 back? Everything else seems correct to me...

Update: CURL Options

As per Adam's comments, I forgot to note that I also knowingly sort of ignored the CURL options being set (I assumed CFHTTP would take care of all of that for me). I'll now go through and document each one being used and make sure that base is covered in CFHTTP:


curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);

TRUE to return the transfer as a string of the return value of curl_exec() instead of outputting it out directly.

This is a poorly written bit of docs (something CF is occasionally guilty of, too), but seems to indicate that with this option set, the result will be returned instead of appended to the response buffer. Check; CFHTTP does that by default.


curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 0);

TRUE to follow any "Location: " header that the server sends as part of the HTTP header (note this is recursive, PHP will follow as many "Location: " headers that it is sent, unless CURLOPT_MAXREDIRS is set).

The equivalent CFHTTP setting is redirect="true" (true by default). This is, then, a mismatch since the PHP version is saying not to follow redirects. I'll keep this in mind but severely doubt this is causing my 401.


curl_setopt($ch, CURLOPT_HEADER, 1);

TRUE to include the header in the output.

Check, CFHTTP includes headers in the responses.


curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);

FALSE to stop cURL from verifying the peer's certificate. Alternate certificates to verify against can be specified with the CURLOPT_CAINFO option or a certificate directory can be specified with the CURLOPT_CAPATH option.

CFHTTP does verify SSL certificates, and as I noted in the comments, it seems like the necessary cert is already imported.


curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);

1 to check the existence of a common name in the SSL peer certificate. 2 to check the existence of a common name and also verify that it matches the hostname provided. In production environments the value of this option should be kept at 2 (default value).

I'm not positive what this is checking, but I don't believe CFHTTP has any correlating setting, so I'll assume for now that this check is being done (or is not a contributing factor to my issue).


curl_setopt($ch, CURLOPT_CAINFO, __DIR__ . '/cacert.pem');

The name of a file holding one or more certificates to verify the peer with. This only makes sense when used in combination with CURLOPT_SSL_VERIFYPEER.

As I mentioned previously and in the comments, the PEM file provided with the proxy source is already included in my configuration and being verified.


curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);

An array of HTTP header fields to set, in the format array('Content-type: text/plain', 'Content-length: 100')

Check: I'm passing headers on to Twitter.


curl_setopt($ch, CURLINFO_HEADER_OUT, 1);

TRUE to track the handle's request string.

What? Based on the setting name I assume this means to include the headers in the output (check, CFHTTP does that), but the documentation makes no sense.


So this seems to rule out the curl settings as a possible problem.

2
Can you use a sniffer like WireShark to compare the exact HTTP requests sent by the PHP library and your CFML code to search for differences. You can also replay HTTP requests in a tool like MS Fiddler to change the differences until you find the offending one.Brad Wood
I'd have to be able to get in the middle of the request between PHP and Twitter to sniff it, and that I can't do unfortunately.Adam Tuttle
If the request is running from your machine to Twitter's server, you should be able to run the sniffer on the machine hosting your code.Brad Wood
Sure, I could easily sniff the one coming from my port of the code... but I can't get in the middle of the 3rd party PHP proxy server and sniff its traffic for comparison purposes.Adam Tuttle
Its' not just that you haven't got the SSL certs installed on CF is it? And have you verified that the <cfhttp> call is correctly implementing all the various CURLOPT settings the PHP is? That's the only significant difference I can see between the two (although I'm inferring a lot of the PHP stuff, as I don't even know enough PHP to be rusty at it!)Adam Cameron

2 Answers

1
votes

I'm not going to accept this as the answer, since the question was about porting the code, and this most certainly doesn't fix the problems I had with the converted code. But I thought it would be useful for anyone else in the same predicament, so I'd log what I did, and why, here.


Today I remembered that Heroku supports PHP, so I set up an instance of the original PHP project there and it's working perfectly. I'm still puzzled by my CFML port not working, but at least I can continue on with my project knowing that I'll be able to use my Heroku-hosted proxy and that its uptime is much more secure than some random website.

At the end of the day, while I have some (but not complete) trust in the host to not log & make malicious use of my twitter credentials, having them in the middle just made me slightly uneasy. The posted source code in the github repo looks good, though -- and I've got nothing against PHP as long as it works! The root of my problem was 99% about uncontrollable uptime, 1% about security.

Doesn't hurt that it can run on a single free Heroku dyno indefinitely, too. <3 Heroku

In case anyone comes across this and wants to do the same, here are the steps I followed (command line). Note, you must first have the Heroku toolbelt installed.

$ git clone https://github.com/mynetx/codebird-cors-proxy.git twitter-cors-proxy-php
Cloning into 'twitter-cors-proxy-php'...
remote: Counting objects: 52, done.
remote: Compressing objects: 100% (34/34), done.
remote: Total 52 (delta 23), reused 47 (delta 18)
Unpacking objects: 100% (52/52), done.

$ cd twitter-cors-proxy-php
$ rm -rf .git
$ ls -al
total 88
drwxr-xr-x   7 adam  staff    238 Aug 15 11:14 .
drwxr-xr-x  57 adam  staff   1938 Aug 15 11:11 ..
-rw-r--r--   1 adam  staff    250 Aug 15 11:11 CHANGELOG
-rw-r--r--   1 adam  staff  35147 Aug 15 11:11 LICENSE
-rw-r--r--   1 adam  staff   1531 Aug 15 11:11 README.md
drwxr-xr-x   5 adam  staff    170 Aug 15 11:11 src

$ rm -f CHANGELOG LICENSE README.md
$ mv src/* ./
$ rm -rf src
$ mv codebird-cors-proxy.php index.php
$ git init
Initialized empty Git repository in /Users/adam/DEV/twitter-cors-proxy-php/.git/

$ git add .
$ git st
## Initial commit on master
A  cacert.pem
A  index.php

$ git commit -am"initial import from codebird"
[master (root-commit) 4196e98] initial import from codebird
 2 files changed, 4115 insertions(+)
 create mode 100644 cacert.pem
 create mode 100644 index.php

$ heroku apps:create twitter-cors-proxy
Creating twitter-cors-proxy... done, stack is cedar
http://twitter-cors-proxy.herokuapp.com/ | [email protected]:twitter-cors-proxy.git
Git remote heroku added

$ git push heroku master
Counting objects: 4, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (4/4), done.
Writing objects: 100% (4/4), 134.24 KiB, done.
Total 4 (delta 0), reused 0 (delta 0)

-----> PHP app detected
-----> Bundling mcrypt version 2.5.8
-----> Bundling Apache version 2.2.25
-----> Bundling PHP version 5.3.27
-----> Discovering process types
       Procfile declares types -> (none)
       Default types for PHP   -> web

-----> Compiled slug size: 22.3MB
-----> Launching... done, v3
       http://twitter-cors-proxy.herokuapp.com deployed to Heroku

To [email protected]:twitter-cors-proxy.git
 * [new branch]      master -> master

Then I just set my new proxy into Codebird with:

var cb = new Codebird;
cb.setProxy('https://twitter-cors-proxy.herokuapp.com/index.php');

...and all was right with the world!

0
votes

Not sure how much you have looked into the twitter API, but they have dropped supported for public request everything now has to be authorised using a registered app. You can register one here dev.Twitter

I can see what you are trying to achieve here, I have a basic CFC that demos a working http call to the twitter API, add your keys and secrets in, and you can change the twitterEndpoint variable to different options and see the different responses. - Hope this helps.

Basic Twitter CFC - Note this has only been testing on OpenBD CFML Engine