winrm 1.7.0 → 1.7.1
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 +4 -4
- data/.gitignore +10 -10
- data/.rspec +3 -3
- data/.rubocop.yml +12 -12
- data/.travis.yml +12 -12
- data/Gemfile +9 -9
- data/LICENSE +202 -202
- data/README.md +194 -194
- data/Rakefile +36 -36
- data/Vagrantfile +9 -9
- data/appveyor.yml +42 -42
- data/bin/rwinrm +97 -97
- data/changelog.md +74 -71
- data/lib/winrm.rb +42 -42
- data/lib/winrm/command_executor.rb +224 -224
- data/lib/winrm/exceptions/exceptions.rb +57 -57
- data/lib/winrm/helpers/iso8601_duration.rb +58 -58
- data/lib/winrm/helpers/powershell_script.rb +42 -42
- data/lib/winrm/http/response_handler.rb +82 -82
- data/lib/winrm/http/transport.rb +421 -421
- data/lib/winrm/output.rb +43 -43
- data/lib/winrm/soap_provider.rb +39 -39
- data/lib/winrm/version.rb +7 -7
- data/lib/winrm/winrm_service.rb +556 -556
- data/preamble +17 -17
- data/spec/auth_timeout_spec.rb +16 -16
- data/spec/cmd_spec.rb +102 -102
- data/spec/command_executor_spec.rb +429 -429
- data/spec/config-example.yml +19 -19
- data/spec/exception_spec.rb +50 -50
- data/spec/issue_184_spec.rb +67 -67
- data/spec/issue_59_spec.rb +23 -23
- data/spec/matchers.rb +74 -74
- data/spec/output_spec.rb +110 -110
- data/spec/powershell_spec.rb +97 -97
- data/spec/response_handler_spec.rb +59 -59
- data/spec/spec_helper.rb +73 -73
- data/spec/stubs/responses/get_command_output_response.xml.erb +13 -13
- data/spec/stubs/responses/open_shell_v1.xml +19 -19
- data/spec/stubs/responses/open_shell_v2.xml +20 -20
- data/spec/stubs/responses/soap_fault_v1.xml +36 -36
- data/spec/stubs/responses/soap_fault_v2.xml +42 -42
- data/spec/stubs/responses/wmi_error_v2.xml +41 -41
- data/spec/transport_spec.rb +124 -124
- data/spec/winrm_options_spec.rb +76 -76
- data/spec/winrm_primitives_spec.rb +51 -51
- data/spec/wql_spec.rb +14 -14
- data/winrm.gemspec +40 -40
- metadata +2 -2
@@ -1,58 +1,58 @@
|
|
1
|
-
# encoding: UTF-8
|
2
|
-
#
|
3
|
-
# Copyright 2010 Dan Wanek <dan.wanek@gmail.com>
|
4
|
-
#
|
5
|
-
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
-
# you may not use this file except in compliance with the License.
|
7
|
-
# You may obtain a copy of the License at
|
8
|
-
#
|
9
|
-
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
-
#
|
11
|
-
# Unless required by applicable law or agreed to in writing, software
|
12
|
-
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
-
# See the License for the specific language governing permissions and
|
15
|
-
# limitations under the License.
|
16
|
-
|
17
|
-
# rubocop:disable Metrics/MethodLength
|
18
|
-
# rubocop:disable Metrics/AbcSize
|
19
|
-
|
20
|
-
# Format an ISO8601 Duration
|
21
|
-
module Iso8601Duration
|
22
|
-
# Convert the number of seconds to an ISO8601 duration format
|
23
|
-
# @see http://tools.ietf.org/html/rfc2445#section-4.3.6
|
24
|
-
# @param [Fixnum] seconds The amount of seconds for this duration
|
25
|
-
def self.sec_to_dur(seconds)
|
26
|
-
seconds = seconds.to_i
|
27
|
-
iso_str = 'P'
|
28
|
-
if seconds > 604_800 # more than a week
|
29
|
-
weeks = seconds / 604_800
|
30
|
-
seconds -= (604_800 * weeks)
|
31
|
-
iso_str << "#{weeks}W"
|
32
|
-
end
|
33
|
-
if seconds > 86_400 # more than a day
|
34
|
-
days = seconds / 86_400
|
35
|
-
seconds -= (86_400 * days)
|
36
|
-
iso_str << "#{days}D"
|
37
|
-
end
|
38
|
-
if seconds > 0
|
39
|
-
iso_str << 'T'
|
40
|
-
if seconds > 3600 # more than an hour
|
41
|
-
hours = seconds / 3600
|
42
|
-
seconds -= (3600 * hours)
|
43
|
-
iso_str << "#{hours}H"
|
44
|
-
end
|
45
|
-
if seconds > 60 # more than a minute
|
46
|
-
minutes = seconds / 60
|
47
|
-
seconds -= (60 * minutes)
|
48
|
-
iso_str << "#{minutes}M"
|
49
|
-
end
|
50
|
-
iso_str << "#{seconds}S"
|
51
|
-
end
|
52
|
-
|
53
|
-
iso_str
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
|
-
# rubocop:enable Metrics/MethodLength
|
58
|
-
# rubocop:enable Metrics/AbcSize
|
1
|
+
# encoding: UTF-8
|
2
|
+
#
|
3
|
+
# Copyright 2010 Dan Wanek <dan.wanek@gmail.com>
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
17
|
+
# rubocop:disable Metrics/MethodLength
|
18
|
+
# rubocop:disable Metrics/AbcSize
|
19
|
+
|
20
|
+
# Format an ISO8601 Duration
|
21
|
+
module Iso8601Duration
|
22
|
+
# Convert the number of seconds to an ISO8601 duration format
|
23
|
+
# @see http://tools.ietf.org/html/rfc2445#section-4.3.6
|
24
|
+
# @param [Fixnum] seconds The amount of seconds for this duration
|
25
|
+
def self.sec_to_dur(seconds)
|
26
|
+
seconds = seconds.to_i
|
27
|
+
iso_str = 'P'
|
28
|
+
if seconds > 604_800 # more than a week
|
29
|
+
weeks = seconds / 604_800
|
30
|
+
seconds -= (604_800 * weeks)
|
31
|
+
iso_str << "#{weeks}W"
|
32
|
+
end
|
33
|
+
if seconds > 86_400 # more than a day
|
34
|
+
days = seconds / 86_400
|
35
|
+
seconds -= (86_400 * days)
|
36
|
+
iso_str << "#{days}D"
|
37
|
+
end
|
38
|
+
if seconds > 0
|
39
|
+
iso_str << 'T'
|
40
|
+
if seconds > 3600 # more than an hour
|
41
|
+
hours = seconds / 3600
|
42
|
+
seconds -= (3600 * hours)
|
43
|
+
iso_str << "#{hours}H"
|
44
|
+
end
|
45
|
+
if seconds > 60 # more than a minute
|
46
|
+
minutes = seconds / 60
|
47
|
+
seconds -= (60 * minutes)
|
48
|
+
iso_str << "#{minutes}M"
|
49
|
+
end
|
50
|
+
iso_str << "#{seconds}S"
|
51
|
+
end
|
52
|
+
|
53
|
+
iso_str
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# rubocop:enable Metrics/MethodLength
|
58
|
+
# rubocop:enable Metrics/AbcSize
|
@@ -1,42 +1,42 @@
|
|
1
|
-
# encoding: UTF-8
|
2
|
-
#
|
3
|
-
# Copyright 2014 Shawn Neal <sneal@sneal.net>
|
4
|
-
#
|
5
|
-
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
-
# you may not use this file except in compliance with the License.
|
7
|
-
# You may obtain a copy of the License at
|
8
|
-
#
|
9
|
-
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
-
#
|
11
|
-
# Unless required by applicable law or agreed to in writing, software
|
12
|
-
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
-
# See the License for the specific language governing permissions and
|
15
|
-
# limitations under the License.
|
16
|
-
|
17
|
-
module WinRM
|
18
|
-
# Wraps a PowerShell script to make it easy to Base64 encode for transport
|
19
|
-
class PowershellScript
|
20
|
-
attr_reader :text
|
21
|
-
|
22
|
-
# Creates a new PowershellScript object which can be used to encode
|
23
|
-
# PS scripts for safe transport over WinRM.
|
24
|
-
# @param [String] The PS script text content
|
25
|
-
def initialize(script)
|
26
|
-
@text = script
|
27
|
-
end
|
28
|
-
|
29
|
-
# Encodes the script so that it can be passed to the PowerShell
|
30
|
-
# --EncodedCommand argument.
|
31
|
-
# @return [String] The UTF-16LE base64 encoded script
|
32
|
-
def encoded
|
33
|
-
encoded_script = safe_script(text).encode('UTF-16LE', 'UTF-8')
|
34
|
-
Base64.strict_encode64(encoded_script)
|
35
|
-
end
|
36
|
-
|
37
|
-
# suppress the progress stream from leaking to stderr
|
38
|
-
def safe_script(script)
|
39
|
-
"$ProgressPreference='SilentlyContinue';" + script
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|
1
|
+
# encoding: UTF-8
|
2
|
+
#
|
3
|
+
# Copyright 2014 Shawn Neal <sneal@sneal.net>
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
17
|
+
module WinRM
|
18
|
+
# Wraps a PowerShell script to make it easy to Base64 encode for transport
|
19
|
+
class PowershellScript
|
20
|
+
attr_reader :text
|
21
|
+
|
22
|
+
# Creates a new PowershellScript object which can be used to encode
|
23
|
+
# PS scripts for safe transport over WinRM.
|
24
|
+
# @param [String] The PS script text content
|
25
|
+
def initialize(script)
|
26
|
+
@text = script
|
27
|
+
end
|
28
|
+
|
29
|
+
# Encodes the script so that it can be passed to the PowerShell
|
30
|
+
# --EncodedCommand argument.
|
31
|
+
# @return [String] The UTF-16LE base64 encoded script
|
32
|
+
def encoded
|
33
|
+
encoded_script = safe_script(text).encode('UTF-16LE', 'UTF-8')
|
34
|
+
Base64.strict_encode64(encoded_script)
|
35
|
+
end
|
36
|
+
|
37
|
+
# suppress the progress stream from leaking to stderr
|
38
|
+
def safe_script(script)
|
39
|
+
"$ProgressPreference='SilentlyContinue';" + script
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -1,82 +1,82 @@
|
|
1
|
-
# encoding: UTF-8
|
2
|
-
#
|
3
|
-
# Copyright 2014 Shawn Neal <sneal@sneal.net>
|
4
|
-
#
|
5
|
-
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
-
# you may not use this file except in compliance with the License.
|
7
|
-
# You may obtain a copy of the License at
|
8
|
-
#
|
9
|
-
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
-
#
|
11
|
-
# Unless required by applicable law or agreed to in writing, software
|
12
|
-
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
-
# See the License for the specific language governing permissions and
|
15
|
-
# limitations under the License.
|
16
|
-
|
17
|
-
require 'rexml/document'
|
18
|
-
|
19
|
-
module WinRM
|
20
|
-
# Handles the raw WinRM HTTP response. Returns the body as an XML doc
|
21
|
-
# or raises the appropriate WinRM error if the response is an error.
|
22
|
-
class ResponseHandler
|
23
|
-
# @param [String] The raw unparsed response body, if any
|
24
|
-
# @param [Integer] The HTTP response status code
|
25
|
-
def initialize(response_body, status_code)
|
26
|
-
@response_body = response_body
|
27
|
-
@status_code = status_code
|
28
|
-
end
|
29
|
-
|
30
|
-
# Processes the response from the WinRM service and either returns an XML
|
31
|
-
# doc or raises an appropriate error.
|
32
|
-
#
|
33
|
-
# @returns [REXML::Document] The parsed response body
|
34
|
-
def parse_to_xml
|
35
|
-
raise_if_error
|
36
|
-
response_xml
|
37
|
-
end
|
38
|
-
|
39
|
-
private
|
40
|
-
|
41
|
-
def response_xml
|
42
|
-
@response_xml ||= REXML::Document.new(@response_body)
|
43
|
-
rescue REXML::ParseException => e
|
44
|
-
raise WinRMHTTPTransportError.new(
|
45
|
-
"Unable to parse WinRM response: #{e.message}", @status_code)
|
46
|
-
end
|
47
|
-
|
48
|
-
def raise_if_error
|
49
|
-
return if @status_code == 200
|
50
|
-
raise_if_auth_error
|
51
|
-
raise_if_wsman_fault
|
52
|
-
raise_if_wmi_error
|
53
|
-
raise_transport_error
|
54
|
-
end
|
55
|
-
|
56
|
-
def raise_if_auth_error
|
57
|
-
fail WinRMAuthorizationError if @status_code == 401
|
58
|
-
end
|
59
|
-
|
60
|
-
def raise_if_wsman_fault
|
61
|
-
soap_errors = REXML::XPath.match(response_xml, "//#{NS_SOAP_ENV}:Body/#{NS_SOAP_ENV}:Fault/*")
|
62
|
-
return if soap_errors.empty?
|
63
|
-
fault = REXML::XPath.first(soap_errors, "//#{NS_WSMAN_FAULT}:WSManFault")
|
64
|
-
fail WinRMWSManFault.new(fault.to_s, fault.attributes['Code']) unless fault.nil?
|
65
|
-
end
|
66
|
-
|
67
|
-
def raise_if_wmi_error
|
68
|
-
soap_errors = REXML::XPath.match(response_xml, "//#{NS_SOAP_ENV}:Body/#{NS_SOAP_ENV}:Fault/*")
|
69
|
-
return if soap_errors.empty?
|
70
|
-
|
71
|
-
error = REXML::XPath.first(soap_errors, "//#{NS_WSMAN_MSFT}:MSFT_WmiError")
|
72
|
-
return if error.nil?
|
73
|
-
|
74
|
-
error_code = REXML::XPath.first(error, "//#{NS_WSMAN_MSFT}:error_Code").text
|
75
|
-
fail WinRMWMIError.new(error.to_s, error_code)
|
76
|
-
end
|
77
|
-
|
78
|
-
def raise_transport_error
|
79
|
-
fail WinRMHTTPTransportError.new('Bad HTTP response returned from server', @status_code)
|
80
|
-
end
|
81
|
-
end
|
82
|
-
end
|
1
|
+
# encoding: UTF-8
|
2
|
+
#
|
3
|
+
# Copyright 2014 Shawn Neal <sneal@sneal.net>
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
17
|
+
require 'rexml/document'
|
18
|
+
|
19
|
+
module WinRM
|
20
|
+
# Handles the raw WinRM HTTP response. Returns the body as an XML doc
|
21
|
+
# or raises the appropriate WinRM error if the response is an error.
|
22
|
+
class ResponseHandler
|
23
|
+
# @param [String] The raw unparsed response body, if any
|
24
|
+
# @param [Integer] The HTTP response status code
|
25
|
+
def initialize(response_body, status_code)
|
26
|
+
@response_body = response_body
|
27
|
+
@status_code = status_code
|
28
|
+
end
|
29
|
+
|
30
|
+
# Processes the response from the WinRM service and either returns an XML
|
31
|
+
# doc or raises an appropriate error.
|
32
|
+
#
|
33
|
+
# @returns [REXML::Document] The parsed response body
|
34
|
+
def parse_to_xml
|
35
|
+
raise_if_error
|
36
|
+
response_xml
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def response_xml
|
42
|
+
@response_xml ||= REXML::Document.new(@response_body)
|
43
|
+
rescue REXML::ParseException => e
|
44
|
+
raise WinRMHTTPTransportError.new(
|
45
|
+
"Unable to parse WinRM response: #{e.message}", @status_code)
|
46
|
+
end
|
47
|
+
|
48
|
+
def raise_if_error
|
49
|
+
return if @status_code == 200
|
50
|
+
raise_if_auth_error
|
51
|
+
raise_if_wsman_fault
|
52
|
+
raise_if_wmi_error
|
53
|
+
raise_transport_error
|
54
|
+
end
|
55
|
+
|
56
|
+
def raise_if_auth_error
|
57
|
+
fail WinRMAuthorizationError if @status_code == 401
|
58
|
+
end
|
59
|
+
|
60
|
+
def raise_if_wsman_fault
|
61
|
+
soap_errors = REXML::XPath.match(response_xml, "//#{NS_SOAP_ENV}:Body/#{NS_SOAP_ENV}:Fault/*")
|
62
|
+
return if soap_errors.empty?
|
63
|
+
fault = REXML::XPath.first(soap_errors, "//#{NS_WSMAN_FAULT}:WSManFault")
|
64
|
+
fail WinRMWSManFault.new(fault.to_s, fault.attributes['Code']) unless fault.nil?
|
65
|
+
end
|
66
|
+
|
67
|
+
def raise_if_wmi_error
|
68
|
+
soap_errors = REXML::XPath.match(response_xml, "//#{NS_SOAP_ENV}:Body/#{NS_SOAP_ENV}:Fault/*")
|
69
|
+
return if soap_errors.empty?
|
70
|
+
|
71
|
+
error = REXML::XPath.first(soap_errors, "//#{NS_WSMAN_MSFT}:MSFT_WmiError")
|
72
|
+
return if error.nil?
|
73
|
+
|
74
|
+
error_code = REXML::XPath.first(error, "//#{NS_WSMAN_MSFT}:error_Code").text
|
75
|
+
fail WinRMWMIError.new(error.to_s, error_code)
|
76
|
+
end
|
77
|
+
|
78
|
+
def raise_transport_error
|
79
|
+
fail WinRMHTTPTransportError.new('Bad HTTP response returned from server', @status_code)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
data/lib/winrm/http/transport.rb
CHANGED
@@ -1,421 +1,421 @@
|
|
1
|
-
# encoding: UTF-8
|
2
|
-
#
|
3
|
-
# Copyright 2010 Dan Wanek <dan.wanek@gmail.com>
|
4
|
-
#
|
5
|
-
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
-
# you may not use this file except in compliance with the License.
|
7
|
-
# You may obtain a copy of the License at
|
8
|
-
#
|
9
|
-
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
-
#
|
11
|
-
# Unless required by applicable law or agreed to in writing, software
|
12
|
-
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
-
# See the License for the specific language governing permissions and
|
15
|
-
# limitations under the License.
|
16
|
-
|
17
|
-
require_relative 'response_handler'
|
18
|
-
|
19
|
-
module WinRM
|
20
|
-
module HTTP
|
21
|
-
# A generic HTTP transport that utilized HTTPClient to send messages back and forth.
|
22
|
-
# This backend will maintain state for every WinRMWebService instance that is instantiated so it
|
23
|
-
# is possible to use GSSAPI with Keep-Alive.
|
24
|
-
class HttpTransport
|
25
|
-
# Set this to an unreasonable amount because WinRM has its own timeouts
|
26
|
-
DEFAULT_RECEIVE_TIMEOUT = 3600
|
27
|
-
|
28
|
-
attr_reader :endpoint
|
29
|
-
|
30
|
-
def initialize(endpoint)
|
31
|
-
@endpoint = endpoint.is_a?(String) ? URI.parse(endpoint) : endpoint
|
32
|
-
@httpcli = HTTPClient.new(agent_name: 'Ruby WinRM Client')
|
33
|
-
@httpcli.receive_timeout = DEFAULT_RECEIVE_TIMEOUT
|
34
|
-
@logger = Logging.logger[self]
|
35
|
-
end
|
36
|
-
|
37
|
-
# Sends the SOAP payload to the WinRM service and returns the service's
|
38
|
-
# SOAP response. If an error occurrs an appropriate error is raised.
|
39
|
-
#
|
40
|
-
# @param [String] The XML SOAP message
|
41
|
-
# @returns [REXML::Document] The parsed response body
|
42
|
-
def send_request(message)
|
43
|
-
ssl_peer_fingerprint_verification!
|
44
|
-
log_soap_message(message)
|
45
|
-
hdr = { 'Content-Type' => 'application/soap+xml;charset=UTF-8',
|
46
|
-
'Content-Length' => message.length }
|
47
|
-
resp = @httpcli.post(@endpoint, message, hdr)
|
48
|
-
log_soap_message(resp.http_body.content)
|
49
|
-
verify_ssl_fingerprint(resp.peer_cert)
|
50
|
-
handler = WinRM::ResponseHandler.new(resp.http_body.content, resp.status)
|
51
|
-
handler.parse_to_xml
|
52
|
-
end
|
53
|
-
|
54
|
-
# We'll need this to force basic authentication if desired
|
55
|
-
def basic_auth_only!
|
56
|
-
auths = @httpcli.www_auth.instance_variable_get('@authenticator')
|
57
|
-
auths.delete_if { |i| i.scheme !~ /basic/i }
|
58
|
-
end
|
59
|
-
|
60
|
-
# Disable SSPI Auth
|
61
|
-
def no_sspi_auth!
|
62
|
-
auths = @httpcli.www_auth.instance_variable_get('@authenticator')
|
63
|
-
auths.delete_if { |i| i.is_a? HTTPClient::SSPINegotiateAuth }
|
64
|
-
end
|
65
|
-
|
66
|
-
# Disable SSL Peer Verification
|
67
|
-
def no_ssl_peer_verification!
|
68
|
-
@httpcli.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
69
|
-
end
|
70
|
-
|
71
|
-
# SSL Peer Fingerprint Verification prior to connecting
|
72
|
-
def ssl_peer_fingerprint_verification!
|
73
|
-
return unless @ssl_peer_fingerprint && ! @ssl_peer_fingerprint_verified
|
74
|
-
|
75
|
-
with_untrusted_ssl_connection do |connection|
|
76
|
-
connection_cert = connection.peer_cert_chain.last
|
77
|
-
verify_ssl_fingerprint(connection_cert)
|
78
|
-
end
|
79
|
-
@logger.info("initial ssl fingerprint #{@ssl_peer_fingerprint} verified\n")
|
80
|
-
@ssl_peer_fingerprint_verified = true
|
81
|
-
no_ssl_peer_verification!
|
82
|
-
end
|
83
|
-
|
84
|
-
# Connect without verification to retrieve untrusted ssl context
|
85
|
-
def with_untrusted_ssl_connection
|
86
|
-
noverify_peer_context = OpenSSL::SSL::SSLContext.new
|
87
|
-
noverify_peer_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
88
|
-
tcp_connection = TCPSocket.new(@endpoint.host, @endpoint.port)
|
89
|
-
begin
|
90
|
-
ssl_connection = OpenSSL::SSL::SSLSocket.new(tcp_connection, noverify_peer_context)
|
91
|
-
ssl_connection.connect
|
92
|
-
yield ssl_connection
|
93
|
-
ensure
|
94
|
-
tcp_connection.close
|
95
|
-
end
|
96
|
-
end
|
97
|
-
|
98
|
-
# compare @ssl_peer_fingerprint to current ssl context
|
99
|
-
def verify_ssl_fingerprint(cert)
|
100
|
-
return unless @ssl_peer_fingerprint
|
101
|
-
conn_fingerprint = OpenSSL::Digest::SHA1.new(cert.to_der).to_s
|
102
|
-
return unless @ssl_peer_fingerprint.casecmp(conn_fingerprint) != 0
|
103
|
-
fail "ssl fingerprint mismatch!!!!\n"
|
104
|
-
end
|
105
|
-
|
106
|
-
# HTTP Client receive timeout. How long should a remote call wait for a
|
107
|
-
# for a response from WinRM?
|
108
|
-
def receive_timeout=(sec)
|
109
|
-
@httpcli.receive_timeout = sec
|
110
|
-
end
|
111
|
-
|
112
|
-
def receive_timeout
|
113
|
-
@httpcli.receive_timeout
|
114
|
-
end
|
115
|
-
|
116
|
-
protected
|
117
|
-
|
118
|
-
def log_soap_message(message)
|
119
|
-
return unless @logger.debug?
|
120
|
-
|
121
|
-
xml_msg = REXML::Document.new(message)
|
122
|
-
formatter = REXML::Formatters::Pretty.new(2)
|
123
|
-
formatter.compact = true
|
124
|
-
formatter.write(xml_msg, @logger)
|
125
|
-
@logger.debug("\n")
|
126
|
-
rescue StandardError => e
|
127
|
-
@logger.debug("Couldn't log SOAP request/response: #{e.message} - #{message}")
|
128
|
-
end
|
129
|
-
end
|
130
|
-
|
131
|
-
|
132
|
-
# Plain text, insecure, HTTP transport
|
133
|
-
class HttpPlaintext < HttpTransport
|
134
|
-
def initialize(endpoint, user, pass, opts)
|
135
|
-
super(endpoint)
|
136
|
-
@httpcli.set_auth(nil, user, pass)
|
137
|
-
no_sspi_auth! if opts[:disable_sspi]
|
138
|
-
basic_auth_only! if opts[:basic_auth_only]
|
139
|
-
no_ssl_peer_verification! if opts[:no_ssl_peer_verification]
|
140
|
-
end
|
141
|
-
end
|
142
|
-
|
143
|
-
|
144
|
-
# NTLM/Negotiate, secure, HTTP transport
|
145
|
-
class HttpNegotiate < HttpTransport
|
146
|
-
def initialize(endpoint, user, pass, opts)
|
147
|
-
super(endpoint)
|
148
|
-
require 'rubyntlm'
|
149
|
-
no_sspi_auth!
|
150
|
-
|
151
|
-
user_parts = user.split('\\')
|
152
|
-
if(user_parts.length > 1)
|
153
|
-
opts[:domain] = user_parts[0]
|
154
|
-
user = user_parts[1]
|
155
|
-
end
|
156
|
-
|
157
|
-
@ntlmcli = Net::NTLM::Client.new(user, pass, opts)
|
158
|
-
@retryable = true
|
159
|
-
no_ssl_peer_verification! if opts[:no_ssl_peer_verification]
|
160
|
-
@ssl_peer_fingerprint = opts[:ssl_peer_fingerprint]
|
161
|
-
@httpcli.ssl_config.set_trust_ca(opts[:ca_trust_path]) if opts[:ca_trust_path]
|
162
|
-
end
|
163
|
-
|
164
|
-
def send_request(message, auth_header = nil)
|
165
|
-
ssl_peer_fingerprint_verification!
|
166
|
-
auth_header = init_auth if @ntlmcli.session.nil?
|
167
|
-
|
168
|
-
original_length = message.length
|
169
|
-
|
170
|
-
emessage = @ntlmcli.session.seal_message message
|
171
|
-
signature = @ntlmcli.session.sign_message message
|
172
|
-
seal = "\x10\x00\x00\x00#{signature}#{emessage}"
|
173
|
-
|
174
|
-
hdr = {
|
175
|
-
"Content-Type" => "multipart/encrypted;protocol=\"application/HTTP-SPNEGO-session-encrypted\";boundary=\"Encrypted Boundary\""
|
176
|
-
}
|
177
|
-
hdr.merge!(auth_header) if auth_header
|
178
|
-
|
179
|
-
body = [
|
180
|
-
"--Encrypted Boundary",
|
181
|
-
"Content-Type: application/HTTP-SPNEGO-session-encrypted",
|
182
|
-
"OriginalContent: type=application/soap+xml;charset=UTF-8;Length=#{original_length}",
|
183
|
-
"--Encrypted Boundary",
|
184
|
-
"Content-Type: application/octet-stream",
|
185
|
-
"#{seal}--Encrypted Boundary--",
|
186
|
-
""
|
187
|
-
].join("\r\n")
|
188
|
-
|
189
|
-
resp = @httpcli.post(@endpoint, body, hdr)
|
190
|
-
verify_ssl_fingerprint(resp.peer_cert)
|
191
|
-
if resp.status == 401 && @retryable
|
192
|
-
@retryable = false
|
193
|
-
send_request(message, init_auth)
|
194
|
-
else
|
195
|
-
@retryable = true
|
196
|
-
decrypted_body = resp.body.empty? ? '' : winrm_decrypt(resp.body)
|
197
|
-
handler = WinRM::ResponseHandler.new(decrypted_body, resp.status)
|
198
|
-
handler.parse_to_xml()
|
199
|
-
end
|
200
|
-
end
|
201
|
-
|
202
|
-
private
|
203
|
-
|
204
|
-
def winrm_decrypt(str)
|
205
|
-
str.force_encoding('BINARY')
|
206
|
-
str.sub!(/^.*Content-Type: application\/octet-stream\r\n(.*)--Encrypted.*$/m, '\1')
|
207
|
-
|
208
|
-
signature = str[4..19]
|
209
|
-
message = @ntlmcli.session.unseal_message str[20..-1]
|
210
|
-
if @ntlmcli.session.verify_signature(signature, message)
|
211
|
-
message
|
212
|
-
else
|
213
|
-
raise WinRMWebServiceError, "Could not verify SOAP message."
|
214
|
-
end
|
215
|
-
end
|
216
|
-
|
217
|
-
def init_auth
|
218
|
-
@logger.debug "Initializing Negotiate for #{@service}"
|
219
|
-
auth1 = @ntlmcli.init_context
|
220
|
-
hdr = {"Authorization" => "Negotiate #{auth1.encode64}",
|
221
|
-
"Content-Type" => "application/soap+xml;charset=UTF-8"
|
222
|
-
}
|
223
|
-
@logger.debug "Sending HTTP POST for Negotiate Authentication"
|
224
|
-
r = @httpcli.post(@endpoint, "", hdr)
|
225
|
-
verify_ssl_fingerprint(r.peer_cert)
|
226
|
-
itok = r.header["WWW-Authenticate"].pop.split.last
|
227
|
-
binding = r.peer_cert.nil? ? nil : Net::NTLM::ChannelBinding.create(r.peer_cert)
|
228
|
-
auth3 = @ntlmcli.init_context(itok, binding)
|
229
|
-
{ "Authorization" => "Negotiate #{auth3.encode64}" }
|
230
|
-
end
|
231
|
-
end
|
232
|
-
|
233
|
-
# Uses SSL to secure the transport
|
234
|
-
class BasicAuthSSL < HttpTransport
|
235
|
-
def initialize(endpoint, user, pass, opts)
|
236
|
-
super(endpoint)
|
237
|
-
@httpcli.set_auth(endpoint, user, pass)
|
238
|
-
basic_auth_only!
|
239
|
-
no_ssl_peer_verification! if opts[:no_ssl_peer_verification]
|
240
|
-
@ssl_peer_fingerprint = opts[:ssl_peer_fingerprint]
|
241
|
-
@httpcli.ssl_config.set_trust_ca(opts[:ca_trust_path]) if opts[:ca_trust_path]
|
242
|
-
end
|
243
|
-
end
|
244
|
-
|
245
|
-
# Uses Kerberos/GSSAPI to authenticate and encrypt messages
|
246
|
-
# rubocop:disable Metrics/ClassLength
|
247
|
-
class HttpGSSAPI < HttpTransport
|
248
|
-
# @param [String,URI] endpoint the WinRM webservice endpoint
|
249
|
-
# @param [String] realm the Kerberos realm we are authenticating to
|
250
|
-
# @param [String<optional>] service the service name, default is HTTP
|
251
|
-
# @param [String<optional>] keytab the path to a keytab file if you are using one
|
252
|
-
# rubocop:disable Lint/UnusedMethodArgument
|
253
|
-
def initialize(endpoint, realm, service = nil, keytab = nil, opts)
|
254
|
-
# rubocop:enable Lint/UnusedMethodArgument
|
255
|
-
super(endpoint)
|
256
|
-
# Remove the GSSAPI auth from HTTPClient because we are doing our own thing
|
257
|
-
no_sspi_auth!
|
258
|
-
service ||= 'HTTP'
|
259
|
-
@service = "#{service}/#{@endpoint.host}@#{realm}"
|
260
|
-
init_krb
|
261
|
-
end
|
262
|
-
|
263
|
-
# Sends the SOAP payload to the WinRM service and returns the service's
|
264
|
-
# SOAP response. If an error occurrs an appropriate error is raised.
|
265
|
-
#
|
266
|
-
# @param [String] The XML SOAP message
|
267
|
-
# @returns [REXML::Document] The parsed response body
|
268
|
-
def send_request(message)
|
269
|
-
resp = send_kerberos_request(message)
|
270
|
-
|
271
|
-
if resp.status == 401
|
272
|
-
@logger.debug 'Got 401 - reinitializing Kerberos and retrying one more time'
|
273
|
-
init_krb
|
274
|
-
resp = send_kerberos_request(message)
|
275
|
-
end
|
276
|
-
|
277
|
-
handler = WinRM::ResponseHandler.new(winrm_decrypt(resp.http_body.content), resp.status)
|
278
|
-
handler.parse_to_xml
|
279
|
-
end
|
280
|
-
|
281
|
-
private
|
282
|
-
|
283
|
-
# rubocop:disable Metrics/MethodLength
|
284
|
-
# rubocop:disable Metrics/AbcSize
|
285
|
-
|
286
|
-
# Sends the SOAP payload to the WinRM service and returns the service's
|
287
|
-
# HTTP response.
|
288
|
-
#
|
289
|
-
# @param [String] The XML SOAP message
|
290
|
-
# @returns [Object] The HTTP response object
|
291
|
-
def send_kerberos_request(message)
|
292
|
-
log_soap_message(message)
|
293
|
-
original_length = message.length
|
294
|
-
pad_len, emsg = winrm_encrypt(message)
|
295
|
-
hdr = {
|
296
|
-
'Connection' => 'Keep-Alive',
|
297
|
-
'Content-Type' =>
|
298
|
-
'multipart/encrypted;' \
|
299
|
-
'protocol="application/HTTP-Kerberos-session-encrypted";' \
|
300
|
-
'boundary="Encrypted Boundary"'
|
301
|
-
}
|
302
|
-
body = [
|
303
|
-
"--Encrypted Boundary",
|
304
|
-
"Content-Type: application/HTTP-Kerberos-session-encrypted",
|
305
|
-
"OriginalContent: type=application/soap+xml;charset=UTF-8;Length=#{original_length + pad_len}",
|
306
|
-
"--Encrypted Boundary",
|
307
|
-
"Content-Type: application/octet-stream",
|
308
|
-
"#{emsg}--Encrypted Boundary--",
|
309
|
-
""
|
310
|
-
].join("\r\n")
|
311
|
-
|
312
|
-
resp = @httpcli.post(@endpoint, body, hdr)
|
313
|
-
log_soap_message(resp.http_body.content)
|
314
|
-
resp
|
315
|
-
end
|
316
|
-
|
317
|
-
def init_krb
|
318
|
-
@logger.debug "Initializing Kerberos for #{@service}"
|
319
|
-
@gsscli = GSSAPI::Simple.new(@endpoint.host, @service)
|
320
|
-
token = @gsscli.init_context
|
321
|
-
auth = Base64.strict_encode64 token
|
322
|
-
|
323
|
-
hdr = {
|
324
|
-
'Authorization' => "Kerberos #{auth}",
|
325
|
-
'Connection' => 'Keep-Alive',
|
326
|
-
'Content-Type' => 'application/soap+xml;charset=UTF-8'
|
327
|
-
}
|
328
|
-
@logger.debug 'Sending HTTP POST for Kerberos Authentication'
|
329
|
-
r = @httpcli.post(@endpoint, '', hdr)
|
330
|
-
itok = r.header['WWW-Authenticate'].pop
|
331
|
-
itok = itok.split.last
|
332
|
-
itok = Base64.strict_decode64(itok)
|
333
|
-
@gsscli.init_context(itok)
|
334
|
-
end
|
335
|
-
|
336
|
-
# @return [String] the encrypted request string
|
337
|
-
def winrm_encrypt(str)
|
338
|
-
@logger.debug "Encrypting SOAP message:\n#{str}"
|
339
|
-
iov_cnt = 3
|
340
|
-
iov = FFI::MemoryPointer.new(GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * iov_cnt)
|
341
|
-
|
342
|
-
iov0 = GSSAPI::LibGSSAPI::GssIOVBufferDesc.new(FFI::Pointer.new(iov.address))
|
343
|
-
iov0[:type] = (GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_HEADER | \
|
344
|
-
GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_FLAG_ALLOCATE)
|
345
|
-
|
346
|
-
iov1 = GSSAPI::LibGSSAPI::GssIOVBufferDesc.new(
|
347
|
-
FFI::Pointer.new(iov.address + (GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * 1)))
|
348
|
-
iov1[:type] = (GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_DATA)
|
349
|
-
iov1[:buffer].value = str
|
350
|
-
|
351
|
-
iov2 = GSSAPI::LibGSSAPI::GssIOVBufferDesc.new(
|
352
|
-
FFI::Pointer.new(iov.address + (GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * 2)))
|
353
|
-
iov2[:type] = (GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_PADDING | \
|
354
|
-
GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_FLAG_ALLOCATE)
|
355
|
-
|
356
|
-
conf_state = FFI::MemoryPointer.new :uint32
|
357
|
-
min_stat = FFI::MemoryPointer.new :uint32
|
358
|
-
|
359
|
-
GSSAPI::LibGSSAPI.gss_wrap_iov(
|
360
|
-
min_stat,
|
361
|
-
@gsscli.context,
|
362
|
-
1,
|
363
|
-
GSSAPI::LibGSSAPI::GSS_C_QOP_DEFAULT,
|
364
|
-
conf_state,
|
365
|
-
iov,
|
366
|
-
iov_cnt)
|
367
|
-
|
368
|
-
token = [iov0[:buffer].length].pack('L')
|
369
|
-
token += iov0[:buffer].value
|
370
|
-
token += iov1[:buffer].value
|
371
|
-
pad_len = iov2[:buffer].length
|
372
|
-
token += iov2[:buffer].value if pad_len > 0
|
373
|
-
[pad_len, token]
|
374
|
-
end
|
375
|
-
|
376
|
-
# @return [String] the unencrypted response string
|
377
|
-
def winrm_decrypt(str)
|
378
|
-
@logger.debug "Decrypting SOAP message:\n#{str}"
|
379
|
-
iov_cnt = 3
|
380
|
-
iov = FFI::MemoryPointer.new(GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * iov_cnt)
|
381
|
-
|
382
|
-
iov0 = GSSAPI::LibGSSAPI::GssIOVBufferDesc.new(FFI::Pointer.new(iov.address))
|
383
|
-
iov0[:type] = (GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_HEADER | \
|
384
|
-
GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_FLAG_ALLOCATE)
|
385
|
-
|
386
|
-
iov1 = GSSAPI::LibGSSAPI::GssIOVBufferDesc.new(
|
387
|
-
FFI::Pointer.new(iov.address + (GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * 1)))
|
388
|
-
iov1[:type] = (GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_DATA)
|
389
|
-
|
390
|
-
iov2 = GSSAPI::LibGSSAPI::GssIOVBufferDesc.new(
|
391
|
-
FFI::Pointer.new(iov.address + (GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * 2)))
|
392
|
-
iov2[:type] = (GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_DATA)
|
393
|
-
|
394
|
-
str.force_encoding('BINARY')
|
395
|
-
str.sub!(/^.*Content-Type: application\/octet-stream\r\n(.*)--Encrypted.*$/m, '\1')
|
396
|
-
|
397
|
-
len = str.unpack('L').first
|
398
|
-
iov_data = str.unpack("LA#{len}A*")
|
399
|
-
iov0[:buffer].value = iov_data[1]
|
400
|
-
iov1[:buffer].value = iov_data[2]
|
401
|
-
|
402
|
-
min_stat = FFI::MemoryPointer.new :uint32
|
403
|
-
conf_state = FFI::MemoryPointer.new :uint32
|
404
|
-
conf_state.write_int(1)
|
405
|
-
qop_state = FFI::MemoryPointer.new :uint32
|
406
|
-
qop_state.write_int(0)
|
407
|
-
|
408
|
-
maj_stat = GSSAPI::LibGSSAPI.gss_unwrap_iov(
|
409
|
-
min_stat, @gsscli.context, conf_state, qop_state, iov, iov_cnt)
|
410
|
-
|
411
|
-
@logger.debug "SOAP message decrypted (MAJ: #{maj_stat}, " \
|
412
|
-
"MIN: #{min_stat.read_int}):\n#{iov1[:buffer].value}"
|
413
|
-
|
414
|
-
iov1[:buffer].value
|
415
|
-
end
|
416
|
-
# rubocop:enable Metrics/MethodLength
|
417
|
-
# rubocop:enable Metrics/AbcSize
|
418
|
-
end
|
419
|
-
# rubocop:enable Metrics/ClassLength
|
420
|
-
end
|
421
|
-
end # WinRM
|
1
|
+
# encoding: UTF-8
|
2
|
+
#
|
3
|
+
# Copyright 2010 Dan Wanek <dan.wanek@gmail.com>
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
17
|
+
require_relative 'response_handler'
|
18
|
+
|
19
|
+
module WinRM
|
20
|
+
module HTTP
|
21
|
+
# A generic HTTP transport that utilized HTTPClient to send messages back and forth.
|
22
|
+
# This backend will maintain state for every WinRMWebService instance that is instantiated so it
|
23
|
+
# is possible to use GSSAPI with Keep-Alive.
|
24
|
+
class HttpTransport
|
25
|
+
# Set this to an unreasonable amount because WinRM has its own timeouts
|
26
|
+
DEFAULT_RECEIVE_TIMEOUT = 3600
|
27
|
+
|
28
|
+
attr_reader :endpoint
|
29
|
+
|
30
|
+
def initialize(endpoint)
|
31
|
+
@endpoint = endpoint.is_a?(String) ? URI.parse(endpoint) : endpoint
|
32
|
+
@httpcli = HTTPClient.new(agent_name: 'Ruby WinRM Client')
|
33
|
+
@httpcli.receive_timeout = DEFAULT_RECEIVE_TIMEOUT
|
34
|
+
@logger = Logging.logger[self]
|
35
|
+
end
|
36
|
+
|
37
|
+
# Sends the SOAP payload to the WinRM service and returns the service's
|
38
|
+
# SOAP response. If an error occurrs an appropriate error is raised.
|
39
|
+
#
|
40
|
+
# @param [String] The XML SOAP message
|
41
|
+
# @returns [REXML::Document] The parsed response body
|
42
|
+
def send_request(message)
|
43
|
+
ssl_peer_fingerprint_verification!
|
44
|
+
log_soap_message(message)
|
45
|
+
hdr = { 'Content-Type' => 'application/soap+xml;charset=UTF-8',
|
46
|
+
'Content-Length' => message.length }
|
47
|
+
resp = @httpcli.post(@endpoint, message, hdr)
|
48
|
+
log_soap_message(resp.http_body.content)
|
49
|
+
verify_ssl_fingerprint(resp.peer_cert)
|
50
|
+
handler = WinRM::ResponseHandler.new(resp.http_body.content, resp.status)
|
51
|
+
handler.parse_to_xml
|
52
|
+
end
|
53
|
+
|
54
|
+
# We'll need this to force basic authentication if desired
|
55
|
+
def basic_auth_only!
|
56
|
+
auths = @httpcli.www_auth.instance_variable_get('@authenticator')
|
57
|
+
auths.delete_if { |i| i.scheme !~ /basic/i }
|
58
|
+
end
|
59
|
+
|
60
|
+
# Disable SSPI Auth
|
61
|
+
def no_sspi_auth!
|
62
|
+
auths = @httpcli.www_auth.instance_variable_get('@authenticator')
|
63
|
+
auths.delete_if { |i| i.is_a? HTTPClient::SSPINegotiateAuth }
|
64
|
+
end
|
65
|
+
|
66
|
+
# Disable SSL Peer Verification
|
67
|
+
def no_ssl_peer_verification!
|
68
|
+
@httpcli.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
69
|
+
end
|
70
|
+
|
71
|
+
# SSL Peer Fingerprint Verification prior to connecting
|
72
|
+
def ssl_peer_fingerprint_verification!
|
73
|
+
return unless @ssl_peer_fingerprint && ! @ssl_peer_fingerprint_verified
|
74
|
+
|
75
|
+
with_untrusted_ssl_connection do |connection|
|
76
|
+
connection_cert = connection.peer_cert_chain.last
|
77
|
+
verify_ssl_fingerprint(connection_cert)
|
78
|
+
end
|
79
|
+
@logger.info("initial ssl fingerprint #{@ssl_peer_fingerprint} verified\n")
|
80
|
+
@ssl_peer_fingerprint_verified = true
|
81
|
+
no_ssl_peer_verification!
|
82
|
+
end
|
83
|
+
|
84
|
+
# Connect without verification to retrieve untrusted ssl context
|
85
|
+
def with_untrusted_ssl_connection
|
86
|
+
noverify_peer_context = OpenSSL::SSL::SSLContext.new
|
87
|
+
noverify_peer_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
88
|
+
tcp_connection = TCPSocket.new(@endpoint.host, @endpoint.port)
|
89
|
+
begin
|
90
|
+
ssl_connection = OpenSSL::SSL::SSLSocket.new(tcp_connection, noverify_peer_context)
|
91
|
+
ssl_connection.connect
|
92
|
+
yield ssl_connection
|
93
|
+
ensure
|
94
|
+
tcp_connection.close
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# compare @ssl_peer_fingerprint to current ssl context
|
99
|
+
def verify_ssl_fingerprint(cert)
|
100
|
+
return unless @ssl_peer_fingerprint
|
101
|
+
conn_fingerprint = OpenSSL::Digest::SHA1.new(cert.to_der).to_s
|
102
|
+
return unless @ssl_peer_fingerprint.casecmp(conn_fingerprint) != 0
|
103
|
+
fail "ssl fingerprint mismatch!!!!\n"
|
104
|
+
end
|
105
|
+
|
106
|
+
# HTTP Client receive timeout. How long should a remote call wait for a
|
107
|
+
# for a response from WinRM?
|
108
|
+
def receive_timeout=(sec)
|
109
|
+
@httpcli.receive_timeout = sec
|
110
|
+
end
|
111
|
+
|
112
|
+
def receive_timeout
|
113
|
+
@httpcli.receive_timeout
|
114
|
+
end
|
115
|
+
|
116
|
+
protected
|
117
|
+
|
118
|
+
def log_soap_message(message)
|
119
|
+
return unless @logger.debug?
|
120
|
+
|
121
|
+
xml_msg = REXML::Document.new(message)
|
122
|
+
formatter = REXML::Formatters::Pretty.new(2)
|
123
|
+
formatter.compact = true
|
124
|
+
formatter.write(xml_msg, @logger)
|
125
|
+
@logger.debug("\n")
|
126
|
+
rescue StandardError => e
|
127
|
+
@logger.debug("Couldn't log SOAP request/response: #{e.message} - #{message}")
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
|
132
|
+
# Plain text, insecure, HTTP transport
|
133
|
+
class HttpPlaintext < HttpTransport
|
134
|
+
def initialize(endpoint, user, pass, opts)
|
135
|
+
super(endpoint)
|
136
|
+
@httpcli.set_auth(nil, user, pass)
|
137
|
+
no_sspi_auth! if opts[:disable_sspi]
|
138
|
+
basic_auth_only! if opts[:basic_auth_only]
|
139
|
+
no_ssl_peer_verification! if opts[:no_ssl_peer_verification]
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
|
144
|
+
# NTLM/Negotiate, secure, HTTP transport
|
145
|
+
class HttpNegotiate < HttpTransport
|
146
|
+
def initialize(endpoint, user, pass, opts)
|
147
|
+
super(endpoint)
|
148
|
+
require 'rubyntlm'
|
149
|
+
no_sspi_auth!
|
150
|
+
|
151
|
+
user_parts = user.split('\\')
|
152
|
+
if(user_parts.length > 1)
|
153
|
+
opts[:domain] = user_parts[0]
|
154
|
+
user = user_parts[1]
|
155
|
+
end
|
156
|
+
|
157
|
+
@ntlmcli = Net::NTLM::Client.new(user, pass, opts)
|
158
|
+
@retryable = true
|
159
|
+
no_ssl_peer_verification! if opts[:no_ssl_peer_verification]
|
160
|
+
@ssl_peer_fingerprint = opts[:ssl_peer_fingerprint]
|
161
|
+
@httpcli.ssl_config.set_trust_ca(opts[:ca_trust_path]) if opts[:ca_trust_path]
|
162
|
+
end
|
163
|
+
|
164
|
+
def send_request(message, auth_header = nil)
|
165
|
+
ssl_peer_fingerprint_verification!
|
166
|
+
auth_header = init_auth if @ntlmcli.session.nil?
|
167
|
+
|
168
|
+
original_length = message.length
|
169
|
+
|
170
|
+
emessage = @ntlmcli.session.seal_message message
|
171
|
+
signature = @ntlmcli.session.sign_message message
|
172
|
+
seal = "\x10\x00\x00\x00#{signature}#{emessage}"
|
173
|
+
|
174
|
+
hdr = {
|
175
|
+
"Content-Type" => "multipart/encrypted;protocol=\"application/HTTP-SPNEGO-session-encrypted\";boundary=\"Encrypted Boundary\""
|
176
|
+
}
|
177
|
+
hdr.merge!(auth_header) if auth_header
|
178
|
+
|
179
|
+
body = [
|
180
|
+
"--Encrypted Boundary",
|
181
|
+
"Content-Type: application/HTTP-SPNEGO-session-encrypted",
|
182
|
+
"OriginalContent: type=application/soap+xml;charset=UTF-8;Length=#{original_length}",
|
183
|
+
"--Encrypted Boundary",
|
184
|
+
"Content-Type: application/octet-stream",
|
185
|
+
"#{seal}--Encrypted Boundary--",
|
186
|
+
""
|
187
|
+
].join("\r\n")
|
188
|
+
|
189
|
+
resp = @httpcli.post(@endpoint, body, hdr)
|
190
|
+
verify_ssl_fingerprint(resp.peer_cert)
|
191
|
+
if resp.status == 401 && @retryable
|
192
|
+
@retryable = false
|
193
|
+
send_request(message, init_auth)
|
194
|
+
else
|
195
|
+
@retryable = true
|
196
|
+
decrypted_body = resp.body.empty? ? '' : winrm_decrypt(resp.body)
|
197
|
+
handler = WinRM::ResponseHandler.new(decrypted_body, resp.status)
|
198
|
+
handler.parse_to_xml()
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
private
|
203
|
+
|
204
|
+
def winrm_decrypt(str)
|
205
|
+
str.force_encoding('BINARY')
|
206
|
+
str.sub!(/^.*Content-Type: application\/octet-stream\r\n(.*)--Encrypted.*$/m, '\1')
|
207
|
+
|
208
|
+
signature = str[4..19]
|
209
|
+
message = @ntlmcli.session.unseal_message str[20..-1]
|
210
|
+
if @ntlmcli.session.verify_signature(signature, message)
|
211
|
+
message
|
212
|
+
else
|
213
|
+
raise WinRMWebServiceError, "Could not verify SOAP message."
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
def init_auth
|
218
|
+
@logger.debug "Initializing Negotiate for #{@service}"
|
219
|
+
auth1 = @ntlmcli.init_context
|
220
|
+
hdr = {"Authorization" => "Negotiate #{auth1.encode64}",
|
221
|
+
"Content-Type" => "application/soap+xml;charset=UTF-8"
|
222
|
+
}
|
223
|
+
@logger.debug "Sending HTTP POST for Negotiate Authentication"
|
224
|
+
r = @httpcli.post(@endpoint, "", hdr)
|
225
|
+
verify_ssl_fingerprint(r.peer_cert)
|
226
|
+
itok = r.header["WWW-Authenticate"].pop.split.last
|
227
|
+
binding = r.peer_cert.nil? ? nil : Net::NTLM::ChannelBinding.create(r.peer_cert)
|
228
|
+
auth3 = @ntlmcli.init_context(itok, binding)
|
229
|
+
{ "Authorization" => "Negotiate #{auth3.encode64}" }
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
# Uses SSL to secure the transport
|
234
|
+
class BasicAuthSSL < HttpTransport
|
235
|
+
def initialize(endpoint, user, pass, opts)
|
236
|
+
super(endpoint)
|
237
|
+
@httpcli.set_auth(endpoint, user, pass)
|
238
|
+
basic_auth_only!
|
239
|
+
no_ssl_peer_verification! if opts[:no_ssl_peer_verification]
|
240
|
+
@ssl_peer_fingerprint = opts[:ssl_peer_fingerprint]
|
241
|
+
@httpcli.ssl_config.set_trust_ca(opts[:ca_trust_path]) if opts[:ca_trust_path]
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
# Uses Kerberos/GSSAPI to authenticate and encrypt messages
|
246
|
+
# rubocop:disable Metrics/ClassLength
|
247
|
+
class HttpGSSAPI < HttpTransport
|
248
|
+
# @param [String,URI] endpoint the WinRM webservice endpoint
|
249
|
+
# @param [String] realm the Kerberos realm we are authenticating to
|
250
|
+
# @param [String<optional>] service the service name, default is HTTP
|
251
|
+
# @param [String<optional>] keytab the path to a keytab file if you are using one
|
252
|
+
# rubocop:disable Lint/UnusedMethodArgument
|
253
|
+
def initialize(endpoint, realm, service = nil, keytab = nil, opts)
|
254
|
+
# rubocop:enable Lint/UnusedMethodArgument
|
255
|
+
super(endpoint)
|
256
|
+
# Remove the GSSAPI auth from HTTPClient because we are doing our own thing
|
257
|
+
no_sspi_auth!
|
258
|
+
service ||= 'HTTP'
|
259
|
+
@service = "#{service}/#{@endpoint.host}@#{realm}"
|
260
|
+
init_krb
|
261
|
+
end
|
262
|
+
|
263
|
+
# Sends the SOAP payload to the WinRM service and returns the service's
|
264
|
+
# SOAP response. If an error occurrs an appropriate error is raised.
|
265
|
+
#
|
266
|
+
# @param [String] The XML SOAP message
|
267
|
+
# @returns [REXML::Document] The parsed response body
|
268
|
+
def send_request(message)
|
269
|
+
resp = send_kerberos_request(message)
|
270
|
+
|
271
|
+
if resp.status == 401
|
272
|
+
@logger.debug 'Got 401 - reinitializing Kerberos and retrying one more time'
|
273
|
+
init_krb
|
274
|
+
resp = send_kerberos_request(message)
|
275
|
+
end
|
276
|
+
|
277
|
+
handler = WinRM::ResponseHandler.new(winrm_decrypt(resp.http_body.content), resp.status)
|
278
|
+
handler.parse_to_xml
|
279
|
+
end
|
280
|
+
|
281
|
+
private
|
282
|
+
|
283
|
+
# rubocop:disable Metrics/MethodLength
|
284
|
+
# rubocop:disable Metrics/AbcSize
|
285
|
+
|
286
|
+
# Sends the SOAP payload to the WinRM service and returns the service's
|
287
|
+
# HTTP response.
|
288
|
+
#
|
289
|
+
# @param [String] The XML SOAP message
|
290
|
+
# @returns [Object] The HTTP response object
|
291
|
+
def send_kerberos_request(message)
|
292
|
+
log_soap_message(message)
|
293
|
+
original_length = message.length
|
294
|
+
pad_len, emsg = winrm_encrypt(message)
|
295
|
+
hdr = {
|
296
|
+
'Connection' => 'Keep-Alive',
|
297
|
+
'Content-Type' =>
|
298
|
+
'multipart/encrypted;' \
|
299
|
+
'protocol="application/HTTP-Kerberos-session-encrypted";' \
|
300
|
+
'boundary="Encrypted Boundary"'
|
301
|
+
}
|
302
|
+
body = [
|
303
|
+
"--Encrypted Boundary",
|
304
|
+
"Content-Type: application/HTTP-Kerberos-session-encrypted",
|
305
|
+
"OriginalContent: type=application/soap+xml;charset=UTF-8;Length=#{original_length + pad_len}",
|
306
|
+
"--Encrypted Boundary",
|
307
|
+
"Content-Type: application/octet-stream",
|
308
|
+
"#{emsg}--Encrypted Boundary--",
|
309
|
+
""
|
310
|
+
].join("\r\n")
|
311
|
+
|
312
|
+
resp = @httpcli.post(@endpoint, body, hdr)
|
313
|
+
log_soap_message(resp.http_body.content)
|
314
|
+
resp
|
315
|
+
end
|
316
|
+
|
317
|
+
def init_krb
|
318
|
+
@logger.debug "Initializing Kerberos for #{@service}"
|
319
|
+
@gsscli = GSSAPI::Simple.new(@endpoint.host, @service)
|
320
|
+
token = @gsscli.init_context
|
321
|
+
auth = Base64.strict_encode64 token
|
322
|
+
|
323
|
+
hdr = {
|
324
|
+
'Authorization' => "Kerberos #{auth}",
|
325
|
+
'Connection' => 'Keep-Alive',
|
326
|
+
'Content-Type' => 'application/soap+xml;charset=UTF-8'
|
327
|
+
}
|
328
|
+
@logger.debug 'Sending HTTP POST for Kerberos Authentication'
|
329
|
+
r = @httpcli.post(@endpoint, '', hdr)
|
330
|
+
itok = r.header['WWW-Authenticate'].pop
|
331
|
+
itok = itok.split.last
|
332
|
+
itok = Base64.strict_decode64(itok)
|
333
|
+
@gsscli.init_context(itok)
|
334
|
+
end
|
335
|
+
|
336
|
+
# @return [String] the encrypted request string
|
337
|
+
def winrm_encrypt(str)
|
338
|
+
@logger.debug "Encrypting SOAP message:\n#{str}"
|
339
|
+
iov_cnt = 3
|
340
|
+
iov = FFI::MemoryPointer.new(GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * iov_cnt)
|
341
|
+
|
342
|
+
iov0 = GSSAPI::LibGSSAPI::GssIOVBufferDesc.new(FFI::Pointer.new(iov.address))
|
343
|
+
iov0[:type] = (GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_HEADER | \
|
344
|
+
GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_FLAG_ALLOCATE)
|
345
|
+
|
346
|
+
iov1 = GSSAPI::LibGSSAPI::GssIOVBufferDesc.new(
|
347
|
+
FFI::Pointer.new(iov.address + (GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * 1)))
|
348
|
+
iov1[:type] = (GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_DATA)
|
349
|
+
iov1[:buffer].value = str
|
350
|
+
|
351
|
+
iov2 = GSSAPI::LibGSSAPI::GssIOVBufferDesc.new(
|
352
|
+
FFI::Pointer.new(iov.address + (GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * 2)))
|
353
|
+
iov2[:type] = (GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_PADDING | \
|
354
|
+
GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_FLAG_ALLOCATE)
|
355
|
+
|
356
|
+
conf_state = FFI::MemoryPointer.new :uint32
|
357
|
+
min_stat = FFI::MemoryPointer.new :uint32
|
358
|
+
|
359
|
+
GSSAPI::LibGSSAPI.gss_wrap_iov(
|
360
|
+
min_stat,
|
361
|
+
@gsscli.context,
|
362
|
+
1,
|
363
|
+
GSSAPI::LibGSSAPI::GSS_C_QOP_DEFAULT,
|
364
|
+
conf_state,
|
365
|
+
iov,
|
366
|
+
iov_cnt)
|
367
|
+
|
368
|
+
token = [iov0[:buffer].length].pack('L')
|
369
|
+
token += iov0[:buffer].value
|
370
|
+
token += iov1[:buffer].value
|
371
|
+
pad_len = iov2[:buffer].length
|
372
|
+
token += iov2[:buffer].value if pad_len > 0
|
373
|
+
[pad_len, token]
|
374
|
+
end
|
375
|
+
|
376
|
+
# @return [String] the unencrypted response string
|
377
|
+
def winrm_decrypt(str)
|
378
|
+
@logger.debug "Decrypting SOAP message:\n#{str}"
|
379
|
+
iov_cnt = 3
|
380
|
+
iov = FFI::MemoryPointer.new(GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * iov_cnt)
|
381
|
+
|
382
|
+
iov0 = GSSAPI::LibGSSAPI::GssIOVBufferDesc.new(FFI::Pointer.new(iov.address))
|
383
|
+
iov0[:type] = (GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_HEADER | \
|
384
|
+
GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_FLAG_ALLOCATE)
|
385
|
+
|
386
|
+
iov1 = GSSAPI::LibGSSAPI::GssIOVBufferDesc.new(
|
387
|
+
FFI::Pointer.new(iov.address + (GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * 1)))
|
388
|
+
iov1[:type] = (GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_DATA)
|
389
|
+
|
390
|
+
iov2 = GSSAPI::LibGSSAPI::GssIOVBufferDesc.new(
|
391
|
+
FFI::Pointer.new(iov.address + (GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * 2)))
|
392
|
+
iov2[:type] = (GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_DATA)
|
393
|
+
|
394
|
+
str.force_encoding('BINARY')
|
395
|
+
str.sub!(/^.*Content-Type: application\/octet-stream\r\n(.*)--Encrypted.*$/m, '\1')
|
396
|
+
|
397
|
+
len = str.unpack('L').first
|
398
|
+
iov_data = str.unpack("LA#{len}A*")
|
399
|
+
iov0[:buffer].value = iov_data[1]
|
400
|
+
iov1[:buffer].value = iov_data[2]
|
401
|
+
|
402
|
+
min_stat = FFI::MemoryPointer.new :uint32
|
403
|
+
conf_state = FFI::MemoryPointer.new :uint32
|
404
|
+
conf_state.write_int(1)
|
405
|
+
qop_state = FFI::MemoryPointer.new :uint32
|
406
|
+
qop_state.write_int(0)
|
407
|
+
|
408
|
+
maj_stat = GSSAPI::LibGSSAPI.gss_unwrap_iov(
|
409
|
+
min_stat, @gsscli.context, conf_state, qop_state, iov, iov_cnt)
|
410
|
+
|
411
|
+
@logger.debug "SOAP message decrypted (MAJ: #{maj_stat}, " \
|
412
|
+
"MIN: #{min_stat.read_int}):\n#{iov1[:buffer].value}"
|
413
|
+
|
414
|
+
iov1[:buffer].value
|
415
|
+
end
|
416
|
+
# rubocop:enable Metrics/MethodLength
|
417
|
+
# rubocop:enable Metrics/AbcSize
|
418
|
+
end
|
419
|
+
# rubocop:enable Metrics/ClassLength
|
420
|
+
end
|
421
|
+
end # WinRM
|