#
# Copyright 2024 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
);
my %debug_level = (
    always => -1,
    none => 0,
    error => 1,
    critical => 2,
    warning => 3,
    message => 4,
    info => 5,
    debug => 6
);


sub new {
    my ($class, %options) = @_;
    my $self  = {};
    bless $self, $class;

    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:s'          => { 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');
    }

    if (defined($self->{wsman_params}->{wsman_debug})) {
        if (!defined($debug_level{ $self->{wsman_params}->{wsman_debug} })) {
            openwsman::set_debug($debug_level{info});
        } else {
            openwsman::set_debug($debug_level{ $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');
        if (defined($options{skip_cmd})) {
            $client_options->add_option('WINRS_SKIP_CMD_SHELL', 'TRUE');
        } else {
            $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 execute_powershell {
    my ($self, %options) = @_;

    my $chunk = 8000;
    my $filename = 'C:/Windows/Temp/' . $options{label} . '-' . sprintf("%08X", rand(0xFFFFFFFF)) . '.bat';
    my $commands = [
        {
            label => 'prev1-' . $options{label},
            value => 'if not exist "C:/Windows/Temp/" mkdir C:/Windows/Temp'
        },
        {
            label => 'prev2-' . $options{label},
            value => '(echo @ECHO OFF) >> ' . $filename
        },
        {
            label => 'prev3-' . $options{label},
            value => 'echo|set /P="powershell -encodedcommand ">> ' . $filename
        }
    ];
    my $i = 0;
    foreach (unpack('(A' . $chunk . ')*', $options{content})) {
        push @$commands, {
            label => 'chunk-' . $i . '-' . $options{label},
            value => 'echo|set /P="' . $_ . '">>' . $filename
        };
    }

    push @$commands,
        {
            label => $options{label},
            value => $filename
        },
        {
            label => 'del-' . $options{label},
            value => 'del /f ' . $filename
        };

    return $self->execute_winshell_commands(
        commands => $commands,
        keep_open => 1
    );
}

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) = @_;

    return $self->{wsman_params}->{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>

Define the hostname to query (mandatory).

=item B<--wsman-port>

Define the port to connect to (default: 5985).

=item B<--wsman-path>

Define the path of the WSMAN URL if it has been customized (default: '/wsman').

=item B<--wsman-scheme>

Define the transport scheme (default: 'http').

=item B<--wsman-username>

Define the username for authentication.

=item B<--wsman-password>

Define the password associated with the user name.

=item B<--wsman-timeout>

Define the HTTP transport timeout in seconds (default: 30).

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

Define the authentication method. Available methods: noauth, basic (default), pass, digest, ntlm, gssnegotiate.

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

Define the URL of the HTTP proxy to use.

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

Define the user name to authenticate to the proxy server.

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

Define the password to authenticate to the proxy server.

=item B<--wsman-debug>

Define the openwsman log level. Available levels: error, critical, warning, message, info (default), debug.

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

Define the expected exit code when wsman errors occur (default: unknown).

=back

=head1 DESCRIPTION

B<wsman>.

=cut
