#!/usr/bin/perl

=pod

/*
 *
 * Copyright (C) 2008 MaxMind LLC
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

=cut

=pod

pure perl version of geoipupdate. can handle anything, that 

  GeoIP_update_database
  GeoIP_update_database_general
  
handle. It is a drop in replacement for geoipupdate, as opposide to geoipupdate is the
pp version able to handle proxy requests even with authentication and can be used with
https

=cut

use strict;
use warnings;

our $VERSION = '0.07';

use 5.008;
use Data::Dumper;
use Digest::MD5;
use File::Spec;
use File::Basename;
use Getopt::Std;
use HTTP::Request::Common;
use LWP::UserAgent;
use PerlIO::gzip;
use URI;

my $ua = LWP::UserAgent->new( agent => "pp_geoipupdate/$VERSION" );
$ua->env_proxy;

## --- for auth proxies use
## $ua->proxy(['http', 'ftp'] => 'http://username:password@proxy.myorg.com');

my $license_file = 'GeoIP.conf';
my $update_host  = 'updates.maxmind.com';
my $proto        = 'http';
my %opts;

if ( !getopts( 'hvf:d:', \%opts ) or $opts{h} ) {
  print STDERR
    "Usage: geoipupdate [-hv] [-f license_file] [-d custom directory]\n";
  exit @ARGV ? 1 : 0;
}

my $rootdir = File::Spec->rootdir;
$opts{d} ||= File::Spec->catfile( $rootdir, qw/ usr local share GeoIP / );
$opts{f} ||=
  File::Spec->catfile( $rootdir, qw/ usr local etc /, $license_file );

die "dir $opts{d} does not exist or is not readable or is not a directory\n"
  unless -d $opts{d};
die "license_file $opts{f} does not exist, is not readable or is not a file\n"
  unless -f $opts{f};

#
# --- parse license file
#
open my $fh, '<', $opts{f}
  or die "Error opening GeoIP Configuration file $opts{f}\n";
print "Opened License file $opts{f}\n" if $opts{v};

my ( $user_id, $license_key, @product_ids );
{
  local $_;

  while (<$fh>) {
    next if /^\s*#/;    # skip comments
    /^\s*UserId\s+(\d+)/        and $user_id     = $1, next;
    /^\s*LicenseKey\s+(\S{12})/ and $license_key = $1, next;
    /^\s*ProductIds\s+(\d+(?:[a-zA-Z]{2,3})?(?:\s+\d+(?:[a-zA-Z]{2,3})?)*)/    
      and @product_ids = split( /\s+/, $1 ), next;

  }
}

if ( $opts{v} ) {
  print "User id $user_id\n" if $user_id;
  print "Read in license key $license_key\n";
  print "Product ids @product_ids\n";
}

if ($user_id) {
  for my $product_id (@product_ids) {

    # update the databases using the user id string,
    # the license key string and the product id for each database
    eval {
      GeoIP_update_database_general( $user_id,    $license_key,
                                     $product_id, $opts{v} );
    };
    my $err = $@;
    die $err if $err and $err !~ /^No new updates available/i;
    print $err;
  }
} else {

  # Old format with just license key for MaxMind GeoIP Country database updates
  # here for backwards compatibility
  eval { GeoIP_update_database( $license_key, $opts{v} ); };
  my $err = $@;
  die $err if $err and $err !~ /^No new updates available/i;
  print $err;
}

exit 0;

sub GeoIP_update_database_general {
  my ( $user_id, $license_key, $product_id, $verbose, $client_ipaddr ) = @_;
  my $u = URI->new("$proto://$update_host/app/update_getfilename");
  $u->query_form( product_id => $product_id );

  print 'Send request ' . $u->as_string, "\n" if ($verbose);
  my $res = $ua->request( GET $u->as_string, Host => $update_host );
  die $res->status_line unless ( $res->is_success );
  # make sure to use only the filename for security reason
  my $geoip_filename = File::Spec->catfile( $opts{d}, basename($res->content) );

  # /* get MD5 of current GeoIP database file */
  my $old_md5 = _get_hexdigest($geoip_filename);

  print "MD5 sum of database $geoip_filename is $old_md5\n" if $verbose;

  unless ($client_ipaddr) {
    print 'Send request ' . $u->as_string, "\n" if ($verbose);

    # /* get client ip address from MaxMind web page */
    $res = $ua->request( GET "$proto://$update_host/app/update_getipaddr",
                         Host => $update_host );
    die $res->status_line unless ( $res->is_success );
    $client_ipaddr = $res->content;
  }

  print "client ip address: $client_ipaddr\n" if $verbose;
  my $hex_digest2 =
    Digest::MD5->new->add( $license_key, $client_ipaddr )->hexdigest;
  print "md5sum of ip address and license key is $hex_digest2\n" if $verbose;

  my $mk_db_req_cref = sub {

    $u->path('/app/update_secure');
    $u->query_form(
                    db_md5        => shift,
                    challenge_md5 => $hex_digest2,
                    user_id       => $user_id,
                    edition_id    => $product_id
    );
    print 'Send request ' . $u->as_string, "\n" if ($verbose);
    return $ua->request( GET $u->as_string, Host => $update_host );
  };
  $res = $mk_db_req_cref->($old_md5);
  die $res->status_line unless ( $res->is_success );

  # print Dumper($res);
  print "Downloading gzipped GeoIP Database...\n" if $verbose;

  _gunzip_and_replace(
    $res->content,
    $geoip_filename,
    sub {

      # as sanity check request a update for the new downloaded file
      # md5 of the new unpacked file
      my $new_md5 = _get_hexdigest(shift);
      return $mk_db_req_cref->($new_md5);
    }
  );
  print "Done\n" if $verbose;
}

sub GeoIP_update_database {
  my ( $license_key, $verbose ) = @_;
  my $geoip_filename = File::Spec->catfile( $opts{d}, 'GeoIP.dat' );

  # /* get MD5 of current GeoIP database file */
  my $hexdigest = _get_hexdigest($geoip_filename);

  print "MD5 sum of database $geoip_filename is $hexdigest\n" if $verbose;

  my $u = URI->new("$proto://$update_host/app/update");
  $u->query_form( license_key => $license_key, md5 => $hexdigest );

  print 'Send request ' . $u->as_string, "\n" if ($verbose);
  my $res = $ua->request( GET $u->as_string, Host => $update_host );
  die $res->status_line unless ( $res->is_success );
  print "Downloading gzipped GeoIP Database...\n" if $verbose;
  _gunzip_and_replace( $res->content, $geoip_filename );
  print "Done\n" if $verbose;

}

# --- hexdigest of the file or 00000000000000000000000000000000
sub _get_hexdigest {
  my $md5 = '0' x 32;
  if ( open my $fh, '<:raw', shift ) {
    $md5 = Digest::MD5->new->addfile($fh)->hexdigest;
  }
  return $md5;
}

sub _gunzip_and_replace {
  my ( $content, $geoip_filename, $sanity_check_c ) = @_;
  my $max_retry = 1;

  my $tmp_fname = $geoip_filename . '.test';

  {

    # --- error if our content does not start with the gzip header
    die $content || 'Not a gzip file' if substr( $content, 0, 2 ) ne "\x1f\x8b";

    # --- uncompress the gzip data
    {
      local $_;
      open my $gin,  '<:gzip', \$content  or die $!;
      open my $gout, '>:raw',  $tmp_fname or die $!;
      print {$gout} $_ while (<$gin>);
    }

    # --- sanity check
    if ( defined $sanity_check_c ) {
      die "Download failed" if $max_retry-- <= 0;
      my $res = $sanity_check_c->($tmp_fname);
      die $res->status_line unless ( $res->is_success );
      $content = $res->content;

      redo if ( $content !~ /^No new updates available/ );
    }
  }

  # --- install GeoIP.dat.test -> GeoIP.dat
  rename( $tmp_fname, $geoip_filename ) or die $!;
}

