winrm 1.3.6 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +10 -10
  3. data/.rspec +3 -3
  4. data/.rubocop.yml +9 -9
  5. data/.travis.yml +4 -4
  6. data/Gemfile +9 -9
  7. data/LICENSE +202 -202
  8. data/README.md +148 -148
  9. data/Rakefile +28 -28
  10. data/Vagrantfile +9 -9
  11. data/bin/rwinrm +97 -97
  12. data/changelog.md +49 -49
  13. data/lib/winrm.rb +42 -41
  14. data/lib/winrm/exceptions/exceptions.rb +57 -57
  15. data/lib/winrm/helpers/iso8601_duration.rb +58 -58
  16. data/lib/winrm/helpers/powershell_script.rb +37 -37
  17. data/lib/winrm/http/response_handler.rb +82 -82
  18. data/lib/winrm/http/transport.rb +294 -294
  19. data/lib/winrm/output.rb +43 -43
  20. data/lib/winrm/soap_provider.rb +40 -40
  21. data/lib/winrm/version.rb +7 -0
  22. data/lib/winrm/winrm_service.rb +490 -490
  23. data/preamble +17 -17
  24. data/spec/auth_timeout_spec.rb +16 -16
  25. data/spec/cmd_spec.rb +102 -102
  26. data/spec/config-example.yml +19 -19
  27. data/spec/exception_spec.rb +50 -50
  28. data/spec/issue_59_spec.rb +15 -15
  29. data/spec/matchers.rb +74 -74
  30. data/spec/output_spec.rb +110 -110
  31. data/spec/powershell_spec.rb +103 -103
  32. data/spec/response_handler_spec.rb +59 -59
  33. data/spec/spec_helper.rb +48 -48
  34. data/spec/stubs/responses/open_shell_v1.xml +19 -19
  35. data/spec/stubs/responses/open_shell_v2.xml +20 -20
  36. data/spec/stubs/responses/soap_fault_v1.xml +36 -36
  37. data/spec/stubs/responses/soap_fault_v2.xml +42 -42
  38. data/spec/stubs/responses/wmi_error_v2.xml +41 -41
  39. data/spec/winrm_options_spec.rb +76 -76
  40. data/spec/winrm_primitives_spec.rb +51 -51
  41. data/spec/wql_spec.rb +14 -14
  42. data/winrm.gemspec +40 -41
  43. metadata +4 -4
  44. data/VERSION +0 -1
@@ -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,294 +1,294 @@
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
- log_soap_message(message)
44
- hdr = {
45
- '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
- handler = WinRM::ResponseHandler.new(resp.http_body.content, resp.status)
50
- handler.parse_to_xml
51
- end
52
-
53
- # We'll need this to force basic authentication if desired
54
- def basic_auth_only!
55
- auths = @httpcli.www_auth.instance_variable_get('@authenticator')
56
- auths.delete_if { |i| i.scheme !~ /basic/i }
57
- end
58
-
59
- # Disable SSPI Auth
60
- def no_sspi_auth!
61
- auths = @httpcli.www_auth.instance_variable_get('@authenticator')
62
- auths.delete_if { |i| i.is_a? HTTPClient::SSPINegotiateAuth }
63
- end
64
-
65
- # Disable SSL Peer Verification
66
- def no_ssl_peer_verification!
67
- @httpcli.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE
68
- end
69
-
70
- # HTTP Client receive timeout. How long should a remote call wait for a
71
- # for a response from WinRM?
72
- def receive_timeout=(sec)
73
- @httpcli.receive_timeout = sec
74
- end
75
-
76
- def receive_timeout
77
- @httpcli.receive_timeout
78
- end
79
-
80
- protected
81
-
82
- def log_soap_message(message)
83
- return unless @logger.debug?
84
-
85
- xml_msg = REXML::Document.new(message)
86
- formatter = REXML::Formatters::Pretty.new(2)
87
- formatter.compact = true
88
- formatter.write(xml_msg, @logger)
89
- @logger.debug("\n")
90
- rescue StandardError => e
91
- @logger.debug("Couldn't log SOAP request/response: #{e.message} - #{message}")
92
- end
93
- end
94
-
95
- # Plain text, insecure, HTTP transport
96
- class HttpPlaintext < HttpTransport
97
- def initialize(endpoint, user, pass, opts)
98
- super(endpoint)
99
- @httpcli.set_auth(nil, user, pass)
100
- no_sspi_auth! if opts[:disable_sspi]
101
- basic_auth_only! if opts[:basic_auth_only]
102
- no_ssl_peer_verification! if opts[:no_ssl_peer_verification]
103
- end
104
- end
105
-
106
- # Uses SSL to secure the transport
107
- class HttpSSL < HttpTransport
108
- def initialize(endpoint, user, pass, ca_trust_path = nil, opts)
109
- super(endpoint)
110
- @httpcli.set_auth(endpoint, user, pass)
111
- @httpcli.ssl_config.set_trust_ca(ca_trust_path) unless ca_trust_path.nil?
112
- no_sspi_auth! if opts[:disable_sspi]
113
- basic_auth_only! if opts[:basic_auth_only]
114
- no_ssl_peer_verification! if opts[:no_ssl_peer_verification]
115
- end
116
- end
117
-
118
- # Uses Kerberos/GSSAPI to authenticate and encrypt messages
119
- # rubocop:disable Metrics/ClassLength
120
- class HttpGSSAPI < HttpTransport
121
- # @param [String,URI] endpoint the WinRM webservice endpoint
122
- # @param [String] realm the Kerberos realm we are authenticating to
123
- # @param [String<optional>] service the service name, default is HTTP
124
- # @param [String<optional>] keytab the path to a keytab file if you are using one
125
- # rubocop:disable Lint/UnusedMethodArgument
126
- def initialize(endpoint, realm, service = nil, keytab = nil, opts)
127
- # rubocop:enable Lint/UnusedMethodArgument
128
- super(endpoint)
129
- # Remove the GSSAPI auth from HTTPClient because we are doing our own thing
130
- no_sspi_auth!
131
- service ||= 'HTTP'
132
- @service = "#{service}/#{@endpoint.host}@#{realm}"
133
- init_krb
134
- end
135
-
136
- # Sends the SOAP payload to the WinRM service and returns the service's
137
- # SOAP response. If an error occurrs an appropriate error is raised.
138
- #
139
- # @param [String] The XML SOAP message
140
- # @returns [REXML::Document] The parsed response body
141
- def send_request(message)
142
- resp = send_kerberos_request(message)
143
-
144
- if resp.status == 401
145
- @logger.debug 'Got 401 - reinitializing Kerberos and retrying one more time'
146
- init_krb
147
- resp = send_kerberos_request(message)
148
- end
149
-
150
- handler = WinRM::ResponseHandler.new(winrm_decrypt(resp.http_body.content), resp.status)
151
- handler.parse_to_xml
152
- end
153
-
154
- private
155
-
156
- # rubocop:disable Metrics/MethodLength
157
- # rubocop:disable Metrics/AbcSize
158
-
159
- # Sends the SOAP payload to the WinRM service and returns the service's
160
- # HTTP response.
161
- #
162
- # @param [String] The XML SOAP message
163
- # @returns [Object] The HTTP response object
164
- def send_kerberos_request(message)
165
- log_soap_message(message)
166
- original_length = message.length
167
- pad_len, emsg = winrm_encrypt(message)
168
- hdr = {
169
- 'Connection' => 'Keep-Alive',
170
- 'Content-Type' =>
171
- 'multipart/encrypted;' \
172
- 'protocol="application/HTTP-Kerberos-session-encrypted";' \
173
- 'boundary="Encrypted Boundary"'
174
- }
175
-
176
- body = <<-EOF
177
- --Encrypted Boundary\r
178
- Content-Type: application/HTTP-Kerberos-session-encrypted\r
179
- OriginalContent: type=application/soap+xml;charset=UTF-8;Length=#{original_length + pad_len}\r
180
- --Encrypted Boundary\r
181
- Content-Type: application/octet-stream\r
182
- #{emsg}--Encrypted Boundary\r
183
- EOF
184
-
185
- resp = @httpcli.post(@endpoint, body, hdr)
186
- log_soap_message(resp.http_body.content)
187
- resp
188
- end
189
-
190
- def init_krb
191
- @logger.debug "Initializing Kerberos for #{@service}"
192
- @gsscli = GSSAPI::Simple.new(@endpoint.host, @service)
193
- token = @gsscli.init_context
194
- auth = Base64.strict_encode64 token
195
-
196
- hdr = {
197
- 'Authorization' => "Kerberos #{auth}",
198
- 'Connection' => 'Keep-Alive',
199
- 'Content-Type' => 'application/soap+xml;charset=UTF-8'
200
- }
201
- @logger.debug 'Sending HTTP POST for Kerberos Authentication'
202
- r = @httpcli.post(@endpoint, '', hdr)
203
- itok = r.header['WWW-Authenticate'].pop
204
- itok = itok.split.last
205
- itok = Base64.strict_decode64(itok)
206
- @gsscli.init_context(itok)
207
- end
208
-
209
- # @return [String] the encrypted request string
210
- def winrm_encrypt(str)
211
- @logger.debug "Encrypting SOAP message:\n#{str}"
212
- iov_cnt = 3
213
- iov = FFI::MemoryPointer.new(GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * iov_cnt)
214
-
215
- iov0 = GSSAPI::LibGSSAPI::GssIOVBufferDesc.new(FFI::Pointer.new(iov.address))
216
- iov0[:type] = (GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_HEADER | \
217
- GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_FLAG_ALLOCATE)
218
-
219
- iov1 = GSSAPI::LibGSSAPI::GssIOVBufferDesc.new(
220
- FFI::Pointer.new(iov.address + (GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * 1)))
221
- iov1[:type] = (GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_DATA)
222
- iov1[:buffer].value = str
223
-
224
- iov2 = GSSAPI::LibGSSAPI::GssIOVBufferDesc.new(
225
- FFI::Pointer.new(iov.address + (GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * 2)))
226
- iov2[:type] = (GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_PADDING | \
227
- GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_FLAG_ALLOCATE)
228
-
229
- conf_state = FFI::MemoryPointer.new :uint32
230
- min_stat = FFI::MemoryPointer.new :uint32
231
-
232
- GSSAPI::LibGSSAPI.gss_wrap_iov(
233
- min_stat,
234
- @gsscli.context,
235
- 1,
236
- GSSAPI::LibGSSAPI::GSS_C_QOP_DEFAULT,
237
- conf_state,
238
- iov,
239
- iov_cnt)
240
-
241
- token = [iov0[:buffer].length].pack('L')
242
- token += iov0[:buffer].value
243
- token += iov1[:buffer].value
244
- pad_len = iov2[:buffer].length
245
- token += iov2[:buffer].value if pad_len > 0
246
- [pad_len, token]
247
- end
248
-
249
- # @return [String] the unencrypted response string
250
- def winrm_decrypt(str)
251
- @logger.debug "Decrypting SOAP message:\n#{str}"
252
- iov_cnt = 3
253
- iov = FFI::MemoryPointer.new(GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * iov_cnt)
254
-
255
- iov0 = GSSAPI::LibGSSAPI::GssIOVBufferDesc.new(FFI::Pointer.new(iov.address))
256
- iov0[:type] = (GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_HEADER | \
257
- GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_FLAG_ALLOCATE)
258
-
259
- iov1 = GSSAPI::LibGSSAPI::GssIOVBufferDesc.new(
260
- FFI::Pointer.new(iov.address + (GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * 1)))
261
- iov1[:type] = (GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_DATA)
262
-
263
- iov2 = GSSAPI::LibGSSAPI::GssIOVBufferDesc.new(
264
- FFI::Pointer.new(iov.address + (GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * 2)))
265
- iov2[:type] = (GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_DATA)
266
-
267
- str.force_encoding('BINARY')
268
- str.sub!(/^.*Content-Type: application\/octet-stream\r\n(.*)--Encrypted.*$/m, '\1')
269
-
270
- len = str.unpack('L').first
271
- iov_data = str.unpack("LA#{len}A*")
272
- iov0[:buffer].value = iov_data[1]
273
- iov1[:buffer].value = iov_data[2]
274
-
275
- min_stat = FFI::MemoryPointer.new :uint32
276
- conf_state = FFI::MemoryPointer.new :uint32
277
- conf_state.write_int(1)
278
- qop_state = FFI::MemoryPointer.new :uint32
279
- qop_state.write_int(0)
280
-
281
- maj_stat = GSSAPI::LibGSSAPI.gss_unwrap_iov(
282
- min_stat, @gsscli.context, conf_state, qop_state, iov, iov_cnt)
283
-
284
- @logger.debug "SOAP message decrypted (MAJ: #{maj_stat}, " \
285
- "MIN: #{min_stat.read_int}):\n#{iov1[:buffer].value}"
286
-
287
- iov1[:buffer].value
288
- end
289
- # rubocop:enable Metrics/MethodLength
290
- # rubocop:enable Metrics/AbcSize
291
- end
292
- # rubocop:enable Metrics/ClassLength
293
- end
294
- 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
+ log_soap_message(message)
44
+ hdr = {
45
+ '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
+ handler = WinRM::ResponseHandler.new(resp.http_body.content, resp.status)
50
+ handler.parse_to_xml
51
+ end
52
+
53
+ # We'll need this to force basic authentication if desired
54
+ def basic_auth_only!
55
+ auths = @httpcli.www_auth.instance_variable_get('@authenticator')
56
+ auths.delete_if { |i| i.scheme !~ /basic/i }
57
+ end
58
+
59
+ # Disable SSPI Auth
60
+ def no_sspi_auth!
61
+ auths = @httpcli.www_auth.instance_variable_get('@authenticator')
62
+ auths.delete_if { |i| i.is_a? HTTPClient::SSPINegotiateAuth }
63
+ end
64
+
65
+ # Disable SSL Peer Verification
66
+ def no_ssl_peer_verification!
67
+ @httpcli.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE
68
+ end
69
+
70
+ # HTTP Client receive timeout. How long should a remote call wait for a
71
+ # for a response from WinRM?
72
+ def receive_timeout=(sec)
73
+ @httpcli.receive_timeout = sec
74
+ end
75
+
76
+ def receive_timeout
77
+ @httpcli.receive_timeout
78
+ end
79
+
80
+ protected
81
+
82
+ def log_soap_message(message)
83
+ return unless @logger.debug?
84
+
85
+ xml_msg = REXML::Document.new(message)
86
+ formatter = REXML::Formatters::Pretty.new(2)
87
+ formatter.compact = true
88
+ formatter.write(xml_msg, @logger)
89
+ @logger.debug("\n")
90
+ rescue StandardError => e
91
+ @logger.debug("Couldn't log SOAP request/response: #{e.message} - #{message}")
92
+ end
93
+ end
94
+
95
+ # Plain text, insecure, HTTP transport
96
+ class HttpPlaintext < HttpTransport
97
+ def initialize(endpoint, user, pass, opts)
98
+ super(endpoint)
99
+ @httpcli.set_auth(nil, user, pass)
100
+ no_sspi_auth! if opts[:disable_sspi]
101
+ basic_auth_only! if opts[:basic_auth_only]
102
+ no_ssl_peer_verification! if opts[:no_ssl_peer_verification]
103
+ end
104
+ end
105
+
106
+ # Uses SSL to secure the transport
107
+ class HttpSSL < HttpTransport
108
+ def initialize(endpoint, user, pass, ca_trust_path = nil, opts)
109
+ super(endpoint)
110
+ @httpcli.set_auth(endpoint, user, pass)
111
+ @httpcli.ssl_config.set_trust_ca(ca_trust_path) unless ca_trust_path.nil?
112
+ no_sspi_auth! if opts[:disable_sspi]
113
+ basic_auth_only! if opts[:basic_auth_only]
114
+ no_ssl_peer_verification! if opts[:no_ssl_peer_verification]
115
+ end
116
+ end
117
+
118
+ # Uses Kerberos/GSSAPI to authenticate and encrypt messages
119
+ # rubocop:disable Metrics/ClassLength
120
+ class HttpGSSAPI < HttpTransport
121
+ # @param [String,URI] endpoint the WinRM webservice endpoint
122
+ # @param [String] realm the Kerberos realm we are authenticating to
123
+ # @param [String<optional>] service the service name, default is HTTP
124
+ # @param [String<optional>] keytab the path to a keytab file if you are using one
125
+ # rubocop:disable Lint/UnusedMethodArgument
126
+ def initialize(endpoint, realm, service = nil, keytab = nil, opts)
127
+ # rubocop:enable Lint/UnusedMethodArgument
128
+ super(endpoint)
129
+ # Remove the GSSAPI auth from HTTPClient because we are doing our own thing
130
+ no_sspi_auth!
131
+ service ||= 'HTTP'
132
+ @service = "#{service}/#{@endpoint.host}@#{realm}"
133
+ init_krb
134
+ end
135
+
136
+ # Sends the SOAP payload to the WinRM service and returns the service's
137
+ # SOAP response. If an error occurrs an appropriate error is raised.
138
+ #
139
+ # @param [String] The XML SOAP message
140
+ # @returns [REXML::Document] The parsed response body
141
+ def send_request(message)
142
+ resp = send_kerberos_request(message)
143
+
144
+ if resp.status == 401
145
+ @logger.debug 'Got 401 - reinitializing Kerberos and retrying one more time'
146
+ init_krb
147
+ resp = send_kerberos_request(message)
148
+ end
149
+
150
+ handler = WinRM::ResponseHandler.new(winrm_decrypt(resp.http_body.content), resp.status)
151
+ handler.parse_to_xml
152
+ end
153
+
154
+ private
155
+
156
+ # rubocop:disable Metrics/MethodLength
157
+ # rubocop:disable Metrics/AbcSize
158
+
159
+ # Sends the SOAP payload to the WinRM service and returns the service's
160
+ # HTTP response.
161
+ #
162
+ # @param [String] The XML SOAP message
163
+ # @returns [Object] The HTTP response object
164
+ def send_kerberos_request(message)
165
+ log_soap_message(message)
166
+ original_length = message.length
167
+ pad_len, emsg = winrm_encrypt(message)
168
+ hdr = {
169
+ 'Connection' => 'Keep-Alive',
170
+ 'Content-Type' =>
171
+ 'multipart/encrypted;' \
172
+ 'protocol="application/HTTP-Kerberos-session-encrypted";' \
173
+ 'boundary="Encrypted Boundary"'
174
+ }
175
+
176
+ body = <<-EOF
177
+ --Encrypted Boundary\r
178
+ Content-Type: application/HTTP-Kerberos-session-encrypted\r
179
+ OriginalContent: type=application/soap+xml;charset=UTF-8;Length=#{original_length + pad_len}\r
180
+ --Encrypted Boundary\r
181
+ Content-Type: application/octet-stream\r
182
+ #{emsg}--Encrypted Boundary\r
183
+ EOF
184
+
185
+ resp = @httpcli.post(@endpoint, body, hdr)
186
+ log_soap_message(resp.http_body.content)
187
+ resp
188
+ end
189
+
190
+ def init_krb
191
+ @logger.debug "Initializing Kerberos for #{@service}"
192
+ @gsscli = GSSAPI::Simple.new(@endpoint.host, @service)
193
+ token = @gsscli.init_context
194
+ auth = Base64.strict_encode64 token
195
+
196
+ hdr = {
197
+ 'Authorization' => "Kerberos #{auth}",
198
+ 'Connection' => 'Keep-Alive',
199
+ 'Content-Type' => 'application/soap+xml;charset=UTF-8'
200
+ }
201
+ @logger.debug 'Sending HTTP POST for Kerberos Authentication'
202
+ r = @httpcli.post(@endpoint, '', hdr)
203
+ itok = r.header['WWW-Authenticate'].pop
204
+ itok = itok.split.last
205
+ itok = Base64.strict_decode64(itok)
206
+ @gsscli.init_context(itok)
207
+ end
208
+
209
+ # @return [String] the encrypted request string
210
+ def winrm_encrypt(str)
211
+ @logger.debug "Encrypting SOAP message:\n#{str}"
212
+ iov_cnt = 3
213
+ iov = FFI::MemoryPointer.new(GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * iov_cnt)
214
+
215
+ iov0 = GSSAPI::LibGSSAPI::GssIOVBufferDesc.new(FFI::Pointer.new(iov.address))
216
+ iov0[:type] = (GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_HEADER | \
217
+ GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_FLAG_ALLOCATE)
218
+
219
+ iov1 = GSSAPI::LibGSSAPI::GssIOVBufferDesc.new(
220
+ FFI::Pointer.new(iov.address + (GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * 1)))
221
+ iov1[:type] = (GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_DATA)
222
+ iov1[:buffer].value = str
223
+
224
+ iov2 = GSSAPI::LibGSSAPI::GssIOVBufferDesc.new(
225
+ FFI::Pointer.new(iov.address + (GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * 2)))
226
+ iov2[:type] = (GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_PADDING | \
227
+ GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_FLAG_ALLOCATE)
228
+
229
+ conf_state = FFI::MemoryPointer.new :uint32
230
+ min_stat = FFI::MemoryPointer.new :uint32
231
+
232
+ GSSAPI::LibGSSAPI.gss_wrap_iov(
233
+ min_stat,
234
+ @gsscli.context,
235
+ 1,
236
+ GSSAPI::LibGSSAPI::GSS_C_QOP_DEFAULT,
237
+ conf_state,
238
+ iov,
239
+ iov_cnt)
240
+
241
+ token = [iov0[:buffer].length].pack('L')
242
+ token += iov0[:buffer].value
243
+ token += iov1[:buffer].value
244
+ pad_len = iov2[:buffer].length
245
+ token += iov2[:buffer].value if pad_len > 0
246
+ [pad_len, token]
247
+ end
248
+
249
+ # @return [String] the unencrypted response string
250
+ def winrm_decrypt(str)
251
+ @logger.debug "Decrypting SOAP message:\n#{str}"
252
+ iov_cnt = 3
253
+ iov = FFI::MemoryPointer.new(GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * iov_cnt)
254
+
255
+ iov0 = GSSAPI::LibGSSAPI::GssIOVBufferDesc.new(FFI::Pointer.new(iov.address))
256
+ iov0[:type] = (GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_HEADER | \
257
+ GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_FLAG_ALLOCATE)
258
+
259
+ iov1 = GSSAPI::LibGSSAPI::GssIOVBufferDesc.new(
260
+ FFI::Pointer.new(iov.address + (GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * 1)))
261
+ iov1[:type] = (GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_DATA)
262
+
263
+ iov2 = GSSAPI::LibGSSAPI::GssIOVBufferDesc.new(
264
+ FFI::Pointer.new(iov.address + (GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * 2)))
265
+ iov2[:type] = (GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_DATA)
266
+
267
+ str.force_encoding('BINARY')
268
+ str.sub!(/^.*Content-Type: application\/octet-stream\r\n(.*)--Encrypted.*$/m, '\1')
269
+
270
+ len = str.unpack('L').first
271
+ iov_data = str.unpack("LA#{len}A*")
272
+ iov0[:buffer].value = iov_data[1]
273
+ iov1[:buffer].value = iov_data[2]
274
+
275
+ min_stat = FFI::MemoryPointer.new :uint32
276
+ conf_state = FFI::MemoryPointer.new :uint32
277
+ conf_state.write_int(1)
278
+ qop_state = FFI::MemoryPointer.new :uint32
279
+ qop_state.write_int(0)
280
+
281
+ maj_stat = GSSAPI::LibGSSAPI.gss_unwrap_iov(
282
+ min_stat, @gsscli.context, conf_state, qop_state, iov, iov_cnt)
283
+
284
+ @logger.debug "SOAP message decrypted (MAJ: #{maj_stat}, " \
285
+ "MIN: #{min_stat.read_int}):\n#{iov1[:buffer].value}"
286
+
287
+ iov1[:buffer].value
288
+ end
289
+ # rubocop:enable Metrics/MethodLength
290
+ # rubocop:enable Metrics/AbcSize
291
+ end
292
+ # rubocop:enable Metrics/ClassLength
293
+ end
294
+ end # WinRM