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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +10 -10
  3. data/.rspec +3 -3
  4. data/.rubocop.yml +12 -12
  5. data/.travis.yml +12 -12
  6. data/Gemfile +9 -9
  7. data/LICENSE +202 -202
  8. data/README.md +194 -194
  9. data/Rakefile +36 -36
  10. data/Vagrantfile +9 -9
  11. data/appveyor.yml +42 -42
  12. data/bin/rwinrm +97 -97
  13. data/changelog.md +74 -71
  14. data/lib/winrm.rb +42 -42
  15. data/lib/winrm/command_executor.rb +224 -224
  16. data/lib/winrm/exceptions/exceptions.rb +57 -57
  17. data/lib/winrm/helpers/iso8601_duration.rb +58 -58
  18. data/lib/winrm/helpers/powershell_script.rb +42 -42
  19. data/lib/winrm/http/response_handler.rb +82 -82
  20. data/lib/winrm/http/transport.rb +421 -421
  21. data/lib/winrm/output.rb +43 -43
  22. data/lib/winrm/soap_provider.rb +39 -39
  23. data/lib/winrm/version.rb +7 -7
  24. data/lib/winrm/winrm_service.rb +556 -556
  25. data/preamble +17 -17
  26. data/spec/auth_timeout_spec.rb +16 -16
  27. data/spec/cmd_spec.rb +102 -102
  28. data/spec/command_executor_spec.rb +429 -429
  29. data/spec/config-example.yml +19 -19
  30. data/spec/exception_spec.rb +50 -50
  31. data/spec/issue_184_spec.rb +67 -67
  32. data/spec/issue_59_spec.rb +23 -23
  33. data/spec/matchers.rb +74 -74
  34. data/spec/output_spec.rb +110 -110
  35. data/spec/powershell_spec.rb +97 -97
  36. data/spec/response_handler_spec.rb +59 -59
  37. data/spec/spec_helper.rb +73 -73
  38. data/spec/stubs/responses/get_command_output_response.xml.erb +13 -13
  39. data/spec/stubs/responses/open_shell_v1.xml +19 -19
  40. data/spec/stubs/responses/open_shell_v2.xml +20 -20
  41. data/spec/stubs/responses/soap_fault_v1.xml +36 -36
  42. data/spec/stubs/responses/soap_fault_v2.xml +42 -42
  43. data/spec/stubs/responses/wmi_error_v2.xml +41 -41
  44. data/spec/transport_spec.rb +124 -124
  45. data/spec/winrm_options_spec.rb +76 -76
  46. data/spec/winrm_primitives_spec.rb +51 -51
  47. data/spec/wql_spec.rb +14 -14
  48. data/winrm.gemspec +40 -40
  49. 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
@@ -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