Posts Tagged ‘tunnel’

Dynamic Tunneling for your Facebook App

Sunday, April 19th, 2009

When I was launching Forgiveness, my first attempt at a Facebook app via RoR, I ran into one of the typical developer quandries. How the heck to I actually make local revs to the app and proxy them through the FB back-end while still keeping the app running live for my millions of satisfied customers?

I sought advice from Steve Enzer, and his answer was ‘create a separate FB app id’. Yep, you can do that. I’m sure it works just fine. However, I wanted to come up with a single-app solution, you know, just because.

My app backbone was based upon Facebooker, a great gem for just such purposes. And assuming that you’re using that gem, you’re provided with:

rake facebooker:tunnel:start

Which starts up the traditional server tunnel to your local dev box; Facebook’s proxy won’t know the difference. I’ve also set up my canvas page URLs and the like with a fixed IP — according to FB’s best practices, because they don’t want to deal with all the DNS resolution stuff — and it listens on port 80 with a dedicated context. So, I’ve got my facebooker.yml set up for port 3100:

tunnel:
  public_host: cantremember.com
  public_port: 3100
  local_port: 3100

And here’s my nginx config:

#
#	included into vhosts/localhost.conf (*.by-ip)
#		answers to :80
#
	#	registered with FaceBook
	location /forgiveness {
		#	no-trailing-/ case (the rewritten URI has a zero length)
		if ($request_filename = '') {
			rewrite  (.*)  $1/;
		}

		#	the only way i could figure to get conditional proxying
		#		proxy_pass via variables = no
		#		if { proxy_pass } = no
		#		so ... two more goofy contexts

		#	magic tunnel cookie (any value)
		if ($http_cookie ~ '_forgiveness_tunnel=1') {
			#	last : stop rewriting, but re-process through nginx (vs. break)
			rewrite  /forgiveness(.*)  /forgiveness-dev$1  last;
		}
		rewrite  /forgiveness(.*)  /forgiveness-pub$1;
	}

	location /forgiveness-pub {
		#	root contextify
		#	break to terminate re-processing through nginx
		rewrite /forgiveness-pub(.*)  $1  break;

		proxy_pass  http://cantremember_forgiveness_cluster;
	}

	location /forgiveness-dev {
		rewrite /forgiveness-dev(.*)  $1  break;

		#	same as public upstream
		proxy_pass  http://127.0.0.1:3100;
	}

Here I omit the upstream cluster configs, and I’ll let the comments speak for themselves … I always try to keep track of red herrings at least at some level, and there were several I bumped into while trying out this solution. The best approach that I found was to simply set a cookie with a naming convention that nginx could parse (yes, there is a regex performance penalty here). The development context is routed locally, which is where the tunnel is waiting to pick up and run with it.

Setting the cookie, well that’s another trick. Though not much of one. Of course your app is completely masked behind the apps.facebook.com domain, so trying to set a cookie to your internal domain (eg. IP) through your browser isn’t so easy, and it’s repetetive manual work. So, just bake it into a dedicated action in your app:

#	allows for Facebook pub / dev tunneling
#		have to set the Cookie within the scope of the app
#		can't use Firefox cookie editor to hack it
def tunnel
	#	how convenient!
	value = params[:id]

	#	this is how we get pub / dev tunneling
	#		see support/nginx
	if value == 'nil'
		cookies.delete :_forgiveness_tunnel
	else
		cookies[:_forgiveness_tunnel] = value
	end

	###render :text => "OK #{value}", :content_type => 'text/plain'
	redirect_to root_url
end

Then use /:controller/tunnel/1 to engage, and /:controller/tunnel/0 to disengage. A little bit of auth logic, and you’re good to go.