#!/usr/pkg/bin/perl

use strict;
use warnings;

use UNIVERSAL::require;
use English qw(-no_match_vars);

use LWP::UserAgent;
use Pod::Usage;
use Getopt::Long;
use Digest::SHA;
use XML::TreePP;
use Time::HiRes qw(gettimeofday);
use File::Which;

our $VERSION = "1.0";

my $options = {
    useragent => "FusionInventory-RemoveInventory/$VERSION",
};

GetOptions(
    $options,
    'help|h',
    'useragent|u=s',
    'verbose|v',
    'debug',
    'port|p=i',
    'timeout|t=i',
    'baseurl|b=s',
    'token|T=s',
    'directory|d=s',
    'id=s',
    'ssl|s',
    'ca-cert-file=s',
    'no-ssl-check',
    'no-compression|C',
) or pod2usage(-verbose => 0);

pod2usage(-verbose => 0, -exitstatus => 0) if $options->{help};

pod2usage(
    -message => "\nGive a least one host to get inventory from as parameter\n",
    -verbose => 0,
    -exitstatus => 1
) unless @ARGV;

pod2usage(
    -message => "\nNo token as shared secret defined\n",
    -verbose => 0,
    -exitstatus => 1
) unless $options->{token};

pod2usage(
    -message => "\nWhen asking inventory to more than one host, you must use the --directory parameter\n",
    -verbose => 0,
    -exitstatus => 1
) if !$options->{directory} && @ARGV>1;

pod2usage(
    -message => "\nDirectory not found: $options->{directory}\n",
    -verbose => 0,
    -exitstatus => 1
) if ($options->{directory} && ! -d $options->{directory});

my $ua = LWP::UserAgent->new(
    agent                 => $options->{useragent},
    timeout               => $options->{timeout} || 180,
    parse_head            => 0, # No need to parse HTML
    keep_alive            => 1,
);

if ($options->{ssl}) {
    $ua->ssl_opts(SSL_ca_file => $options->{'ca-cert-file'})
        if $options->{'ca-cert-file'};
    $ua->ssl_opts(verify_hostname => 0, SSL_verify_mode => 0)
        if $options->{ssl} && $options->{'no-ssl-check'};
}

$options->{verbose} = 1 if $options->{debug};

my $id = $options->{id} || id();

warn "Using $id as request id\n"
    if $options->{verbose};

foreach my $host (@ARGV) {
    my $url = ( $options->{ssl} ? "https://" : "http://" ). $host;
    $url .= $options->{port} ? ":".$options->{port} : ":62354";
    $url .= $options->{baseurl} ? $options->{baseurl} : "/inventory";

    warn "$host: Trying $url\n"
        if $options->{verbose};

    my $req = HTTP::Request->new(GET => $url.'/session');
    $req->header( 'X-Request-ID' => $id );
    $req->protocol( 'HTTP/1.1' );

    if ($options->{debug}) {
        warn "--->\n";
        warn "Request: ".$req->as_string();
    }

    my $session = $ua->request($req);

    if ($options->{debug}) {
        warn "<---\n";
        warn "Response: ".$session->as_string();
    }

    if (!$session->is_success()) {
        warn "$host: No session (".$session->status_line().")\n";
        next;
    }

    my $nonce = $session->header('X-Auth-Nonce')
        or die "No nonce\n";

    my $sha = Digest::SHA->new(256);
    $sha->add($nonce.'++'.$options->{token});

    my $payload = $sha->b64digest;

    # Update request to get inventory
    $req->uri($url.'/get');
    $req->header( 'X-Auth-Payload' => $payload );

    # Set Accept header
    my $accept = 'application/xml';
    if (!$options->{'no-compression'}) {
        $accept .= ', application/x-compress-zlib'
            if Compress::Zlib->require();
        my $zcat = scalar(which('zcat'));
        $accept .= ', application/x-compress-gzip'
            if -x $zcat;
    }
    $req->header('Accept' => $accept);

    if ($options->{debug}) {
        warn "--->\n";
        warn "Request: ".$req->as_string();
    }

    my $xml = $ua->request($req);

    if ($options->{debug}) {
        warn "<---\n";
        warn "Response: ".$xml->status_line()."\n".$xml->headers->as_string()."\n";
    }

    if (!$xml->is_success()) {
        warn "$host: Inventory request: ".$xml->status_line()."\n";
        next;
    }

    my $content = $xml->content();

    # check compression mode
    if ($xml->header('Content-Type') eq 'application/x-compress-zlib') {
        # RFC 1950
        warn "$host: Using Compress::Zlib for decompression\n"
            if $options->{debug};
        $content = Compress::Zlib::uncompress($content);
    } elsif ($xml->header('Content-Type') eq 'application/x-compress-gzip') {
        # RFC 1952
        warn "$host: Using gzip for decompression\n"
            if $options->{debug};

        File::Temp->require();
        my $fd = File::Temp->new();
        print $fd $content;
        close $fd;

        my $OUT;
        unless(open $OUT, '-|', 'zcat ' . $fd->filename()) {
            warn "$host: Failed to uncompress response, skipping\n";
            next;
        }
        local $INPUT_RECORD_SEPARATOR; # Set input to "slurp" mode.
        $content = <$OUT>;
        close($OUT);
    }

    my $deviceid;
    eval {
        my $tpp = XML::TreePP->new();
        my $tree = $tpp->parse($content);
        $deviceid = $tree->{REQUEST}->{DEVICEID};
    };

    unless ($deviceid) {
        warn "$host: No deviceid found in returned inventory output, skipping\n";
        next;
    }

    warn "$host: Got remote inventory from $deviceid\n"
        if $options->{verbose};

    if ($options->{directory}) {
        my $filename = $options->{directory}."/$deviceid.xml";
        open my $FILE, ">", $filename
            or die "$host: Can't open $filename: $!\n";
        print $FILE $content;
        close($FILE);
        warn "$host: Written inventory in $filename\n"
            if $options->{verbose};
    } else {
        print $content;
    }
}

exit(0);

# Compute a simple and as-possible safe id
sub id {
    my $sha = Digest::SHA->new(1);
    $sha->add(gettimeofday());
    $sha->add(gettimeofday());
    my $digest = $sha->hexdigest;
    return substr($digest, 0, 8);
}

__END__

=head1 NAME

fusioninventory-remoteinventory - A tool to pull inventory from an agent

=head1 SYNOPSIS

fusioninventory-remoteinventory [options] <host1> [<host2> ...]

  Options:
    -h --help      this menu
    -d --directory load every .ocs files from a directory
    -t --timeout   requests timeout and even inventory get timeout
    -b --baseurl   remote base url if not /inventory
    -p --port      remote port (62354 by default)
    -T --token     token as shared secret
    -i --id        id for request to identify requests in agent log
    -s --ssl       connect using SSL
    --no-ssl-check do not check agent SSL certificate
    --ca-cert-file CA certificates file

    -C --no-compression
                   ask to not compress sent XML inventories

    -v --verbose   verbose mode
    --debug        debug mode
    -u --useragent set used HTTP User-Agent for requests

  Examples:
    fusioninventory-remoteinventory -T strong-shared-secret 192.168.43.236
    fusioninventory-remoteinventory -v -T strong-shared-secret 192.168.43.237 | \
        fusioninventory-injector -url https://login:pw@example/plugins/fusioninventory/
    fusioninventory-remoteinventory -T strong-shared-secret -d /tmp 192.168.43.236 192.168.43.237

=head1 DESCRIPTION

This tool can be used to securely request an inventory from remote agents not able
to contact a server.
