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
- 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
. - 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 theCURLOPT_CAINFO
option or a certificate directory can be specified with theCURLOPT_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.
<cfhttp>
call is correctly implementing all the variousCURLOPT
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