winrm 1.3.6 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
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