#
# Copyright 2022 Centreon (http://www.centreon.com/)
#
# Centreon is a full-fledged industry-strength solution that meets
# the needs in IT infrastructure and application monitoring for
# service performance.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

package centreon::plugins::wsman;

use strict;
use warnings;
use openwsman;
use MIME::Base64;

my %auth_method_map = (
    noauth          => $openwsman::NO_AUTH_STR,
    basic           => $openwsman::BASIC_AUTH_STR,
    digest          => $openwsman::DIGEST_AUTH_STR,
    pass            => $openwsman::PASS_AUTH_STR,
    ntlm            => $openwsman::NTLM_AUTH_STR,
    gssnegotiate    => $openwsman::GSSNEGOTIATE_AUTH_STR,
);

sub new {
    my ($class, %options) = @_;
    my $self  = {};
    bless $self, $class;
    # $options{options} = options object
    # $options{output} = output object
    # $options{exit_value} = integer
    
    if (!defined($options{output})) {
        print "Class wsman: Need to specify 'output' argument.\n";
        exit 3;
    }
    if (!defined($options{options})) {
        $options{output}->add_option_msg(short_msg => "Class wsman: Need to specify 'options' argument.");
        $options{output}->option_exit();
    }

    $options{options}->add_options(arguments => 
                { "hostname|host:s"           => { name => 'host' },
                  "wsman-port:s"              => { name => 'wsman_port', default => 5985 },
                  "wsman-path:s"              => { name => 'wsman_path', default => '/wsman' },
                  "wsman-scheme:s"            => { name => 'wsman_scheme', default => 'http' },
                  "wsman-username:s"          => { name => 'wsman_username' },
                  "wsman-password:s"          => { name => 'wsman_password' },
                  "wsman-timeout:s"           => { name => 'wsman_timeout', default => 30 },
                  "wsman-proxy-url:s"         => { name => 'wsman_proxy_url', },
                  "wsman-proxy-username:s"    => { name => 'wsman_proxy_username', },
                  "wsman-proxy-password:s"    => { name => 'wsman_proxy_password', },
                  "wsman-debug"               => { name => 'wsman_debug', },
                  "wsman-auth-method:s"       => { name => 'wsman_auth_method', default => 'basic' },
                  "wsman-errors-exit:s"       => { name => 'wsman_errors_exit', default => 'unknown' },
    });
    $options{options}->add_help(package => __PACKAGE__, sections => 'WSMAN OPTIONS');

    #####
    $self->{client} = undef;
    $self->{output} = $options{output};
    $self->{wsman_params} = {};

    $self->{error_msg} = undef;
    $self->{error_status} = 0;
    
    return $self;
}

sub connect {
    my ($self, %options) = @_;

    if (!$self->{output}->is_litteral_status(status => $self->{wsman_errors_exit})) {
        $self->{output}->add_option_msg(short_msg => "Unknown value '" . $self->{wsman_errors_exit}  . "' for --wsman-errors-exit.");
        $self->{output}->option_exit(exit_litteral => 'unknown');
    }
    
    openwsman::set_debug(1) if (defined($self->{wsman_params}->{wsman_debug}));
    $self->{client} = new openwsman::Client::($self->{wsman_params}->{host}, $self->{wsman_params}->{wsman_port}, 
                                              $self->{wsman_params}->{wsman_path}, $self->{wsman_params}->{wsman_scheme},
                                              $self->{wsman_params}->{wsman_username}, $self->{wsman_params}->{wsman_password});
    if (!defined($self->{client})) {
        $self->{output}->add_option_msg(short_msg => 'Could not create client handler');
        $self->{output}->option_exit(exit_litteral => $self->{wsman_errors_exit});
    }
    
    if ($self->{wsman_params}->{wsman_scheme} eq 'https') {
        # Dont verify
        $self->{client}->transport()->set_verify_peer(0);
        $self->{client}->transport()->set_verify_host(0);
    }
    
    $self->{client}->transport()->set_auth_method($auth_method_map{$self->{wsman_params}->{wsman_auth_method}});
    $self->{client}->transport()->set_timeout($self->{wsman_params}->{wsman_timeout});
    if (defined($self->{wsman_params}->{wsman_proxy_url})) {
        $self->{client}->transport()->set_proxy($self->{wsman_params}->{wsman_proxy_url});
        if (defined($self->{wsman_params}->{wsman_proxy_username}) && defined($self->{wsman_params}->{wsman_proxy_password})) {
            $self->{client}->transport()->set_proxyauth($self->{wsman_params}->{wsman_proxy_username} . ':' . $self->{wsman_params}->{wsman_proxy_password});
        }
    }
}

sub execute_winshell_commands {
    my ($self, %options) = @_;
    # $options{keep_open} = integer
    # $options{commands} = ref array of hash ([{label => 'myipconfig', value => 'ipconfig /all' }])

    my ($dont_quit) = (defined($options{dont_quit}) && $options{dont_quit} == 1) ? 1 : 0;
    $self->set_error();
    
    my $uri = 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd';
    my $namespace = 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell';
    my $command_result = {};
    my ($command_id, $client_options, $data, $result, $node);
    
    # Init result
    foreach my $command (@{$options{commands}}) {
        $command_result->{$command->{label}} = {stdout => undef, stderr => undef, exit_code => undef};
    }
    
    if (!defined($self->{shell_id})) { 
        $self->{shell_id} = undef;
    
        ######
        # Start Shell
        $client_options = new openwsman::ClientOptions::() 
                                            or $self->internal_exit(msg => 'Could not create client options handler');
        $client_options->set_timeout(30 * 1000); # 30sec
        $client_options->add_selector('Name', 'Themes');
        $client_options->add_option('WINRS_NOPROFILE', 'FALSE');
        $client_options->add_option('WINRS_CODEPAGE', '437'); # utf-8
    
        $data = new openwsman::XmlDoc::('Shell', $namespace)
                                            or $self->internal_exit(msg => 'Could not create XmlDoc');
        $data->root()->add($namespace, 'InputStreams', 'stdin');
        $data->root()->add($namespace, 'OutputStreams', 'stdout stderr');

        $result = $self->{client}->create($client_options, $uri, $data->string(), length($data->string()), "utf-8");
        return undef if ($self->handle_dialog_fault(result => $result, msg => 'Create failed: ', dont_quit => $dont_quit));
        $node = $result->root()->find(undef, 'Selector')
                                            or $self->internal_exit(msg => 'No shell id returned');
        $self->{shell_id} = $node->text();
    }
    
    foreach my $command (@{$options{commands}}) {
        #######
        # Issue command
        $client_options = new openwsman::ClientOptions::()
                                                or $self->internal_exit(msg => 'Could not create client options handler');
        $client_options->set_timeout(30 * 1000); # 30sec
        $client_options->add_option('WINRS_CONSOLEMODE_STDIN', 'TRUE');
        $client_options->add_option('WINRS_SKIP_CMD_SHELL', 'FALSE');
        $client_options->add_selector('ShellId', $self->{shell_id});
        $data = new openwsman::XmlDoc::('CommandLine', $namespace)
                                                or $self->internal_exit(msg => 'Could not create XmlDoc');
        $data->root()->add($namespace, 'Command', $command->{value});

        $result = $self->{client}->invoke($client_options, $uri, 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Command',
                                          $data);
        return undef if ($self->handle_dialog_fault(result => $result, msg => 'Invoke failed: ', dont_quit => $dont_quit));

        $node = $result->root()->find(undef, 'CommandId')
                                                or $self->internal_exit(msg => 'No command id returned');
        $command_id = $node->text();
        
        #######
        # Request stdout/stderr
        $client_options = new openwsman::ClientOptions::()
                                                or $self->internal_exit(msg => 'Could not create client options handler');
        $client_options->set_timeout(30 * 1000); # 30sec
        $client_options->add_selector('ShellId', $self->{shell_id});

        $data = new openwsman::XmlDoc::('Receive', $namespace);
        $node = $data->root()->add($namespace, 'DesiredStream', 'stdout stderr')
                                                or $self->internal_exit(msg => 'Could not create XmlDoc');
        $node->attr_add(undef, 'CommandId', $command_id);
        
        my $timeout_global = 30; #seconds
        my $wait_timeout_done = 0;
        my $loop_out = 1;
        my ($current_stdout, $current_stderr) = ('', '');
        
        while ($loop_out == 1 && $wait_timeout_done < $timeout_global) {
            
        
            $result = $self->{client}->invoke($client_options, $uri, 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Receive',
                                            $data);
            return undef if ($self->handle_dialog_fault(result => $result, msg => 'Invoke failed: ', dont_quit => $dont_quit));
            my $response = $result->root()->find($namespace, 'ReceiveResponse')
                                                    or $self->internal_exit(msg => 'No ReceiveResponse');
            ######
            # Parsing Reponse
            for (my $cnt = 0; $cnt < $response->size(); $cnt++) {
                my $node = $response->get($cnt);
                my $node_command = $node->attr_find(undef, 'CommandId')
                                                    or $self->internal_exit(msg => 'No CommandId in ReceiveResponse');
                if ($node_command->value() ne $command_id) {
                    $self->internal_exit(msg => 'Wrong CommandId in ReceiveResponse node');
                }
                
                if ($node->name() eq 'Stream') {
                    my $node_tmp = $node->attr_find(undef, 'Name');
                    if (!defined($node_tmp)) {
                        next;
                    }
                    my $stream_type = $node_tmp->value();
                    my $output = decode_base64($node->text());
                    next if (!defined($output) || $output eq '');

                    if ($stream_type eq 'stderr') {
                        $current_stderr .= $output;
                    }
                    if ($stream_type eq 'stdout') {
                        $current_stdout .= $output;
                    }
                }
                if ($node->name() eq 'CommandState') {
                    my $node_tmp = $node->attr_find(undef, 'State');
                    if (!defined($node_tmp)) {
                        next;
                    }
                    my $state = $node_tmp->value();
                    if ($state eq 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done') {
                        my $exit_code = $node->find(undef, 'ExitCode');
                        if (defined($exit_code)) {
                            $command_result->{$command->{label}}->{exit_code} = $exit_code->text();
                        } else {
                            $self->internal_exit(msg => "No exit code for 'done' command");
                        }
                        $loop_out = 0;
                    } elsif ($state eq 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Running') {
                        # we wait
                        $wait_timeout_done += 3;
                        sleep 3;
                    } elsif ($state eq 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Pending') {
                       # no-op
                       # WinRM 1.1 sends this with ExitCode:0
                       $loop_out = 0;
                    } else {
                        # unknown
                        $self->internal_exit(msg => 'Unknown command state: ' . $state);
                    }
                }
                
            }
            
        }
        
        if ($loop_out == 1) {
            $self->internal_exit(msg => 'Command to long to execute...');
        }
        
        $current_stderr =~ s/\r//mg;
        $current_stdout =~ s/\r//mg;
        
        $command_result->{$command->{label}}->{stderr} = $current_stderr if ($current_stderr ne '');
        $command_result->{$command->{label}}->{stdout} = $current_stdout if ($current_stdout ne '');
        
        #
        # terminate shell command
        #
        # not strictly needed for WinRM 2.0, but WinRM 1.1 requires this
        #
        $data = new openwsman::XmlDoc::('Signal', $namespace)
                                            or $self->internal_exit(msg => 'Could not create XmlDoc');
        $data->root()->attr_add(undef, 'CommandId', $command_id);
        $data->root()->add($namespace, 'Code', 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/signal/terminate');
        $result = $self->{client}->invoke($client_options, $uri, 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Signal',
                                          $data);
        return undef if ($self->handle_dialog_fault(result => $result, msg => 'Invoke failed: ', dont_quit => $dont_quit));
    }
    
    # Delete Shell resource
    if (defined($self->{shell_id}) && !(defined($options{keep_open}) && $options{keep_open} == 1)) {
        my $client_options = new openwsman::ClientOptions::()
            or die print "[ERROR] Could not create client options handler.\n";
        $client_options->set_timeout(30 * 1000); # 30sec
        $client_options->add_selector('ShellId', $self->{shell_id});
        $self->{shell_id} = undef;
        $result = $self->{client}->invoke($client_options, $uri, 'http://schemas.xmlsoap.org/ws/2004/09/transfer/Delete', undef);
        return undef if ($self->handle_dialog_fault(result => $result, msg => 'Invoke failed: ', dont_quit => $dont_quit));
    }
    
    return $command_result;
}

sub request {
    my ($self, %options) = @_;
    # $options{nothing_quit} = integer
    # $options{dont_quit} = integer
    # $options{uri} = string
    # $options{wql_filter} = string
    # $options{result_type} = string ('array' or 'hash' with a key)
    # $options{hash_key} = string
    
    my ($dont_quit) = (defined($options{dont_quit}) && $options{dont_quit} == 1) ? 1 : 0;
    my ($nothing_quit) = (defined($options{nothing_quit}) && $options{nothing_quit} == 1) ? 1 : 0;
    my ($result_type) = (defined($options{result_type}) && $options{result_type} =~ /^(array|hash)$/) ? $options{result_type} : 'array';
    $self->set_error();
    
    ######
    # Check options
    if (!defined($options{uri})) {
        $self->{output}->add_option_msg(short_msg => 'Need to specify uri option');
        $self->{output}->option_exit(exit_litteral => $self->{wsman_errors_exit});
    }
    
    ######
    # ClientOptions object
    my $client_options = new openwsman::ClientOptions::() 
                                            or $self->internal_exit(msg => 'Could not create client options handler');

    # Optimization
    $client_options->set_flags($openwsman::FLAG_ENUMERATION_OPTIMIZATION);
    $client_options->set_max_elements(999);
    
    ######
    # Filter/Enumerate
    my $filter;
    if (defined($options{wql_filter})) {
        $filter = new openwsman::Filter::()
                                        or $self->internal_exit(msg => 'Could not create filter');
        $filter->wql($options{wql_filter});
    }

    my $result = $self->{client}->enumerate($client_options, $filter, $options{uri});
    return undef if ($self->handle_dialog_fault(result => $result, msg => 'Could not enumerate instances: ', dont_quit => $dont_quit));

    ######
    # Fetch values
    my ($array_return, $hash_return);
    
    $array_return = [] if ($result_type eq 'array');
    $hash_return = {} if ($result_type eq 'hash');
    my $context;
    my $total = 0;

    while (1) {
        my $nodes = $result->body()->find(undef, "Items");
        
        # Get items.
        my $items;
        for (my $cnt = 0; defined($nodes) && ($cnt<$nodes->size()); $cnt++) {
            my $row_return = {};
            for (my $cnt2 = 0; ($cnt2<$nodes->get($cnt)->size()); $cnt2++) {
                $row_return->{$nodes->get($cnt)->get($cnt2)->name()} = $nodes->get($cnt)->get($cnt2)->text();
            }
            $total++;
            push @{$array_return}, $row_return if ($result_type eq 'array');
            $hash_return->{$row_return->{$options{hash_key}}} = $row_return if ($result_type eq 'hash');
        }
        
        $context = $result->context()
                                or last;
        $result = $self->{client}->pull($client_options, $filter, $options{uri}, $context)
                                or last;

    }

    # Release context.
    $self->{client}->release($client_options, $options{uri}, $context) if($context);
    
    if ($nothing_quit == 1 && $total == 0) {
        $self->{output}->add_option_msg(short_msg => "Cant get a single value.");
        $self->{output}->option_exit(exit_litteral => $self->{option_results}->{wsman_errors_exit});
    }
    
    if ($result_type eq 'array') {
        return $array_return;
    }
    return $hash_return;
}

sub check_options {
    my ($self, %options) = @_;
    # $options{option_results} = ref to options result
    
    $self->{wsman_errors_exit} = $options{option_results}->{wsman_errors_exit};

    if (!defined($options{option_results}->{host})) {
        $self->{output}->add_option_msg(short_msg => "Missing parameter --hostname.");
        $self->{output}->option_exit();
    }
    $self->{wsman_params}->{host} = $options{option_results}->{host};

    if (!defined($options{option_results}->{wsman_scheme}) || $options{option_results}->{wsman_scheme} !~ /^(http|https)$/) {
        $self->{output}->add_option_msg(short_msg => "Wrong scheme parameter. Must be 'http' or 'https'.");
        $self->{output}->option_exit();
    }
    $self->{wsman_params}->{wsman_scheme} = $options{option_results}->{wsman_scheme};
    
    if (!defined($options{option_results}->{wsman_auth_method}) || !defined($auth_method_map{$options{option_results}->{wsman_auth_method}})) {
        $self->{output}->add_option_msg(short_msg => "Wrong wsman auth method parameter. Must be 'basic', 'noauth', 'digest', 'pass', 'ntlm' or 'gssnegotiate'.");
        $self->{output}->option_exit();
    }
    $self->{wsman_params}->{wsman_auth_method} = $options{option_results}->{wsman_auth_method};

    if (!defined($options{option_results}->{wsman_port}) || $options{option_results}->{wsman_port} !~ /^([0-9]+)$/) {
        $self->{output}->add_option_msg(short_msg => "Wrong wsman port parameter. Must be an integer.");
        $self->{output}->option_exit();
    }
    $self->{wsman_params}->{wsman_port} = $options{option_results}->{wsman_port};    
    
    $self->{wsman_params}->{wsman_path} = $options{option_results}->{wsman_path};
    $self->{wsman_params}->{wsman_username} = $options{option_results}->{wsman_username};
    $self->{wsman_params}->{wsman_password} = $options{option_results}->{wsman_password};
    $self->{wsman_params}->{wsman_timeout} = $options{option_results}->{wsman_timeout};
    $self->{wsman_params}->{wsman_proxy_url} = $options{option_results}->{wsman_proxy_url};
    $self->{wsman_params}->{wsman_proxy_username} = $options{option_results}->{wsman_proxy_username};
    $self->{wsman_params}->{wsman_proxy_password} = $options{option_results}->{wsman_proxy_password};
    $self->{wsman_params}->{wsman_debug} = $options{option_results}->{wsman_debug};
}

sub handle_dialog_fault {
    my ($self, %options) = @_;
    my $result = $options{result};
    my $msg = $options{msg};
    
    unless($result && $result->is_fault eq 0) {
        my $fault_string = $self->{client}->fault_string();
        my $msg = 'Could not enumerate instances: ' . ((defined($fault_string)) ? $fault_string : 'use debug option to have details');
        if ($options{dont_quit} == 1) {
            $self->set_error(error_status => -1, error_msg => $msg);
            return 1;
        }
        $self->{output}->add_option_msg(short_msg => $msg);
        $self->{output}->option_exit(exit_litteral => $self->{wsman_errors_exit});
    }
    
    return 0;
}

sub internal_exit {
    my ($self, %options) = @_;
    
    $self->{output}->add_option_msg(short_msg => $options{msg});
    $self->{output}->option_exit(exit_litteral => $self->{wsman_errors_exit});
}

sub set_error {
    my ($self, %options) = @_;
    # $options{error_msg} = string error
    # $options{error_status} = integer status
    
    $self->{error_status} = defined($options{error_status}) ? $options{error_status} : 0;
    $self->{error_msg} = defined($options{error_msg}) ? $options{error_msg} : undef;
}

sub error_status {
     my ($self) = @_;
    
    return $self->{error_status};
}

sub error {
    my ($self) = @_;
    
    return $self->{error_msg};
}

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

    my $host = $self->{wsman_params}->{host};
    return $host;
}

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

    return $self->{wsman_params}->{wsman_port};
}

1;

__END__

=head1 NAME

WSMAN global

=head1 SYNOPSIS

wsman class

=head1 WSMAN OPTIONS

Need at least openwsman-perl version >= 2.4.0

=over 8

=item B<--hostname>

Hostname to query (required).

=item B<--wsman-port>

Port (default: 5985).

=item B<--wsman-path>

Set path of URL (default: '/wsman').

=item B<--wsman-scheme>

Set transport scheme (default: 'http').

=item B<--wsman-username>

Set username for authentification.

=item B<--wsman-password>

Set username password for authentification.

=item B<--wsman-timeout>

Set HTTP Transport Timeout in seconds (default: 30).

=item B<--wsman-auth-method>

Set the authentification method (default: 'basic').

=item B<--wsman-proxy-url>

Set HTTP proxy URL.

=item B<--wsman-proxy-username>

Set the proxy username.

=item B<--wsman-proxy-password>

Set the proxy password.

=item B<--wsman-debug>

Set openwsman debug on (Only for test purpose).

=item B<--wsman-errors-exit>

Exit code for wsman Errors (default: unknown)

=back

=head1 DESCRIPTION

B<wsman>.

=cut