#!/usr/bin/perl
use CGI;
use IO::Socket::INET;
use XML::Parser;
use XML::LibXML;
use Net::Subnets;
use JSON;
use WWW::Mechanize;
use strict;
### Constants
my $id_rand_len = 32;
my $ref_param = "ref";
my $dbfile = "held-cache.conf";
my $logfile = "held-log.txt";
my $swarmfile = "held-swarm.txt";
my @ietf_subnets = (
"130.129.16.0/20",
"130.129.32.0/20",
"130.129.48.0/20",
"130.129.64.0/20",
"130.129.80.0/20",
"130.129.96.0/20",
);
my $gears_uri = "http://www.google.com/loc/json";
### Derived constants
my $subnets = Net::Subnets->new;
$subnets->subnets( \@ietf_subnets );
###
### Subroutine: Store a value in the database
### Input: key, value (value MUST NOT contain "\t")
### Output: nothing
###
sub db_put($$)
{
my($key,$value) = @_;
print DBFILE "$key\t$value\n";
}
###
### Subroutine: Get a value from the database
### Input: key
### Output: value
###
sub db_get($)
{
my $key = shift;
my @db = `cat $dbfile`;
my @goodlines = grep /^$key\t/, @db;
if (!@goodlines) { return undef; }
else {
my $val = $goodlines[0];
$val =~ s/^$key\t//;
chomp($val);
return $val;
}
}
###
### Subroutine: Get the key for a value from the database
### Input: value
### Output: key
###
sub db_rget($)
{
my $key = shift;
my @db = `cat $dbfile`;
my @goodlines = grep /\t$key$/, @db;
if (!@goodlines) { return undef; }
else {
my $val = $goodlines[0];
$val =~ s/\t$key$//;
chomp($val);
return $val;
}
}
###
### Subroutine: Generate a string of random characters
### Input: Length of string
### Output: String
### Courtesy of http://guymal.com
###
sub random_id ()
{
my $length_of_randomstring=$id_rand_len;
my @chars=('a'..'z','A'..'Z','0'..'9');
my $random_string;
foreach (1..$length_of_randomstring)
{
# rand @chars will generate a random
# number between 0 and scalar @chars
$random_string.=$chars[rand @chars];
}
return $random_string;
}
### END sub random_string
###
### Subroutine: Get location (from one of multiple sources)
### Input: IP address (in text form)
### Output: Hash of location information
###
sub get_location($%) {
my $ip = $_[0];
my %meas = %{$_[1]};
return gears_get_location($ip, \%meas);
}
###
### Subroutine: Get location from Google Gears
### Input: IP address (in text form), wifi measurement hash
### Output: Hash of location information
###
sub gears_get_location($%) {
my $ip = $_[0];
my %meas = %{$_[1]};
my %out = ();
# Construct the query
my $gearsRequest = '{ "version": "1.1.0", "wifi_towers": [';
my @macs = keys %meas;
for (my $i=0; $i<= $#macs; ++$i) {
$gearsRequest .= '{ "mac_address": "'. $macs[$i] . '", ';
$gearsRequest .= '"signal_strength": '. $meas{$macs[$i]} .'} ';
if ($i < $#macs) { $gearsRequest .= ", "; }
}
$gearsRequest .= '] }';
# Send the query
my $gears_uri = "http://www.google.com/loc/json";
my $browser = LWP::UserAgent->new();
my $response = $browser->post($gears_uri, Content=>$gearsRequest);
my $gearsResponse = "";
if ($response->is_success) {
$gearsResponse = $response->content;
} else {
return \%out;
}
# Decode the response
my $decodedResponse = from_json( $gearsResponse );
if (!$decodedResponse || !($decodedResponse->{location})) {
return \%out;
}
# Construct the object to return
$out{"Latitude"} = $decodedResponse->{location}->{latitude};
$out{"Longitude"} = $decodedResponse->{location}->{longitude};
$out{"Accuracy"} = $decodedResponse->{location}->{accuracy};
return \%out;
}
###
### Subroutine: Parse the XML locationRequest
### Input: String containing XML
### Output: Hash containing relevant parameters
###
sub parse_request($)
{
my $in = shift;
my %out = ();
my $error_code;
my $error_message;
my $checker = XML::Parser->new( ErrorContext => 2 );
my $parser = new XML::LibXML;
my $doc;
# Check that it the XML well-formed using XML::Parser...
eval { $checker->parse( $in ); };
if ($@) {
$error_code = "xmlError";
$error_message = "Mal-formed XML locationRequest";
goto error;
}
# ... then get the DOM using libxml (which dies on malformed)
eval { $doc = $parser->parse_string($in); };
# Check that we actually have a locationRequest object
my $req = $doc->documentElement();
if ($req->localname() ne "locationRequest") {
$error_code = "unsupportedMessage";
$error_message = "Only requests of type locationRequest are supported";
goto error;
}
# Pull the data
$out{"responseTime"} = $req->getAttribute("responseTime");
my $locationTypeElement = ${$req->getChildrenByTagName("locationType")}[0];
if ($locationTypeElement) {
$out{"exact"} = $locationTypeElement->getAttribute("exact");
my $locationTypeString = $locationTypeElement->textContent;
my @locationTypes = split(/\s+/, $locationTypeString);
@locationTypes = grep /any|civic|geodetic|locationURI/, @locationTypes;
$out{"locationType"} = join(" ", @locationTypes);
}
my $measElement = ${$req->getChildrenByTagName("measurements")}[0];
my %meas = ();
if ($measElement) {
my $wifiElement = ${$measElement->getChildrenByTagName("wifi")}[0];
if ($wifiElement) {
my @aps = ();
push @aps, @{$wifiElement->getChildrenByTagName("servingWap")};
push @aps, @{$wifiElement->getChildrenByTagName("neighbourWap")};
foreach my $apElement (@aps) {
my $bssidElement = ${$apElement->getChildrenByTagName("bssid")}[0];
my $rssiElement = ${$apElement->getChildrenByTagName("rssi")}[0];
if ($bssidElement && $rssiElement) {
$meas{$bssidElement->textContent()} = $rssiElement->textContent();
}
}
}
}
$out{"wifiMeas"} = \%meas;
return \%out;
error:
$out{"error_code"} = $error_code;
$out{"error_message"} = $error_message;
return \%out;
}
### END sub parse_request
###
### Subroutine: Log a HELD transaction
### Input: 1) Method of the HTTP request
### 2) Body of the HTTP request
### 3) Value of the locationResponse returned
### 4) Value of the errorResponse returned
### Output: None
###
sub log_transaction($$$$$) {
# my($method, $ruri, $body, $locationResponse, $errorResponse) = @_;
# print LOGFILE "\n";
# print LOGFILE "\n";
# print LOGFILE "$body\n";
# print LOGFILE "\n";
# print LOGFILE "\n";
# print LOGFILE "$locationResponse\n";
# print LOGFILE "\n";
# print LOGFILE "\n";
# print LOGFILE "$errorResponse\n";
# print LOGFILE "\n";
# print LOGFILE "\n\n\n";
# return;
}
### END sub log_transaction
BEGIN:
open DBFILE, ">>$dbfile" or die "Unable to open URI cache: $!\n";
select((select(DBFILE), $|=1)[0]);
open LOGFILE, ">>$logfile" or die "Unable to open transaction log: $!\n";
select((select(LOGFILE), $|=1)[0]);
###
### BEGIN Server Logic
###
my $error_code = "";
my $error_message = "";
###
### Step 1: Read the request
###
# Pull out relevant things from the environment
my $method = $ENV{'REQUEST_METHOD'};
my $request_uri = $ENV{'REQUEST_URI'};
my $my_hostname = $ENV{'SERVER_NAME'};
my $my_uri = $ENV{'SCRIPT_URI'};
my $client_ip = $ENV{'REMOTE_ADDR'};
# If this is a reference it should be in a query string variable
my $loc_reference = $ENV{"QUERY_STRING"};
# Check that the request method is either GET or POST
if (($method != "GET")&&($method != "POST")) {
$error_code = "requestError";
$error_message = "HTTP method MUST be GET or POST";
goto error;
}
# If it's a POST, we'll need to read the body
my $body = "";
if ($method eq "POST") {
while (<>) { $body .= $_; }
}
my $ip = "";
# Decide which IP to use
if (!$loc_reference) {
# If there's no path component, it's the LCP; use the src IP
$ip = $client_ip;
} else {
# Else, validate the path component and try to retrieve the cached IP
my $reference_valid = ($loc_reference =~ /^[a-zA-Z0-9]{$id_rand_len}$/);
# If the path looks good, try to get a cached IP
if ($reference_valid) {
$ip = db_get($loc_reference) or '';
if (!$ip) {
$error_code = "locationUnknown";
$error_message = "No target IP found for reference";
goto error;
}
} else {
$error_code = "requestError";
$error_message = "Mal-formed reference";
goto error;
}
}
###
### Step 1a: Parse the request
###
my %req = ();
if ($body ne "") { %req = %{parse_request($body)}; }
if ($req{"error_code"}) {
$error_code = $req{"error_code"};
$error_message = $req{"error_message"};
goto error;
}
# NB: We ignore the responseTime attribute.
my %meas;
if ($req{"wifiMeas"}) {
%meas = %{$req{"wifiMeas"}};
} else {
undef %meas;
}
###
### Step 2: Use the IP address to get location from Akamai
###
my %loc_info = %{get_location($ip,\%meas)};
###
### Step 3: Construct the PIDF-LO(s)
###
# Determine which formats to return
# First, see which we can determine from the given data
my $got_geo = (($loc_info{"Latitude"})&&($loc_info{"Longitude"}));
my $got_civic = (($loc_info{"Country Code"})||($loc_info{"Region Code"})||($loc_info{"County"})||($loc_info{"City"})||($loc_info{"country"}));
# Willing to provide a URI if we've got either geo or civic, fail otherwise
my $got_uri = ($got_geo || $got_civic);
if (!$got_geo && !$got_civic) {
$error_code = "locationUnknown";
$error_message = "Unable to determine location for target IP address";
}
my $geo_lo = "";
my $civic_lo = "";
my $uri_lo = "";
# Next, see which are allowed by the request (start false, flip to true)
my $geo_allowed = undef;
my $civic_allowed = undef;
my $uri_allowed = undef;
if (!$req{"locationType"} || !$req{"exact"} || $req{"exact"} ne "true") {
$geo_allowed = !$geo_allowed;
$civic_allowed = !$civic_allowed;
$uri_allowed = !$uri_allowed;
}
elsif ($req{"locationType"} =~ /civic|any/) { $civic_allowed = !$civic_allowed; }
elsif ($req{"locationType"} =~ /geodetic|any/) { $geo_allowed = !$geo_allowed; }
elsif ($req{"locationType"} =~ /locationURI|any/) { $uri_allowed = !$uri_allowed; }
# Match the two criteria up; if nothing's left, fail with cannotProvideLiType
my $geo = ($got_geo && $geo_allowed);
my $civic = ($got_civic && $civic_allowed);
my $uri = ($got_uri && $uri_allowed);
if (!$geo && !$civic && !$uri) {
$error_code = "cannotProvideLiType";
$error_message = "Available LI types: ";
if ($got_geo) { $error_message .= "geodetic "; };
if ($got_civic) { $error_message .= "civic "; };
goto error;
}
# Make the wrapper for the presence object if we're going to return one
my $lo_header = "";
my $lo_footer = "";
if ($geo || $civic) {
$lo_header .= qq| \n|;
$lo_footer .= qq| \n|;
}
# Create a geodetic tuple
if ($geo)
{
# Generate a tuple ID
my $tuple_id = random_id();
# Compute the timestamp
my($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst)=localtime(time);
my $timestamp = sprintf "%4d-%02d-%02dT%02d:%02d:%02d+00:00", $year+1900, $mon+1, $mday, $hour, $min, $sec;
# Grab the lat/long
my $lat = $loc_info{"Latitude"};
my $lon = $loc_info{"Longitude"};
my $acc = $loc_info{"Accuracy"};
$geo_lo .= qq| \n|;
$geo_lo .= qq| \n|;
$geo_lo .= qq| \n|;
$geo_lo .= qq| \n|;
if ($acc) {
$geo_lo .= qq| \n|;
$geo_lo .= qq| $lat $lon \n|;
$geo_lo .= qq| $acc \n|;
$geo_lo .= qq| \n|;
} else {
$geo_lo .= qq| \n|;
$geo_lo .= qq| $lat $lon \n|;
$geo_lo .= qq| \n|;
}
$geo_lo .= qq| \n|;
$geo_lo .= qq| \n|;
$geo_lo .= qq| \n|;
$geo_lo .= qq| \n|;
$geo_lo .= qq| $timestamp \n|;
$geo_lo .= qq| \n|;
}
# Create a civic tuple
if ($civic)
{
# Generate a tuple ID
my $tuple_id = random_id();
# Compute the timestamp
my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst)=localtime(time);
my $timestamp = sprintf "%4d-%02d-%02dT%02d:%02d:%02d+00:00", $year+1900, $mon+1, $mday, $hour, $min, $sec;
# Grab the CATypes
my $country = ($loc_info{"Country Code"} or $loc_info{"country"});
my $a1 = ($loc_info{"Region Code"} or $loc_info{"A1"});
my $a2 = ($loc_info{"County"} or $loc_info{"A2"});
my $a3 = ($loc_info{"City"} or $loc_info{"A3"});
my $rd = $loc_info{"RD"};
my $sts = $loc_info{"STS"};
my $pod = $loc_info{"POD"};
my $hno = $loc_info{"HNO"};
my $pc = $loc_info{"PC"};
my $room = $loc_info{"ROOM"};
my $flr = $loc_info{"FLR"};
my $loc = $loc_info{"LOC"};
$civic_lo .= qq| \n|;
$civic_lo .= qq| \n|;
$civic_lo .= qq| \n|;
$civic_lo .= qq| \n|;
$civic_lo .= qq| \n|;
$civic_lo .= qq| $country \n| if ($country);
$civic_lo .= qq| $a1 \n| if ($a1);
$civic_lo .= qq| $a2 \n| if ($a2);
$civic_lo .= qq| $a3 \n| if ($a3);
$civic_lo .= qq| $rd \n| if ($rd);
$civic_lo .= qq| $sts \n| if ($sts);
$civic_lo .= qq| $pod \n| if ($pod);
$civic_lo .= qq| $hno \n| if ($hno);
$civic_lo .= qq| $pc \n| if ($pc);
$civic_lo .= qq| $room \n| if ($room);
$civic_lo .= qq| $flr \n| if ($flr);
$civic_lo .= qq| $loc \n| if ($loc);
$civic_lo .= qq| \n|;
$civic_lo .= qq| \n|;
$civic_lo .= qq| \n|;
$civic_lo .= qq| \n|;
$civic_lo .= qq| \n|;
$civic_lo .= qq| $timestamp \n|;
$civic_lo .= qq| \n|;
}
# Allocate a location URI
if ($uri)
{
# Check to see if the target is already cached
my $id = db_rget($ip);
# If not, generate and store a new id-ip mapping
if (!$id)
{
# Generate a random identifier
$id = random_id();
# Cache the id-ip mapping
db_put($id,$ip);
}
# Compute an expiry time that's effectively infinite (100yrs)
my($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst)=localtime(time);
my $timestamp = sprintf "%4d-%02d-%02dT%02d:%02d:%02d+00:00", $year+1900+100, $mon+1, $mday, $hour, $min, $sec;
# Form a held: URI with that ID in the appropriate query field
my $new_uri = $my_uri . "?" . $ref_param . "=" . $id;
# Encapsulate the URI in the approprate XML
$uri_lo .= qq|\n|;
$uri_lo .= qq| $new_uri\n|;
$uri_lo .= qq|\n|;
}
###
### Step 4: Form the Location Response
###
my $locationResponse = "";
$locationResponse .= qq| \n|;
$locationResponse .= qq| \n|;
$locationResponse .= $uri_lo if ($uri);
if ($geo || $civic) {
$locationResponse .= $lo_header;
$locationResponse .= $geo_lo if ($geo);
$locationResponse .= $civic_lo if ($civic);
$locationResponse .= $lo_footer;
}
$locationResponse .= qq| \n|;
###
### Step 5: Deliver the response
###
# We use text/xml for now so that it shows up in a browser
# (should be application/held+xml)
my $content_length = length($locationResponse);
print < \n|;
$errorResponse .= qq| \n\n|;
$content_length = length($errorResponse);
print <