winrm 2.2.3 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.rubocop.yml +13 -1
- data/.travis.yml +10 -11
- data/Gemfile +2 -3
- data/README.md +1 -1
- data/Rakefile +3 -4
- data/appveyor.yml +1 -2
- data/bin/rwinrm +90 -97
- data/changelog.md +5 -0
- data/lib/winrm.rb +3 -5
- data/lib/winrm/connection.rb +84 -86
- data/lib/winrm/connection_opts.rb +90 -91
- data/lib/winrm/exceptions.rb +14 -2
- data/lib/winrm/http/response_handler.rb +127 -96
- data/lib/winrm/http/transport.rb +462 -427
- data/lib/winrm/http/transport_factory.rb +1 -5
- data/lib/winrm/output.rb +1 -2
- data/lib/winrm/psrp/fragment.rb +0 -2
- data/lib/winrm/psrp/message.rb +1 -3
- data/lib/winrm/psrp/message_data.rb +0 -2
- data/lib/winrm/psrp/message_data/base.rb +0 -2
- data/lib/winrm/psrp/message_data/error_record.rb +0 -2
- data/lib/winrm/psrp/message_data/pipeline_host_call.rb +0 -2
- data/lib/winrm/psrp/message_data/pipeline_output.rb +48 -54
- data/lib/winrm/psrp/message_data/pipeline_state.rb +0 -2
- data/lib/winrm/psrp/message_data/runspacepool_host_call.rb +0 -2
- data/lib/winrm/psrp/message_data/runspacepool_state.rb +0 -2
- data/lib/winrm/psrp/message_data/session_capability.rb +0 -2
- data/lib/winrm/psrp/message_defragmenter.rb +2 -2
- data/lib/winrm/psrp/message_factory.rb +2 -3
- data/lib/winrm/psrp/message_fragmenter.rb +1 -3
- data/lib/winrm/psrp/powershell_output_decoder.rb +0 -2
- data/lib/winrm/psrp/receive_response_reader.rb +3 -5
- data/lib/winrm/psrp/uuid.rb +1 -2
- data/lib/winrm/shells/base.rb +6 -4
- data/lib/winrm/shells/cmd.rb +63 -65
- data/lib/winrm/shells/power_shell.rb +207 -202
- data/lib/winrm/shells/retryable.rb +44 -45
- data/lib/winrm/shells/shell_factory.rb +0 -2
- data/lib/winrm/version.rb +1 -3
- data/lib/winrm/wsmv/base.rb +0 -2
- data/lib/winrm/wsmv/cleanup_command.rb +1 -2
- data/lib/winrm/wsmv/close_shell.rb +1 -2
- data/lib/winrm/wsmv/command.rb +2 -3
- data/lib/winrm/wsmv/command_output.rb +2 -3
- data/lib/winrm/wsmv/command_output_decoder.rb +1 -2
- data/lib/winrm/wsmv/configuration.rb +0 -2
- data/lib/winrm/wsmv/create_pipeline.rb +0 -2
- data/lib/winrm/wsmv/create_shell.rb +2 -6
- data/lib/winrm/wsmv/header.rb +213 -215
- data/lib/winrm/wsmv/init_runspace_pool.rb +96 -95
- data/lib/winrm/wsmv/iso8601_duration.rb +0 -2
- data/lib/winrm/wsmv/keep_alive.rb +0 -2
- data/lib/winrm/wsmv/receive_response_reader.rb +128 -126
- data/lib/winrm/wsmv/send_data.rb +0 -2
- data/lib/winrm/wsmv/soap.rb +0 -2
- data/lib/winrm/wsmv/wql_pull.rb +54 -56
- data/lib/winrm/wsmv/wql_query.rb +98 -99
- data/lib/winrm/wsmv/write_stdin.rb +0 -2
- data/tests/integration/auth_timeout_spec.rb +0 -1
- data/tests/integration/cmd_spec.rb +2 -3
- data/tests/integration/issue_59_spec.rb +0 -1
- data/tests/integration/powershell_spec.rb +4 -5
- data/tests/integration/spec_helper.rb +3 -6
- data/tests/integration/transport_spec.rb +0 -1
- data/tests/integration/wql_spec.rb +33 -34
- data/tests/matchers.rb +2 -3
- data/tests/spec/configuration_spec.rb +0 -1
- data/tests/spec/connection_spec.rb +0 -2
- data/tests/spec/exception_spec.rb +0 -1
- data/tests/spec/http/transport_factory_spec.rb +1 -3
- data/tests/spec/http/transport_spec.rb +0 -1
- data/tests/spec/output_spec.rb +4 -3
- data/tests/spec/psrp/fragment_spec.rb +0 -2
- data/tests/spec/psrp/message_data/base_spec.rb +0 -2
- data/tests/spec/psrp/message_data/error_record_spec.rb +0 -2
- data/tests/spec/psrp/message_data/pipeline_host_call_spec.rb +0 -2
- data/tests/spec/psrp/message_data/pipeline_output_spec.rb +0 -2
- data/tests/spec/psrp/message_data/pipeline_state_spec.rb +0 -2
- data/tests/spec/psrp/message_data/runspace_pool_host_call_spec.rb +0 -2
- data/tests/spec/psrp/message_data/runspacepool_state_spec.rb +0 -2
- data/tests/spec/psrp/message_data/session_capability_spec.rb +0 -2
- data/tests/spec/psrp/message_data_spec.rb +0 -2
- data/tests/spec/psrp/message_defragmenter_spec.rb +0 -2
- data/tests/spec/psrp/message_fragmenter_spec.rb +0 -2
- data/tests/spec/psrp/powershell_output_decoder_spec.rb +0 -2
- data/tests/spec/psrp/psrp_message_spec.rb +10 -7
- data/tests/spec/psrp/recieve_response_reader_spec.rb +0 -2
- data/tests/spec/psrp/uuid_spec.rb +2 -2
- data/tests/spec/response_handler_spec.rb +69 -61
- data/tests/spec/shells/base_spec.rb +7 -5
- data/tests/spec/shells/cmd_spec.rb +4 -4
- data/tests/spec/shells/powershell_spec.rb +221 -175
- data/tests/spec/spec_helper.rb +0 -1
- data/tests/spec/stubs/responses/get_omi_command_output_response.xml.erb +23 -0
- data/tests/spec/stubs/responses/get_omi_command_output_response_not_done.xml.erb +24 -0
- data/tests/spec/stubs/responses/get_omi_config_response.xml +45 -0
- data/tests/spec/stubs/responses/get_omi_powershell_keepalive_response.xml.erb +33 -0
- data/tests/spec/stubs/responses/open_shell_omi.xml +43 -0
- data/tests/spec/stubs/responses/soap_fault_omi.xml +31 -0
- data/tests/spec/wsmv/cleanup_command_spec.rb +0 -2
- data/tests/spec/wsmv/close_shell_spec.rb +0 -2
- data/tests/spec/wsmv/command_output_decoder_spec.rb +0 -2
- data/tests/spec/wsmv/command_output_spec.rb +1 -3
- data/tests/spec/wsmv/command_spec.rb +0 -2
- data/tests/spec/wsmv/configuration_spec.rb +0 -2
- data/tests/spec/wsmv/create_pipeline_spec.rb +2 -3
- data/tests/spec/wsmv/create_shell_spec.rb +6 -5
- data/tests/spec/wsmv/init_runspace_pool_spec.rb +38 -36
- data/tests/spec/wsmv/keep_alive_spec.rb +4 -4
- data/tests/spec/wsmv/receive_response_reader_spec.rb +124 -123
- data/tests/spec/wsmv/send_data_spec.rb +4 -4
- data/tests/spec/wsmv/wql_query_spec.rb +11 -13
- data/tests/spec/wsmv/write_stdin_spec.rb +0 -2
- data/winrm.gemspec +11 -12
- metadata +73 -67
@@ -1,91 +1,90 @@
|
|
1
|
-
#
|
2
|
-
#
|
3
|
-
#
|
4
|
-
#
|
5
|
-
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
config
|
32
|
-
config
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
config =
|
48
|
-
config[:
|
49
|
-
config[:
|
50
|
-
config[:
|
51
|
-
config[:
|
52
|
-
config[:
|
53
|
-
config[:
|
54
|
-
config
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
raise '
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
validate_integer(:
|
79
|
-
validate_integer(:
|
80
|
-
validate_integer(:
|
81
|
-
validate_integer(:operation_timeout)
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
value
|
87
|
-
raise "#{key} must be
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
end
|
1
|
+
# Copyright 2016 Shawn Neal <sneal@sneal.net>
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
require 'securerandom'
|
16
|
+
|
17
|
+
module WinRM
|
18
|
+
# WinRM connection options, provides defaults and validation.
|
19
|
+
class ConnectionOpts < Hash
|
20
|
+
DEFAULT_OPERATION_TIMEOUT = 60
|
21
|
+
DEFAULT_RECEIVE_TIMEOUT = DEFAULT_OPERATION_TIMEOUT + 10
|
22
|
+
DEFAULT_MAX_ENV_SIZE = 153600
|
23
|
+
DEFAULT_LOCALE = 'en-US'.freeze
|
24
|
+
DEFAULT_RETRY_DELAY = 10
|
25
|
+
DEFAULT_RETRY_LIMIT = 3
|
26
|
+
|
27
|
+
class << self
|
28
|
+
def create_with_defaults(overrides)
|
29
|
+
config = default.merge(overrides)
|
30
|
+
config = ensure_receive_timeout_is_greater_than_operation_timeout(config)
|
31
|
+
config.validate
|
32
|
+
config
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def ensure_receive_timeout_is_greater_than_operation_timeout(config)
|
38
|
+
if config[:receive_timeout] < config[:operation_timeout]
|
39
|
+
config[:receive_timeout] = config[:operation_timeout] + 10
|
40
|
+
end
|
41
|
+
config
|
42
|
+
end
|
43
|
+
|
44
|
+
def default
|
45
|
+
config = ConnectionOpts.new
|
46
|
+
config[:session_id] = SecureRandom.uuid.to_s.upcase
|
47
|
+
config[:transport] = :negotiate
|
48
|
+
config[:locale] = DEFAULT_LOCALE
|
49
|
+
config[:max_envelope_size] = DEFAULT_MAX_ENV_SIZE
|
50
|
+
config[:operation_timeout] = DEFAULT_OPERATION_TIMEOUT
|
51
|
+
config[:receive_timeout] = DEFAULT_RECEIVE_TIMEOUT
|
52
|
+
config[:retry_delay] = DEFAULT_RETRY_DELAY
|
53
|
+
config[:retry_limit] = DEFAULT_RETRY_LIMIT
|
54
|
+
config
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def validate
|
59
|
+
validate_required_fields
|
60
|
+
validate_data_types
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def validate_required_fields
|
66
|
+
raise 'endpoint is a required option' unless self[:endpoint]
|
67
|
+
|
68
|
+
if self[:client_cert]
|
69
|
+
raise 'path to client key is required' unless self[:client_key]
|
70
|
+
else
|
71
|
+
raise 'user is a required option' unless self[:user]
|
72
|
+
raise 'password is a required option' unless self[:password]
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def validate_data_types
|
77
|
+
validate_integer(:retry_limit)
|
78
|
+
validate_integer(:retry_delay)
|
79
|
+
validate_integer(:max_envelope_size)
|
80
|
+
validate_integer(:operation_timeout)
|
81
|
+
validate_integer(:receive_timeout, self[:operation_timeout])
|
82
|
+
end
|
83
|
+
|
84
|
+
def validate_integer(key, min = 0)
|
85
|
+
value = self[key]
|
86
|
+
raise "#{key} must be a Integer" unless value && value.is_a?(Integer)
|
87
|
+
raise "#{key} must be greater than #{min}" unless value > min
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
data/lib/winrm/exceptions.rb
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
# encoding: UTF-8
|
2
|
-
#
|
3
1
|
# Copyright 2010 Dan Wanek <dan.wanek@gmail.com>
|
4
2
|
#
|
5
3
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
@@ -52,6 +50,20 @@ module WinRM
|
|
52
50
|
end
|
53
51
|
end
|
54
52
|
|
53
|
+
# A Fault returned in the SOAP response. The XML node contains Code, SubCode and Reason
|
54
|
+
class WinRMSoapFault < WinRMError
|
55
|
+
attr_reader :code
|
56
|
+
attr_reader :subcode
|
57
|
+
attr_reader :reason
|
58
|
+
|
59
|
+
def initialize(code, subcode, reason)
|
60
|
+
@code = code
|
61
|
+
@subcode = subcode
|
62
|
+
@reason = reason
|
63
|
+
super("[SOAP ERROR CODE: #{code} (#{subcode})]: #{reason}")
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
55
67
|
# A Fault returned in the SOAP response. The XML node is a MSFT_WmiError
|
56
68
|
class WinRMWMIError < WinRMError
|
57
69
|
attr_reader :error_code
|
@@ -1,96 +1,127 @@
|
|
1
|
-
#
|
2
|
-
#
|
3
|
-
#
|
4
|
-
#
|
5
|
-
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
#
|
32
|
-
#
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
raise_if_auth_error
|
52
|
-
raise_if_wsman_fault
|
53
|
-
raise_if_wmi_error
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
return if
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
"
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
"
|
92
|
-
|
93
|
-
)
|
94
|
-
end
|
95
|
-
|
96
|
-
|
1
|
+
# Copyright 2014 Shawn Neal <sneal@sneal.net>
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
require 'rexml/document'
|
16
|
+
require_relative '../wsmv/soap'
|
17
|
+
|
18
|
+
module WinRM
|
19
|
+
# Handles the raw WinRM HTTP response. Returns the body as an XML doc
|
20
|
+
# or raises the appropriate WinRM error if the response is an error.
|
21
|
+
class ResponseHandler
|
22
|
+
# @param [String] The raw unparsed response body, if any
|
23
|
+
# @param [Integer] The HTTP response status code
|
24
|
+
def initialize(response_body, status_code)
|
25
|
+
@response_body = response_body
|
26
|
+
@status_code = status_code
|
27
|
+
end
|
28
|
+
|
29
|
+
# Processes the response from the WinRM service and either returns an XML
|
30
|
+
# doc or raises an appropriate error.
|
31
|
+
#
|
32
|
+
# @returns [REXML::Document] The parsed response body
|
33
|
+
def parse_to_xml
|
34
|
+
raise_if_error
|
35
|
+
response_xml
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def response_xml
|
41
|
+
@response_xml ||= REXML::Document.new(@response_body)
|
42
|
+
rescue REXML::ParseException => e
|
43
|
+
raise WinRMHTTPTransportError.new(
|
44
|
+
"Unable to parse WinRM response: #{e.message}", @status_code
|
45
|
+
)
|
46
|
+
end
|
47
|
+
|
48
|
+
def raise_if_error
|
49
|
+
return if @status_code == 200
|
50
|
+
|
51
|
+
raise_if_auth_error
|
52
|
+
raise_if_wsman_fault
|
53
|
+
raise_if_wmi_error
|
54
|
+
raise_if_soap_fault
|
55
|
+
raise_transport_error
|
56
|
+
end
|
57
|
+
|
58
|
+
def raise_if_auth_error
|
59
|
+
raise WinRMAuthorizationError if @status_code == 401
|
60
|
+
end
|
61
|
+
|
62
|
+
def raise_if_wsman_fault
|
63
|
+
soap_errors = REXML::XPath.match(
|
64
|
+
response_xml,
|
65
|
+
"//*[local-name() = 'Envelope']/*[local-name() = 'Body']/*[local-name() = 'Fault']/*"
|
66
|
+
)
|
67
|
+
return if soap_errors.empty?
|
68
|
+
|
69
|
+
fault = REXML::XPath.first(
|
70
|
+
soap_errors,
|
71
|
+
"//*[local-name() = 'WSManFault']"
|
72
|
+
)
|
73
|
+
raise WinRMWSManFault.new(fault.to_s, fault.attributes['Code']) unless fault.nil?
|
74
|
+
end
|
75
|
+
|
76
|
+
def raise_if_wmi_error
|
77
|
+
soap_errors = REXML::XPath.match(
|
78
|
+
response_xml,
|
79
|
+
"//*[local-name() = 'Envelope']/*[local-name() = 'Body']/*[local-name() = 'Fault']/*"
|
80
|
+
)
|
81
|
+
return if soap_errors.empty?
|
82
|
+
|
83
|
+
error = REXML::XPath.first(
|
84
|
+
soap_errors,
|
85
|
+
"//*[local-name() = 'MSFT_WmiError']"
|
86
|
+
)
|
87
|
+
return if error.nil?
|
88
|
+
|
89
|
+
error_code = REXML::XPath.first(
|
90
|
+
error,
|
91
|
+
"//*[local-name() = 'error_Code']"
|
92
|
+
).text
|
93
|
+
raise WinRMWMIError.new(error.to_s, error_code)
|
94
|
+
end
|
95
|
+
|
96
|
+
def raise_if_soap_fault
|
97
|
+
soap_errors = REXML::XPath.match(
|
98
|
+
response_xml,
|
99
|
+
"//*[local-name() = 'Envelope']/*[local-name() = 'Body']/*[local-name() = 'Fault']/*"
|
100
|
+
)
|
101
|
+
return if soap_errors.empty?
|
102
|
+
|
103
|
+
code = REXML::XPath.first(
|
104
|
+
soap_errors,
|
105
|
+
"//*[local-name() = 'Code']/*[local-name() = 'Value']/text()"
|
106
|
+
)
|
107
|
+
subcode = REXML::XPath.first(
|
108
|
+
soap_errors,
|
109
|
+
"//*[local-name() = 'Subcode']/*[local-name() = 'Value']/text()"
|
110
|
+
)
|
111
|
+
reason = REXML::XPath.first(
|
112
|
+
soap_errors,
|
113
|
+
"//*[local-name() = 'Reason']/*[local-name() = 'Text']/text()"
|
114
|
+
)
|
115
|
+
|
116
|
+
raise WinRMSoapFault.new(code, subcode, reason) unless
|
117
|
+
code.nil? && subcode.nil? && reason.nil?
|
118
|
+
end
|
119
|
+
|
120
|
+
def raise_transport_error
|
121
|
+
raise WinRMHTTPTransportError.new(
|
122
|
+
"Bad HTTP response returned from server. Body(if present):#{@response_body}",
|
123
|
+
@status_code
|
124
|
+
)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
data/lib/winrm/http/transport.rb
CHANGED
@@ -1,427 +1,462 @@
|
|
1
|
-
#
|
2
|
-
#
|
3
|
-
#
|
4
|
-
#
|
5
|
-
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
#
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
@
|
30
|
-
@httpcli =
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
#
|
36
|
-
#
|
37
|
-
#
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
resp
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
ssl_connection
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
raise "ssl fingerprint mismatch!!!!\n"
|
107
|
-
end
|
108
|
-
|
109
|
-
protected
|
110
|
-
|
111
|
-
def body(message, length, type = 'application/HTTP-SPNEGO-session-encrypted')
|
112
|
-
[
|
113
|
-
'--Encrypted Boundary',
|
114
|
-
"Content-Type: #{type}",
|
115
|
-
"OriginalContent: type=application/soap+xml;charset=UTF-8;Length=#{length}",
|
116
|
-
'--Encrypted Boundary',
|
117
|
-
'Content-Type: application/octet-stream',
|
118
|
-
"#{message}--Encrypted Boundary--"
|
119
|
-
].join("\r\n").concat("\r\n")
|
120
|
-
end
|
121
|
-
|
122
|
-
def log_soap_message(message)
|
123
|
-
return unless @logger.debug?
|
124
|
-
|
125
|
-
xml_msg = REXML::Document.new(message)
|
126
|
-
formatter = REXML::Formatters::Pretty.new(2)
|
127
|
-
formatter.compact = true
|
128
|
-
formatter.write(xml_msg, @logger)
|
129
|
-
@logger.debug("\n")
|
130
|
-
rescue StandardError => e
|
131
|
-
@logger.debug("Couldn't log SOAP request/response: #{e.message} - #{message}")
|
132
|
-
end
|
133
|
-
end
|
134
|
-
|
135
|
-
# Plain text, insecure, HTTP transport
|
136
|
-
class HttpPlaintext < HttpTransport
|
137
|
-
def initialize(endpoint, user, pass, opts)
|
138
|
-
super(endpoint, opts)
|
139
|
-
@httpcli.set_auth(nil, user, pass)
|
140
|
-
no_sspi_auth! if opts[:disable_sspi]
|
141
|
-
basic_auth_only! if opts[:basic_auth_only]
|
142
|
-
no_ssl_peer_verification! if opts[:no_ssl_peer_verification]
|
143
|
-
end
|
144
|
-
end
|
145
|
-
|
146
|
-
# NTLM/Negotiate, secure, HTTP transport
|
147
|
-
class HttpNegotiate < HttpTransport
|
148
|
-
def initialize(endpoint, user, pass, opts)
|
149
|
-
super(endpoint, opts)
|
150
|
-
require 'rubyntlm'
|
151
|
-
no_sspi_auth!
|
152
|
-
|
153
|
-
user_parts = user.split('\\')
|
154
|
-
if user_parts.length > 1
|
155
|
-
opts[:domain] = user_parts[0]
|
156
|
-
user = user_parts[1]
|
157
|
-
end
|
158
|
-
|
159
|
-
@ntlmcli = Net::NTLM::Client.new(user, pass, opts)
|
160
|
-
@retryable = true
|
161
|
-
no_ssl_peer_verification! if opts[:no_ssl_peer_verification]
|
162
|
-
@ssl_peer_fingerprint = opts[:ssl_peer_fingerprint]
|
163
|
-
@httpcli.ssl_config.set_trust_ca(opts[:ca_trust_path]) if opts[:ca_trust_path]
|
164
|
-
end
|
165
|
-
|
166
|
-
def send_request(message
|
167
|
-
ssl_peer_fingerprint_verification!
|
168
|
-
|
169
|
-
log_soap_message(message)
|
170
|
-
|
171
|
-
hdr = {
|
172
|
-
'Content-Type' => 'multipart/encrypted;'\
|
173
|
-
'protocol="application/HTTP-SPNEGO-session-encrypted";boundary="Encrypted Boundary"'
|
174
|
-
}
|
175
|
-
|
176
|
-
|
177
|
-
resp
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
send_request(message
|
182
|
-
else
|
183
|
-
@retryable = true
|
184
|
-
decrypted_body = winrm_decrypt(resp
|
185
|
-
log_soap_message(decrypted_body)
|
186
|
-
WinRM::ResponseHandler.new(decrypted_body, resp.status).parse_to_xml
|
187
|
-
end
|
188
|
-
end
|
189
|
-
|
190
|
-
private
|
191
|
-
|
192
|
-
def seal(message)
|
193
|
-
emessage = @ntlmcli.session.seal_message message
|
194
|
-
signature = @ntlmcli.session.sign_message message
|
195
|
-
"\x10\x00\x00\x00#{signature}#{emessage}"
|
196
|
-
end
|
197
|
-
|
198
|
-
def winrm_decrypt(
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
'
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
def
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
resp
|
313
|
-
@
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
resp
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_FLAG_ALLOCATE)
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
iov0[:buffer].
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
1
|
+
# Copyright 2010 Dan Wanek <dan.wanek@gmail.com>
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
require 'httpclient'
|
16
|
+
require_relative 'response_handler'
|
17
|
+
|
18
|
+
module WinRM
|
19
|
+
module HTTP
|
20
|
+
# A generic HTTP transport that utilized HTTPClient to send messages back and forth.
|
21
|
+
# This backend will maintain state for every WinRMWebService instance that is instantiated so it
|
22
|
+
# is possible to use GSSAPI with Keep-Alive.
|
23
|
+
class HttpTransport
|
24
|
+
attr_reader :endpoint
|
25
|
+
|
26
|
+
def initialize(endpoint, options)
|
27
|
+
@endpoint = endpoint.is_a?(String) ? URI.parse(endpoint) : endpoint
|
28
|
+
@httpcli = HTTPClient.new(agent_name: 'Ruby WinRM Client')
|
29
|
+
@logger = Logging.logger[self]
|
30
|
+
@httpcli.receive_timeout = options[:receive_timeout]
|
31
|
+
end
|
32
|
+
|
33
|
+
# Sends the SOAP payload to the WinRM service and returns the service's
|
34
|
+
# SOAP response. If an error occurrs an appropriate error is raised.
|
35
|
+
#
|
36
|
+
# @param [String] The XML SOAP message
|
37
|
+
# @returns [REXML::Document] The parsed response body
|
38
|
+
def send_request(message)
|
39
|
+
ssl_peer_fingerprint_verification!
|
40
|
+
log_soap_message(message)
|
41
|
+
hdr = { 'Content-Type' => 'application/soap+xml;charset=UTF-8',
|
42
|
+
'Content-Length' => message.bytesize }
|
43
|
+
# We need to add this header if using Client Certificate authentication
|
44
|
+
unless @httpcli.ssl_config.client_cert.nil?
|
45
|
+
hdr['Authorization'] = 'http://schemas.dmtf.org/wbem/wsman/1/wsman/secprofile/https/mutual'
|
46
|
+
end
|
47
|
+
|
48
|
+
resp = @httpcli.post(@endpoint, message, hdr)
|
49
|
+
log_soap_message(resp.http_body.content)
|
50
|
+
verify_ssl_fingerprint(resp.peer_cert)
|
51
|
+
handler = WinRM::ResponseHandler.new(resp.http_body.content, resp.status)
|
52
|
+
handler.parse_to_xml
|
53
|
+
end
|
54
|
+
|
55
|
+
# We'll need this to force basic authentication if desired
|
56
|
+
def basic_auth_only!
|
57
|
+
auths = @httpcli.www_auth.instance_variable_get('@authenticator')
|
58
|
+
auths.delete_if { |i| i.scheme !~ /basic/i }
|
59
|
+
end
|
60
|
+
|
61
|
+
# Disable SSPI Auth
|
62
|
+
def no_sspi_auth!
|
63
|
+
auths = @httpcli.www_auth.instance_variable_get('@authenticator')
|
64
|
+
auths.delete_if { |i| i.is_a? HTTPClient::SSPINegotiateAuth }
|
65
|
+
end
|
66
|
+
|
67
|
+
# Disable SSL Peer Verification
|
68
|
+
def no_ssl_peer_verification!
|
69
|
+
@httpcli.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
70
|
+
end
|
71
|
+
|
72
|
+
# SSL Peer Fingerprint Verification prior to connecting
|
73
|
+
def ssl_peer_fingerprint_verification!
|
74
|
+
return unless @ssl_peer_fingerprint && !@ssl_peer_fingerprint_verified
|
75
|
+
|
76
|
+
with_untrusted_ssl_connection do |connection|
|
77
|
+
connection_cert = connection.peer_cert_chain.last
|
78
|
+
verify_ssl_fingerprint(connection_cert)
|
79
|
+
end
|
80
|
+
@logger.info("initial ssl fingerprint #{@ssl_peer_fingerprint} verified\n")
|
81
|
+
@ssl_peer_fingerprint_verified = true
|
82
|
+
no_ssl_peer_verification!
|
83
|
+
end
|
84
|
+
|
85
|
+
# Connect without verification to retrieve untrusted ssl context
|
86
|
+
def with_untrusted_ssl_connection
|
87
|
+
noverify_peer_context = OpenSSL::SSL::SSLContext.new
|
88
|
+
noverify_peer_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
89
|
+
tcp_connection = TCPSocket.new(@endpoint.host, @endpoint.port)
|
90
|
+
begin
|
91
|
+
ssl_connection = OpenSSL::SSL::SSLSocket.new(tcp_connection, noverify_peer_context)
|
92
|
+
ssl_connection.connect
|
93
|
+
yield ssl_connection
|
94
|
+
ensure
|
95
|
+
tcp_connection.close
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# compare @ssl_peer_fingerprint to current ssl context
|
100
|
+
def verify_ssl_fingerprint(cert)
|
101
|
+
return unless @ssl_peer_fingerprint
|
102
|
+
|
103
|
+
conn_fingerprint = OpenSSL::Digest::SHA1.new(cert.to_der).to_s
|
104
|
+
return unless @ssl_peer_fingerprint.casecmp(conn_fingerprint) != 0
|
105
|
+
|
106
|
+
raise "ssl fingerprint mismatch!!!!\n"
|
107
|
+
end
|
108
|
+
|
109
|
+
protected
|
110
|
+
|
111
|
+
def body(message, length, type = 'application/HTTP-SPNEGO-session-encrypted')
|
112
|
+
[
|
113
|
+
'--Encrypted Boundary',
|
114
|
+
"Content-Type: #{type}",
|
115
|
+
"OriginalContent: type=application/soap+xml;charset=UTF-8;Length=#{length}",
|
116
|
+
'--Encrypted Boundary',
|
117
|
+
'Content-Type: application/octet-stream',
|
118
|
+
"#{message}--Encrypted Boundary--"
|
119
|
+
].join("\r\n").concat("\r\n")
|
120
|
+
end
|
121
|
+
|
122
|
+
def log_soap_message(message)
|
123
|
+
return unless @logger.debug?
|
124
|
+
|
125
|
+
xml_msg = REXML::Document.new(message)
|
126
|
+
formatter = REXML::Formatters::Pretty.new(2)
|
127
|
+
formatter.compact = true
|
128
|
+
formatter.write(xml_msg, @logger)
|
129
|
+
@logger.debug("\n")
|
130
|
+
rescue StandardError => e
|
131
|
+
@logger.debug("Couldn't log SOAP request/response: #{e.message} - #{message}")
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# Plain text, insecure, HTTP transport
|
136
|
+
class HttpPlaintext < HttpTransport
|
137
|
+
def initialize(endpoint, user, pass, opts)
|
138
|
+
super(endpoint, opts)
|
139
|
+
@httpcli.set_auth(nil, user, pass)
|
140
|
+
no_sspi_auth! if opts[:disable_sspi]
|
141
|
+
basic_auth_only! if opts[:basic_auth_only]
|
142
|
+
no_ssl_peer_verification! if opts[:no_ssl_peer_verification]
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# NTLM/Negotiate, secure, HTTP transport
|
147
|
+
class HttpNegotiate < HttpTransport
|
148
|
+
def initialize(endpoint, user, pass, opts)
|
149
|
+
super(endpoint, opts)
|
150
|
+
require 'rubyntlm'
|
151
|
+
no_sspi_auth!
|
152
|
+
|
153
|
+
user_parts = user.split('\\')
|
154
|
+
if user_parts.length > 1
|
155
|
+
opts[:domain] = user_parts[0]
|
156
|
+
user = user_parts[1]
|
157
|
+
end
|
158
|
+
|
159
|
+
@ntlmcli = Net::NTLM::Client.new(user, pass, opts)
|
160
|
+
@retryable = true
|
161
|
+
no_ssl_peer_verification! if opts[:no_ssl_peer_verification]
|
162
|
+
@ssl_peer_fingerprint = opts[:ssl_peer_fingerprint]
|
163
|
+
@httpcli.ssl_config.set_trust_ca(opts[:ca_trust_path]) if opts[:ca_trust_path]
|
164
|
+
end
|
165
|
+
|
166
|
+
def send_request(message)
|
167
|
+
ssl_peer_fingerprint_verification!
|
168
|
+
init_auth if @ntlmcli.session.nil?
|
169
|
+
log_soap_message(message)
|
170
|
+
|
171
|
+
hdr = {
|
172
|
+
'Content-Type' => 'multipart/encrypted;'\
|
173
|
+
'protocol="application/HTTP-SPNEGO-session-encrypted";boundary="Encrypted Boundary"'
|
174
|
+
}
|
175
|
+
|
176
|
+
resp = @httpcli.post(@endpoint, body(seal(message), message.bytesize), hdr)
|
177
|
+
verify_ssl_fingerprint(resp.peer_cert)
|
178
|
+
if resp.status == 401 && @retryable
|
179
|
+
@retryable = false
|
180
|
+
init_auth
|
181
|
+
send_request(message)
|
182
|
+
else
|
183
|
+
@retryable = true
|
184
|
+
decrypted_body = winrm_decrypt(resp)
|
185
|
+
log_soap_message(decrypted_body)
|
186
|
+
WinRM::ResponseHandler.new(decrypted_body, resp.status).parse_to_xml
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
private
|
191
|
+
|
192
|
+
def seal(message)
|
193
|
+
emessage = @ntlmcli.session.seal_message message
|
194
|
+
signature = @ntlmcli.session.sign_message message
|
195
|
+
"\x10\x00\x00\x00#{signature}#{emessage}"
|
196
|
+
end
|
197
|
+
|
198
|
+
def winrm_decrypt(resp)
|
199
|
+
# OMI server doesn't always respond to encrypted messages with encrypted responses over SSL
|
200
|
+
return resp.body if resp.header['Content-Type'].first =~ %r{\Aapplication\/soap\+xml}i
|
201
|
+
return '' if resp.body.empty?
|
202
|
+
|
203
|
+
str = resp.body.force_encoding('BINARY')
|
204
|
+
str.sub!(%r{^.*Content-Type: application\/octet-stream\r\n(.*)--Encrypted.*$}m, '\1')
|
205
|
+
|
206
|
+
signature = str[4..19]
|
207
|
+
message = @ntlmcli.session.unseal_message str[20..-1]
|
208
|
+
return message if @ntlmcli.session.verify_signature(signature, message)
|
209
|
+
|
210
|
+
raise WinRMHTTPTransportError, 'Could not decrypt NTLM message.'
|
211
|
+
end
|
212
|
+
|
213
|
+
def issue_challenge_response(negotiate)
|
214
|
+
auth_header = {
|
215
|
+
'Authorization' => "Negotiate #{negotiate.encode64}",
|
216
|
+
'Content-Type' => 'application/soap+xml;charset=UTF-8'
|
217
|
+
}
|
218
|
+
|
219
|
+
# OMI Server on Linux requires an empty payload with the new auth header to proceed
|
220
|
+
# because the config check for max payload size will otherwise break the auth handshake
|
221
|
+
# given the OMI server does not support that check
|
222
|
+
@httpcli.post(@endpoint, '', auth_header)
|
223
|
+
|
224
|
+
# return an empty hash of headers for subsequent requests to use
|
225
|
+
{}
|
226
|
+
end
|
227
|
+
|
228
|
+
def init_auth
|
229
|
+
@logger.debug "Initializing Negotiate for #{@endpoint}"
|
230
|
+
auth1 = @ntlmcli.init_context
|
231
|
+
hdr = {
|
232
|
+
'Authorization' => "Negotiate #{auth1.encode64}",
|
233
|
+
'Content-Type' => 'application/soap+xml;charset=UTF-8'
|
234
|
+
}
|
235
|
+
@logger.debug 'Sending HTTP POST for Negotiate Authentication'
|
236
|
+
r = @httpcli.post(@endpoint, '', hdr)
|
237
|
+
verify_ssl_fingerprint(r.peer_cert)
|
238
|
+
auth_header = r.header['WWW-Authenticate'].pop
|
239
|
+
unless auth_header
|
240
|
+
msg = "Unable to parse authorization header. Headers: #{r.headers}\r\nBody: #{r.body}"
|
241
|
+
raise WinRMHTTPTransportError.new(msg, r.status_code)
|
242
|
+
end
|
243
|
+
itok = auth_header.split.last
|
244
|
+
auth3 = @ntlmcli.init_context(itok, channel_binding(r))
|
245
|
+
issue_challenge_response(auth3)
|
246
|
+
end
|
247
|
+
|
248
|
+
def channel_binding(response)
|
249
|
+
if response.peer_cert.nil?
|
250
|
+
nil
|
251
|
+
else
|
252
|
+
cert = if RUBY_PLATFORM == 'java'
|
253
|
+
OpenSSL::X509::Certificate.new(response.peer_cert.cert.getEncoded)
|
254
|
+
else
|
255
|
+
response.peer_cert
|
256
|
+
end
|
257
|
+
Net::NTLM::ChannelBinding.create(OpenSSL::X509::Certificate.new(cert))
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
# Uses SSL to secure the transport
|
263
|
+
class BasicAuthSSL < HttpTransport
|
264
|
+
def initialize(endpoint, user, pass, opts)
|
265
|
+
super(endpoint, opts)
|
266
|
+
@httpcli.set_auth(endpoint, user, pass)
|
267
|
+
basic_auth_only!
|
268
|
+
no_ssl_peer_verification! if opts[:no_ssl_peer_verification]
|
269
|
+
@ssl_peer_fingerprint = opts[:ssl_peer_fingerprint]
|
270
|
+
@httpcli.ssl_config.set_trust_ca(opts[:ca_trust_path]) if opts[:ca_trust_path]
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
# Uses Client Certificate to authenticate and SSL to secure the transport
|
275
|
+
class ClientCertAuthSSL < HttpTransport
|
276
|
+
def initialize(endpoint, client_cert, client_key, key_pass, opts)
|
277
|
+
super(endpoint, opts)
|
278
|
+
@httpcli.ssl_config.set_client_cert_file(client_cert, client_key, key_pass)
|
279
|
+
@httpcli.www_auth.instance_variable_set('@authenticator', [])
|
280
|
+
no_ssl_peer_verification! if opts[:no_ssl_peer_verification]
|
281
|
+
@ssl_peer_fingerprint = opts[:ssl_peer_fingerprint]
|
282
|
+
@httpcli.ssl_config.set_trust_ca(opts[:ca_trust_path]) if opts[:ca_trust_path]
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
# Uses Kerberos/GSSAPI to authenticate and encrypt messages
|
287
|
+
class HttpGSSAPI < HttpTransport
|
288
|
+
# @param [String,URI] endpoint the WinRM webservice endpoint
|
289
|
+
# @param [String] realm the Kerberos realm we are authenticating to
|
290
|
+
# @param [String<optional>] service the service name, default is HTTP
|
291
|
+
def initialize(endpoint, realm, opts, service = nil)
|
292
|
+
require 'gssapi'
|
293
|
+
require 'gssapi/extensions'
|
294
|
+
|
295
|
+
super(endpoint, opts)
|
296
|
+
# Remove the GSSAPI auth from HTTPClient because we are doing our own thing
|
297
|
+
no_sspi_auth!
|
298
|
+
service ||= 'HTTP'
|
299
|
+
@service = "#{service}/#{@endpoint.host}@#{realm}"
|
300
|
+
no_ssl_peer_verification! if opts[:no_ssl_peer_verification]
|
301
|
+
init_krb
|
302
|
+
end
|
303
|
+
|
304
|
+
# Sends the SOAP payload to the WinRM service and returns the service's
|
305
|
+
# SOAP response. If an error occurrs an appropriate error is raised.
|
306
|
+
#
|
307
|
+
# @param [String] The XML SOAP message
|
308
|
+
# @returns [REXML::Document] The parsed response body
|
309
|
+
def send_request(message)
|
310
|
+
resp = send_kerberos_request(message)
|
311
|
+
|
312
|
+
if resp.status == 401
|
313
|
+
@logger.debug 'Got 401 - reinitializing Kerberos and retrying one more time'
|
314
|
+
init_krb
|
315
|
+
resp = send_kerberos_request(message)
|
316
|
+
end
|
317
|
+
|
318
|
+
handler = WinRM::ResponseHandler.new(winrm_decrypt(resp.http_body.content), resp.status)
|
319
|
+
handler.parse_to_xml
|
320
|
+
end
|
321
|
+
|
322
|
+
private
|
323
|
+
|
324
|
+
# Sends the SOAP payload to the WinRM service and returns the service's
|
325
|
+
# HTTP response.
|
326
|
+
#
|
327
|
+
# @param [String] The XML SOAP message
|
328
|
+
# @returns [Object] The HTTP response object
|
329
|
+
def send_kerberos_request(message)
|
330
|
+
log_soap_message(message)
|
331
|
+
original_length = message.bytesize
|
332
|
+
pad_len, emsg = winrm_encrypt(message)
|
333
|
+
req_length = original_length + pad_len
|
334
|
+
hdr = {
|
335
|
+
'Connection' => 'Keep-Alive',
|
336
|
+
'Content-Type' => 'multipart/encrypted;' \
|
337
|
+
'protocol="application/HTTP-Kerberos-session-encrypted";boundary="Encrypted Boundary"'
|
338
|
+
}
|
339
|
+
|
340
|
+
resp = @httpcli.post(
|
341
|
+
@endpoint,
|
342
|
+
body(emsg, req_length, 'application/HTTP-Kerberos-session-encrypted'),
|
343
|
+
hdr
|
344
|
+
)
|
345
|
+
log_soap_message(resp.http_body.content)
|
346
|
+
resp
|
347
|
+
end
|
348
|
+
|
349
|
+
def init_krb
|
350
|
+
@logger.debug "Initializing Kerberos for #{@service}"
|
351
|
+
@gsscli = GSSAPI::Simple.new(@endpoint.host, @service)
|
352
|
+
token = @gsscli.init_context
|
353
|
+
auth = Base64.strict_encode64 token
|
354
|
+
|
355
|
+
hdr = {
|
356
|
+
'Authorization' => "Kerberos #{auth}",
|
357
|
+
'Connection' => 'Keep-Alive',
|
358
|
+
'Content-Type' => 'application/soap+xml;charset=UTF-8'
|
359
|
+
}
|
360
|
+
@logger.debug 'Sending HTTP POST for Kerberos Authentication'
|
361
|
+
r = @httpcli.post(@endpoint, '', hdr)
|
362
|
+
itok = r.header['WWW-Authenticate'].pop
|
363
|
+
itok = itok.split.last
|
364
|
+
itok = Base64.strict_decode64(itok)
|
365
|
+
@gsscli.init_context(itok)
|
366
|
+
end
|
367
|
+
|
368
|
+
# rubocop:disable Metrics/MethodLength
|
369
|
+
# rubocop:disable Metrics/AbcSize
|
370
|
+
|
371
|
+
# @return [String] the encrypted request string
|
372
|
+
def winrm_encrypt(str)
|
373
|
+
@logger.debug "Encrypting SOAP message:\n#{str}"
|
374
|
+
iov_cnt = 3
|
375
|
+
iov = FFI::MemoryPointer.new(GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * iov_cnt)
|
376
|
+
|
377
|
+
iov0 = GSSAPI::LibGSSAPI::GssIOVBufferDesc.new(FFI::Pointer.new(iov.address))
|
378
|
+
iov0[:type] = (GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_HEADER | \
|
379
|
+
GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_FLAG_ALLOCATE)
|
380
|
+
|
381
|
+
iov1 = GSSAPI::LibGSSAPI::GssIOVBufferDesc.new(
|
382
|
+
FFI::Pointer.new(iov.address + (GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * 1))
|
383
|
+
)
|
384
|
+
iov1[:type] = GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_DATA
|
385
|
+
iov1[:buffer].value = str
|
386
|
+
|
387
|
+
iov2 = GSSAPI::LibGSSAPI::GssIOVBufferDesc.new(
|
388
|
+
FFI::Pointer.new(iov.address + (GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * 2))
|
389
|
+
)
|
390
|
+
iov2[:type] = (GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_PADDING | \
|
391
|
+
GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_FLAG_ALLOCATE)
|
392
|
+
|
393
|
+
conf_state = FFI::MemoryPointer.new :uint32
|
394
|
+
min_stat = FFI::MemoryPointer.new :uint32
|
395
|
+
|
396
|
+
GSSAPI::LibGSSAPI.gss_wrap_iov(
|
397
|
+
min_stat,
|
398
|
+
@gsscli.context,
|
399
|
+
1,
|
400
|
+
GSSAPI::LibGSSAPI::GSS_C_QOP_DEFAULT,
|
401
|
+
conf_state,
|
402
|
+
iov,
|
403
|
+
iov_cnt
|
404
|
+
)
|
405
|
+
|
406
|
+
token = [iov0[:buffer].length].pack('L')
|
407
|
+
token += iov0[:buffer].value
|
408
|
+
token += iov1[:buffer].value
|
409
|
+
pad_len = iov2[:buffer].length
|
410
|
+
token += iov2[:buffer].value if pad_len > 0
|
411
|
+
[pad_len, token]
|
412
|
+
end
|
413
|
+
|
414
|
+
# @return [String] the unencrypted response string
|
415
|
+
def winrm_decrypt(str)
|
416
|
+
@logger.debug "Decrypting SOAP message:\n#{str}"
|
417
|
+
iov_cnt = 3
|
418
|
+
iov = FFI::MemoryPointer.new(GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * iov_cnt)
|
419
|
+
|
420
|
+
iov0 = GSSAPI::LibGSSAPI::GssIOVBufferDesc.new(FFI::Pointer.new(iov.address))
|
421
|
+
iov0[:type] = (GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_HEADER | \
|
422
|
+
GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_FLAG_ALLOCATE)
|
423
|
+
|
424
|
+
iov1 = GSSAPI::LibGSSAPI::GssIOVBufferDesc.new(
|
425
|
+
FFI::Pointer.new(iov.address + (GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * 1))
|
426
|
+
)
|
427
|
+
iov1[:type] = GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_DATA
|
428
|
+
|
429
|
+
iov2 = GSSAPI::LibGSSAPI::GssIOVBufferDesc.new(
|
430
|
+
FFI::Pointer.new(iov.address + (GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * 2))
|
431
|
+
)
|
432
|
+
iov2[:type] = GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_DATA
|
433
|
+
|
434
|
+
str.force_encoding('BINARY')
|
435
|
+
str.sub!(%r{^.*Content-Type: application\/octet-stream\r\n(.*)--Encrypted.*$}m, '\1')
|
436
|
+
|
437
|
+
len = str.unpack('L').first
|
438
|
+
iov_data = str.unpack("LA#{len}A*")
|
439
|
+
iov0[:buffer].value = iov_data[1]
|
440
|
+
iov1[:buffer].value = iov_data[2]
|
441
|
+
|
442
|
+
min_stat = FFI::MemoryPointer.new :uint32
|
443
|
+
conf_state = FFI::MemoryPointer.new :uint32
|
444
|
+
conf_state.write_int(1)
|
445
|
+
qop_state = FFI::MemoryPointer.new :uint32
|
446
|
+
qop_state.write_int(0)
|
447
|
+
|
448
|
+
maj_stat = GSSAPI::LibGSSAPI.gss_unwrap_iov(
|
449
|
+
min_stat, @gsscli.context, conf_state, qop_state, iov, iov_cnt
|
450
|
+
)
|
451
|
+
|
452
|
+
@logger.debug "SOAP message decrypted (MAJ: #{maj_stat}, " \
|
453
|
+
"MIN: #{min_stat.read_int}):\n#{iov1[:buffer].value}"
|
454
|
+
|
455
|
+
iov1[:buffer].value
|
456
|
+
end
|
457
|
+
# rubocop:enable Metrics/MethodLength
|
458
|
+
# rubocop:enable Metrics/AbcSize
|
459
|
+
end
|
460
|
+
end
|
461
|
+
end
|
462
|
+
# WinRM
|