Be Your Own Big Brother

Many modern browsers (including recent versions of Firefox and Opera) support the W3C's geolocation API. This is a standardised mechanism for Javascript to ask your browser where it geographically is in the world. Typically the browser will then pop up a message asking you if you wish to reveal this information, giving you the opportunity to opt out.

But how does your browser know where it is? The method that Firefox and Opera both seem to use is this:

  1. They sniff data on your Wifi to figure out the local hotspots;
  2. They submit the details of these hotspots to a web service run by Google;
  3. Google tells them where you are.

How is Google able to figure out where you are based on local wifi hotspots? The answer is their Street View cars. At the same time that they drive around photographing streets for Street View, they also take a survey of wifi hot spots.

Sounds cool, albeit a little scary, right? Well, if you're afraid of living under such surveillance, here's something that might comfort you: in my case at least, Google tends to be off by around 80 kilometres.

But that's a problem, because I actually want certain websites to know where I am. So how can I customize my browser's response when a website asks where I am?

Firefox has a setting "geo.wifi.uri" which allows you to customise the web service they call for the data. The default is Google's at https://www.google.com/loc/json. In Opera, there's a "Location Provider URL" setting listed in opera:config. So, what other web services are available that I can plug in there? Here's a list:

*

That was a short list, wasn't it?

But let's think about it another way. Whenever I'm using my computer, there's always something sitting right beside my computer... me. And what's in my pocket? An Android phone with access to the GPS. If I could query my phone, problem solved.

Step one, Big Brother GPS is an open source (GPL 2) Android app which allows you to regularly post (15 minute updates, or any other interval you choose) your phone's location (and speed and bearing if you're travelling) to a web service of your choice. OK, so we'll point it to a URL on my server.

The app has the ability to configure a shared secret (i.e. password) to pass to the server. We'll do that, and call it "s3cr3t".

Step two, on the server, we'll create a little SQLite database to log each ping:

  $ sqlite3 pings.sqlite
  sqlite> CREATE TABLE pings (
     ...> time INTEGER,
     ...> latitude REAL,
     ...> longitude REAL,
     ...> altitude REAL,
     ...> accuracy REAL,
     ...> bearing REAL,
     ...> speed REAL,
     ...> );
  sqlite> .quit

Step three, we need to create a little Perl script to receive data from Big Brother GPS and log it to the database. For this we'll be using:

  • DateTimeX::Auto
  • DBD::SQLite
  • DBI
  • JSON
  • LWP::Simple
  • Plack::Request

So we need to install them from CPAN if we don't already have them. Now, here's our basic script, which I'll call app.psgi.

use 5.010;
use DateTimeX::Auto qw[dt];
use DBI;
use JSON qw[from_json to_json];
use LWP::Simple qw[get];
use Plack::Request;

(my $dbfile = __FILE__) =~ s/app.psgi$/pings.sqlite/;
my $dbh = DBI->connect("dbi:SQLite:dbname=$dbfile");
my $sth = $dbh->prepare(<<GO);
INSERT INTO pings (time, latitude, longitude, altitude, accuracy, bearing, speed)
VALUES (?, ?, ?, ?, ?, ?, ?);
GO

my $app = sub
{
	my $req = Plack::Request->new(shift);
	
	if (uc $req->method eq 'POST' and $req->param('secret') eq 's3cr3t')
	{
		$sth->execute(
			dt($req->param('time'))->epoch,
			map {
				~~eval~~ $req->param($_)
			} qw(latitude longitude altitude accuracy bearing speed)
		) or return [
			400,
			[ 'Content-Type' => 'text/plain' ],
			[ $dbh->errstr ],
		];
		
		return [
			200,
			[ 'Content-Type' => 'text/plain' ],
			[ $sql ],
		];
	}
	
	# We'll add some more stuff here soon...
};

That should mostly be self-explanatory with one possible exception: dt($req->param('time'))->epoch converts the incoming ISO 8601 formatted datetime into a Unix timestamp. SQLite doesn't have a datetime data type, so we're storing the time as an integer.

Now, configure your web server to run that PSGI. Make sure Big Brother GPS is pointing at the right URL and watch the data come in.

echo "SELECT datetime(time, 'unixepoch'), latitude, longitude FROM pings ORDER BY time DESC LIMIT 5;" | sqlite3 pings.sqlite

Once you have a few data points, it's time to move onto step four: hooking your browser up to the service. Your browser is going to HTTP POST a bunch of JSON to that URL (which is why it's useful to have that Big Brother GPS secret set up, to help distinguish between POSTs from it, and POSTs from your browser - yes, there are other ways) but we can actually just ignore that and give it a canned response. The response format is actually pretty simple. We just need to grab the latest row from the database and serve it up as JSON.

We'll add this to our app.psgi...

# ... start is unchanged
my $app = sub
{
	my $req = Plack::Request->new(shift);
	
	if (uc $req->method eq 'POST' and $req->param('secret') eq 's3cr3t')
	{
		# this is unchanged too
	}
	
	my $sth = $dbh->prepare('SELECT * FROM pings ORDER BY time DESC LIMIT 1');
	$sth->execute;
	if (my $result = $sth->fetchrow_hashref)
	{
		return [
			200,
			[ 'Content-Type' => 'application/json' ],
			[ to_json({
				location => {
					(map { $_ => $result->{$_} }
					qw(latitude longitude altitude accuracy bearing speed)),
				},
			}, { pretty => 1, canonical => 1 })],
		];
	}
};

Our response is missing the 'address' structure that Google's API provides. As far as I can tell, the browser doesn't use this information, but it's not actually especially difficult to add. I leave this as an exercise for the reader, but I'll give you a clue... 'http://maps.googleapis.com/maps/api/geocode/json?latlng=%f,%f&sensor=false'

This article was originally published on my blog. It is available under a CC BY-SA 2.0 licence.

5 Comments

This was pretty interesting.

By the way, the link to your blog doesn't seem to show the same content as posted above.

Interesting post. I love the simplicity of Plack, and this shows it off really nicely.

There was one line that I'm having a hard time grokking:

map { eval ~~ $req->param($_) } 
    qw(latitude longitude altitude accuracy bearing);
At first glance, I assumed ~~ was the smart match operator and eval was acting on $_. But when I ran it with warnings on and some missing params, I got: Use of uninitialized value in 1's complement (~). So, evidently you are using ~~ to force scalar context and then eval'ing the result. Basically:
eval ( ~~ $req->params($_) )

The net effect is to eliminate any undefined paramaters from the list. Something similar to

map { $req->param($_) // () } ...

So now that (I think) I understand what the code is doing, we get the real question: don't you need the undef's in order to keep the SQL placeholders lined up? Did I interpret the code correctly?

Leave a comment

About Toby Inkster

user-pic I'm tobyink on CPAN, IRC and PerlMonks.