Group
Extension

FusionInventory-Agent/lib/FusionInventory/Agent/HTTP/Server/Proxy.pm

package FusionInventory::Agent::HTTP::Server::Proxy;

use strict;
use warnings;

use English qw(-no_match_vars);
use XML::TreePP;
use XML::XPath;
use Compress::Zlib;
use File::Temp;

use base "FusionInventory::Agent::HTTP::Server::Plugin";

use FusionInventory::Agent::Tools;
use FusionInventory::Agent::HTTP::Client::OCS;

our $VERSION = "1.1";

sub urlMatch {
    my ($self, $path) = @_;
    # By default, re_path_match => qr{^/proxy/(version|fusioninventory)/?$}
    return 0 unless $path =~ $self->{re_path_match};
    $self->{request} = $1;
    return 1;
}

sub log_prefix {
    return "[proxy server plugin] ";
}

sub config_file {
    return "proxy-server-plugin.cfg";
}

sub defaults {
    return {
        disabled            => "yes",
        url_path            => "/proxy",
        port                => 0,
        only_local_store    => "no",
        local_store         => '',
        prolog_freq         => 24,
        max_proxy_threads   => 10,
        # Supported by class FusionInventory::Agent::HTTP::Server::Plugin
        maxrate             => 30,
        maxrate_period      => 3600,
    };
}

sub supported_method {
    my ($self, $method) = @_;

    return 1 if $method eq 'GET' || $method eq 'POST';

    $self->error("invalid request type: $method");

    return 0;
}

sub init {
    my ($self) = @_;

    $self->SUPER::init(@_);

    $self->{request}  = 'none';

    my $defaults = $self->defaults();
    my $url_path = $self->config('url_path');
    $self->debug("Using $url_path as base url matching")
        if ($url_path ne $defaults->{url_path});
    $self->{re_path_match} = qr{^$url_path/(apiversion|fusioninventory)/?$};

    # Normalize only_local_store
    $self->{only_local_store} = $self->config('only_local_store') !~ /^0|no$/i ? 1 : 0;
}

sub handle {
    my ($self, $client, $request, $clientIp) = @_;

    my $agent  = $self->{server}->{agent};

    # rate limit by ip to avoid abuse
    if ($self->rate_limited($clientIp)) {
        $client->send_error(429); # Too Many Requests
        return 429;
    }

    if ($self->{request} eq 'apiversion') {
        my $response = HTTP::Response->new(
            200,
            'OK',
            HTTP::Headers->new( 'Content-Type' => 'text/plain' ),
            $VERSION
        );

        $client->send_response($response);

        return 200;
    }

    my $retcode = $self->_handle_proxy_request($client, $request, $clientIp);

    # In the case we run in a fork, just close the socket and quit
    if ($agent->forked()) {
        $self->debug("response status $retcode");
        $client->close();
        $agent->fork_exit(logger => $self, name => $self->name());
    }

    return $retcode;
}

sub _handle_proxy_request {
    my ($self, $client, $request, $clientIp) = @_;

    return unless $client && $request && $clientIp;

    my $remoteid = $clientIp;

    # /proxy/fusioninventory request

    # From here we should fork and return
    my $agent = $self->{server}->{agent};
    unless ($agent->forked()) {
        # check against max_proxy_threads
        my $current_requests = $agent->forked(name => $self->name());

        if ($current_requests >= $self->config('max_proxy_threads')) {
            $client->send_error(429); # Too Many Requests
            return 429;
        }

        return 1 if $agent->fork(name => $self->name(), description => $self->name()." request");
    }

    my $content_type = $request->header('Content-type');

    my ($url, $params) = split(/[?]/, $request->uri());

    if ($params && $params =~ /action=getConfig/) {
        $self->debug("$params request from $clientIp, sending nothing to do");
        my $response = HTTP::Response->new(
            200,
            'OK',
            HTTP::Headers->new( 'Content-Type' => 'application/json' ),
            '{}'
        );

        $client->send_response($response);

        return 200;
    } elsif ($params) {
        $self->info("Unsupported $params request from $clientIp");
        $client->send_error(403, 'Unsupported request');
        return 403;
    }

    unless ($content_type) {
        $self->info("No mandatory Content-type header provided in $self->{request} request from $clientIp");
        $client->send_error(403, 'Content-type not set');
        return 403;
    }

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

    # Uncompress if needed
    if ($content_type =~ m|^application/x-compress(-zlib)?$|i && $content =~ /(\x78\x9C.*)/s) {
        $content = Compress::Zlib::uncompress($content);
    } elsif ($content_type =~ m|^application/x-compress-gzip$|i) {
        my $in = File::Temp->new(SUFFIX => '.proxy');
        print $in $content;
        close($in);

        my $out;
        eval {
            $out = getFileHandle(
                command => 'gzip -dc ' . $in->filename(),
                logger  => $self->{logger}
            );
        };

        unless ($out) {
            $client->send_error(403, 'Unsupported $content_type Content-type');
            $self->info("Can't uncompress $content_type Content-type in $self->{request} request from $clientIp");
            return 403;
        }

        local $INPUT_RECORD_SEPARATOR; # Set input to "slurp" mode.
        $content = <$out>;
        close($out);
    } elsif ($content_type !~ m|^application/xml$|i) {
        $client->send_error(403, 'Unsupported Content-type');
        $self->info("Unsupported '$content_type' Content-type header provided in $self->{request} request from $clientIp");
        return 403;
    }

    unless ($content) {
        $self->info("No Content found in $self->{request} request from $clientIp");
        $client->send_error(403, 'No content');
        return 403;
    }

    my $deviceid;
    if ($content =~ m|^<\?xml|ms) {
        # Check if it's a PROLOG request
        my $parser = XML::XPath->new(xml => $content);

        # Don't validate XML against DTD, parsing may fail if a proxy is active
        $XML::XPath::ParseParamEnt = 0;

        my $query = $parser->getNodeText("/REQUEST/QUERY");

        unless ($query && $query =~ /^PROLOG|INVENTORY$/) {
            $self->info("Not supported ".($query||"unknown")." query from $remoteid");
            my ($sample) = $content =~ /^(.{1,80})/ms;
            if ($sample) {
                $sample =~ s/\n\s*//gs;
                $sample = getSanitizedString($sample);
                $self->debug("Not supported XML looking like: $sample")
                    if $sample;
            }
            $client->send_error(403, 'Unsupported query');
            return 403;
        }

        $deviceid = $parser->getNodeText("/REQUEST/DEVICEID");

        unless ($deviceid) {
            $self->info("Not supported $query query from $remoteid");
            $client->send_error(403, "$query query without deviceid");
            return 403;
        }

        $remoteid = $deviceid . '@' . $clientIp;
        $self->info("$query query from $remoteid");

        if ($query eq 'PROLOG') {

            $self->debug2("PROLOG request from $remoteid");

            my $tpp = XML::TreePP->new(indent => 2);
            my $data = {
                REPLY => {
                    RESPONSE    => 'SEND',
                    PROLOG_FREQ => $self->config("prolog_freq")
                }
            };

            my $response = HTTP::Response->new(
                200,
                'OK',
                HTTP::Headers->new( 'Content-Type' => 'application/xml' ),
                $tpp->write($data)
            );

            $client->send_response($response);

            return 200;
        }
    } else {
        $client->send_error(403, 'Unsupported content');
        $self->info("Unsupported content in $self->{request} request from $clientIp");
        $self->debug("Content from $clientIp was starting with '".(substr($content,0,40))."'");
        return 403;
    }

    $self->debug("proxy request for $remoteid");

    my @servers = ();
    my $serverconfig = $agent->{config};

    unless ($serverconfig) {
        $client->send_error(500, 'Server configuration missing');
        $self->info("Server configuration is missing");
        return 500;
    }

    my $response = HTTP::Response->new(
        200,
        'OK',
        HTTP::Headers->new( 'Content-Type' => 'application/xml' ),
        "<?xml version='1.0' encoding='UTF-8'?>\n<REPLY></REPLY>\n"
    );

    if ($self->config('only_local_store')) {
        $response = HTTP::Response->new(500, 'No local storage for inventory')
            unless ($self->config('local_store') && -d $self->config('local_store'));
    } else {
        @servers = grep { $_->isType('server') } $agent->getTargets();
    }

    if ($self->config('local_store') && -d $self->config('local_store')) {
        my $xmlfile = $self->config('local_store');
        $xmlfile =~ s|/*$||;
        $xmlfile .= "/$deviceid.xml";
        $self->debug("Saving inventory in $xmlfile");
        my $XML;
        if (!open($XML, '>', $xmlfile)) {
            $client->send_error(500, 'Cannot store content');
            $self->error("Can't store content from $clientIp $self->{request} request");
            return 500;
        }
        print $XML $content;
        close($XML);
        if (-s $xmlfile != length($content)) {
            $client->send_error(500, 'Content store failure');
            $self->error("Can't store content from $clientIp $self->{request} request");
            return 500;
        }
        if ($self->config('only_local_store')) {
            $client->send_response($response);
            return 200;
        }
    }

    if (@servers) {
        my $proxyclient = FusionInventory::Agent::HTTP::Client::OCS->new(
            logger       => $self->{logger},
            user         => $serverconfig->{user},
            password     => $serverconfig->{password},
            proxy        => $serverconfig->{proxy},
            ca_cert_file => $serverconfig->{'ca-cert-file'},
            ca_cert_dir  => $serverconfig->{'ca-cert-dir'},
            no_ssl_check => $serverconfig->{'no-ssl-check'},
            no_compress  => $serverconfig->{'no-compress'},
        );

        my $message = FusionInventory::Agent::HTTP::Server::Proxy::Message->new(
            content  => $content,
        );

        foreach my $target (@servers) {
            $self->debug("Submitting inventory from $remoteid to ".$target->getName());
            my $sent = $proxyclient->send(
                url     => $target->getUrl(),
                message => $message
            );
            unless ($sent) {
                $response = HTTP::Response->new(500, 'Inventory not sent to server');
                $self->error("Can't submit $remoteid inventory to ".$target->getName()." server");
                last;
            }
            $self->info("Inventory from $remoteid submitted to ".$target->getName());
        }
    }

    $client->send_response($response);

    return $response->code();
}

## no critic (ProhibitMultiplePackages)
package
    FusionInventory::Agent::HTTP::Server::Proxy::Message;

sub new {
    my ($class, %params) = @_;

    my $self = {
        content => $params{content},
    };
    bless $self, $class;
}

sub getContent {
    my ($self) = @_;

    return $self->{content};
}

1;
__END__

=head1 NAME

FusionInventory::Agent::HTTP::Server::Proxy - An embedded HTTP server plugin
providing a proxy for agents not able to contact the server

=head1 DESCRIPTION

This is a server plugin to transmit inventory toward a server.

It listens on port 62354 by default.

The following default requests are accepted:

=over

=item /proxy/fusioninventory

=item /proxy/apiversion

=back

=head1 CONFIGURATION

=over

=item disabled         C<yes> by default

=item url_path         C</proxy> by default

=item port             C<0> by default to use default one

=item prolog_freq      C<24> by default, this is the delay agents will finally
                       recontact the proxy

=item local_store      empty by default, this is the folder where to store inventories

=item only_local_store C<no> by default, set it to C<yes> to not submit inventories
                       to server.

=item maxrate          C<30> by default

=item maxrate_period   C<3600> (in seconds) by default.

=back

Defaults can be overrided in C<proxy-server-plugin.cfg> file or better in the
C<proxy-server-plugin.local> if included from C<proxy-server-plugin.cfg>.


Powered by Groonga
Maintained by Kenichi Ishigaki <ishigaki@cpan.org>. If you find anything, submit it on GitHub.