You're viewing all posts tagged with sinatra

hello web from ada

I decided to mess around with some Ada.  This is an old school compiled language, that is popular with the Department of Defense and almost nobody else.  Except me.  Mainly because I like the name.  It’s pretty.

So here’s what you do:

1) Download the GPL version of GNAT from AdaCore.  Make sure you grab AWS also.

2) If you are using Windows, you will need to have a Unix shell prompt available to you.  You can get this using Cygwin or MINGW.

3) Install GNAT.

4) Unzip the AWS source code.  In a moment, we will build this library.  For some reason, on my machine, it didn’t auto-detect the install directory of GNAT properly, so first edit makefile.conf and update the prefix line to point to that directory you installed GNAT into.

5) Run “make setup build install” to install AWS libraries so your projects can find them. 

6) Go into the templates_parser subdirectory and edit makefile.setup to change your installation directory and run “make install”. 

There is no standard way to install libraries so you have to actually read the README and INSTALL files.

7) Use GPS (which comes with GNAT) or install GNATBench (Eclipse plugin) to set up a new Ada project.  Declare the main source file to be “Server.adb”.

8) Create “Server.adb”:

Server.adb:

with GNAT.IO; use GNAT.IO;
with AWS.Default;
with AWS.Server;
with ServerCallback;

procedure Server is
WS : AWS.Server.HTTP;
begin
AWS.Server.Start (WS, "Hello World",
Max_Connection => 1,
Callback => ServerCallback.Echo'Access);
AWS.Server.Wait (AWS.Server.Q_Key_Pressed);
AWS.Server.Shutdown (WS);
end Server;

This sets up a new web server and declares that the method Echo in the file ServerCallback.adb will handle our HTTP requests.  The .EXE will run until you hit the Q key.  It will listen, by default, on port 8080.

9) Define the server callback files “ServerCallback.adb” and “ServerCallback.ads”.

ServerCallback.adb:

with Templates_Parser; use Templates_Parser;
with Templates_Parser.Utils; use Templates_Parser.Utils;
with Ada.Strings.Unbounded; use Ada.Strings.Unbounded;

package body ServerCallback is
function Echo (Request : AWS.Status.Data) return AWS.Response.Data is
pragma Unreferenced (Request);
URI : constant String := AWS.Status.URI (Request);
Params : constant Translate_Table := (1 => Assoc("URI", URI));
Response : Unbounded_String;
begin
Response := Parse (Utils.Get_Program_Directory & ".." & Directory_Separator& "site.thtml", Params);
return AWS.Response.Build ("text/html", Response);
end Echo;
end ServerCallback;

ServerCallback.ads:

with AWS.Response;
with AWS.Status;

package ServerCallback is
function Echo (Request : AWS.Status.Data) return AWS.Response.Data;
end ServerCallback;

This loads a template file called site.thtml and passes in a parameter URI which gets resolves to the request URI.  The parameters are passed to the template_parser using TranslateTable or TranslateSet (I used TranslateTable because I knew in advance how many parameters I would have.)

The actual template itself I put in the root folder of the project (“..” from the obj_debug folder that server.exe gets deposited in.)  It’s basically html with tag declarations that the template_parser replaces with the parameters you supplied above.

site.thtml:

<html>
<body>
Hello from @_URI_@.
</body>
</html>

Now you can hit http://localhost:8080/whatever and you should see the response echo the URI you provided.

You might be wondering how the same program would look in Ruby. Well, like this (assuming you have Ruby and the Sintra gem installed):

require 'rubygems'
require 'sinatra'

get '*' do
"Hello from #{params[:splat]}"
end

But that’s nowhere near as fun.

(Of course, Ada isn’t really a language intended for web programming.  And I don’t think I would feel comfortable in an airplane where the fly-by-wire controls were powered by the Ruby VM and the software cobbled together by splicing together features from a half dozen open source gems.)

a little sinatra oauth ditty

I have a toy Ruby webapp I created using Sinatra that I have hosted on Heroku.  It’s a small little ditty, nothing fancy.  It’s called “Tweet Tools” and I wrote it to make it easier to read tweets from a particular class of Twitter user who don’t seem to understand the point of Twitter: these people insist on making dozens upon dozens of rapid fire and related comments in a row.  Because Twitter and most Twitter clients display the most recent tweets first, their stories or observations or rants or whatever end up reading backwards.

It only took a few hours to write, and I did it a year or so ago and hadn’t messed with it until recently, when it suddenly stopped working.  This is because Twitter went and decided HTTP Basic Auth wasn’t good enough, and now we all have to upgrade to OAuth.  NO problem.  Here’s how you do it with your Ruby webapps in only three simple steps:

1) Go to http://www.twitter.com/apps/ and register your application.  You’ll get a nice little consumer key and a secret.  It really is a secret.  Don’t tell anyone, especially your nosy neighbor.

2) Install the Ruby gem oauth. Unless you want to do everything yourself. In which case, go for broke, but don’t ask me for help. I would rather get real work done.

3) Before every HTTP request, we need to do a few things.

First, we do some basic setup. We clear an area in the session where we intend to hold the OAuth tokens we get, and then construct a Consumer object that identifies who we are using the consumer_key and consumer_secret Twitter gave us, and it identifies Twitter as our OAuth provider.

before do
session[:oauth] ||= {} # we'll store the request and access tokens here
host = request.host
host << ":4567" if request.host == "localhost"
consumer_key = CONSUMER_KEY # what twitter.com/apps says
consumer_secret = CONSUMER_SECRET # shhhh, its a secret
@consumer = OAuth::Consumer.new(consumer_key, consumer_secret, :site => "http://twitter.com")

OK, now we have our Consumer object. That represents our webapp, the OAuth consumer. The ‘host’ variable will come in handy later when we tell Twitter where to call back to. When running Sinatra on my localhost, it uses port :4567 so we append that to the hostname. That way, when we approve Tweet Tools’ permission to use my Twitter account (for browsing other people’s timelines) we’ll get redirected right back to the very same Internet Explorer window I started off in.

Now we need to get a request token. If we asked for one before, we store it in the session so we don’t need to ask for it again.

We will later exchange the request token for an access token by redirecting the user to Twitter and letting them approve our webapp.

  # generate a request token for this user session if we haven't already
request_token = session[:oauth][:request_token]
request_token_secret = session[:oauth][:request_token_secret]
if request_token.nil? || request_token_secret.nil?
# new user? create a request token and stick it in their session
@request_token = @consumer.get_request_token(:oauth_callback => "http://#{host}/auth")
session[:oauth][:request_token] = @request_token.token
session[:oauth][:request_token_secret] = @request_token.secret
else
# we made this user's request token before, so recreate the object
@request_token = OAuth::RequestToken.new(@consumer, request_token, request_token_secret)
end

Awesome. But we might already have an access token, if the user had already logged in and been authenticated by Twitter, so let’s check for that. If we do, we can create our OAuth client object and start perusing timelines.

Astute readers no doubt realize we don’t need to check the request token if we already have an access token. But I’ll leave those optimizations as an exercise for the reader. Mostly because I’m lazy.

  # this is what we came here for...   
access_token = session[:oauth][:access_token]
access_token_secret = session[:oauth][:access_token_secret]
unless access_token.nil? || access_token_secret.nil?
# the ultimate goal is to get here, where we can create our Twitter @client
# object
@access_token = OAuth::AccessToken.new(@consumer, access_token, access_token_secret)
oauth = Twitter::OAuth.new(consumer_key, consumer_secret)
oauth.authorize_from_access(@access_token.token, @access_token.secret)
@client = Twitter::Base.new(oauth)
end
end

OK, now how do we exchange our request token for the access token in the first place? Well, by asking Twitter nicely of course. Set up a route that redirects to the authorization URL. We can get that from the request token.

<a href="/request"><img src="/images/twitter_sign_in.png" />

get "/request" do
redirect @request_token.authorize_url
end

After the user is redirected to Twitter they will see this lovely screen.

Assuming they press “ALLOW”, they will come back to the URL you specified in the oauth_callback earlier.

# remember this code from earlier?  This is why we're here:
@request_token = @consumer.get_request_token(:oauth_callback => "http://#{host}/auth")

get "/auth" do
@access_token = @request_token.get_access_token(:oauth_verifier => params[:oauth_verifier])
session[:oauth][:access_token] = @access_token.token
session[:oauth][:access_token_secret] = @access_token.secret
redirect "/"
end

Hooray! Now we can be all up in the user’s tweets, checking their timelines and whatnot.

@client.user_timeline(:id=>@userid, :count=>35).each do |status|
puts "that's right I be all up in your timeline checking your #{status} and whatnot"
end

If you want to block users from accessing certain parts of the system unless they are logged on, you can simply redirect them to the front page or login page by simply checking for the presence of the @access_token.

redirect '/' unless @access_token

At this point in your webapp, you could implement SSO. When you get redirected to your Twitter landing page (/auth in my case), you could query the Twitter API to find the user’s login and check that against a column on your user table. If you find them, you automatically log them into their account. If no such Twitter user exists, you would redirect them to a page where they are asked to link it to their existing account or register for a new one (it’s easy and only takes… where’d they go?) Or whatever. I don’t have time to walk you through all that right now though, as I have some tweets I need to go read. (Backwards.)