winrm 2.0.3 → 2.1.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 (127) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +10 -10
  3. data/.rubocop.yml +26 -26
  4. data/.travis.yml +11 -11
  5. data/Gemfile +3 -3
  6. data/README.md +260 -233
  7. data/Rakefile +34 -34
  8. data/Vagrantfile +6 -6
  9. data/WinrmAppveyor.psm1 +31 -31
  10. data/appveyor.yml +51 -51
  11. data/changelog.md +104 -101
  12. data/lib/winrm.rb +39 -39
  13. data/lib/winrm/connection.rb +83 -82
  14. data/lib/winrm/connection_opts.rb +91 -91
  15. data/lib/winrm/exceptions.rb +76 -76
  16. data/lib/winrm/http/response_handler.rb +96 -96
  17. data/lib/winrm/http/transport.rb +424 -424
  18. data/lib/winrm/http/transport_factory.rb +68 -68
  19. data/lib/winrm/output.rb +59 -59
  20. data/lib/winrm/psrp/create_pipeline.xml.erb +167 -167
  21. data/lib/winrm/psrp/fragment.rb +70 -70
  22. data/lib/winrm/psrp/init_runspace_pool.xml.erb +224 -224
  23. data/lib/winrm/psrp/message.rb +130 -130
  24. data/lib/winrm/psrp/message_data.rb +42 -42
  25. data/lib/winrm/psrp/message_data/base.rb +49 -49
  26. data/lib/winrm/psrp/message_data/error_record.rb +68 -68
  27. data/lib/winrm/psrp/message_data/pipeline_host_call.rb +32 -32
  28. data/lib/winrm/psrp/message_data/pipeline_output.rb +49 -49
  29. data/lib/winrm/psrp/message_data/pipeline_state.rb +40 -40
  30. data/lib/winrm/psrp/message_data/runspacepool_host_call.rb +32 -32
  31. data/lib/winrm/psrp/message_data/runspacepool_state.rb +39 -39
  32. data/lib/winrm/psrp/message_data/session_capability.rb +36 -36
  33. data/lib/winrm/psrp/message_defragmenter.rb +62 -62
  34. data/lib/winrm/psrp/message_factory.rb +75 -75
  35. data/lib/winrm/psrp/message_fragmenter.rb +60 -60
  36. data/lib/winrm/psrp/powershell_output_decoder.rb +139 -139
  37. data/lib/winrm/psrp/receive_response_reader.rb +97 -97
  38. data/lib/winrm/psrp/session_capability.xml.erb +7 -7
  39. data/lib/winrm/psrp/uuid.rb +40 -40
  40. data/lib/winrm/shells/base.rb +180 -175
  41. data/lib/winrm/shells/cmd.rb +65 -65
  42. data/lib/winrm/shells/power_shell.rb +202 -202
  43. data/lib/winrm/shells/retryable.rb +45 -45
  44. data/lib/winrm/shells/shell_factory.rb +58 -50
  45. data/lib/winrm/version.rb +7 -7
  46. data/lib/winrm/wsmv/base.rb +59 -59
  47. data/lib/winrm/wsmv/cleanup_command.rb +61 -61
  48. data/lib/winrm/wsmv/close_shell.rb +50 -50
  49. data/lib/winrm/wsmv/command.rb +101 -101
  50. data/lib/winrm/wsmv/command_output.rb +76 -76
  51. data/lib/winrm/wsmv/command_output_decoder.rb +55 -55
  52. data/lib/winrm/wsmv/configuration.rb +46 -46
  53. data/lib/winrm/wsmv/create_pipeline.rb +66 -66
  54. data/lib/winrm/wsmv/create_shell.rb +119 -119
  55. data/lib/winrm/wsmv/header.rb +203 -203
  56. data/lib/winrm/wsmv/init_runspace_pool.rb +95 -95
  57. data/lib/winrm/wsmv/iso8601_duration.rb +60 -60
  58. data/lib/winrm/wsmv/keep_alive.rb +68 -68
  59. data/lib/winrm/wsmv/receive_response_reader.rb +126 -126
  60. data/lib/winrm/wsmv/send_data.rb +68 -68
  61. data/lib/winrm/wsmv/soap.rb +51 -51
  62. data/lib/winrm/wsmv/wql_query.rb +79 -79
  63. data/lib/winrm/wsmv/write_stdin.rb +88 -88
  64. data/tests/integration/auth_timeout_spec.rb +18 -18
  65. data/tests/integration/cmd_spec.rb +131 -110
  66. data/tests/integration/config-example.yml +16 -16
  67. data/tests/integration/issue_59_spec.rb +26 -26
  68. data/tests/integration/powershell_spec.rb +165 -165
  69. data/tests/integration/spec_helper.rb +65 -65
  70. data/tests/integration/transport_spec.rb +99 -99
  71. data/tests/integration/wql_spec.rb +16 -16
  72. data/tests/matchers.rb +60 -60
  73. data/tests/spec/configuration_spec.rb +184 -184
  74. data/tests/spec/connection_spec.rb +39 -39
  75. data/tests/spec/exception_spec.rb +50 -50
  76. data/tests/spec/http/transport_factory_spec.rb +68 -68
  77. data/tests/spec/http/transport_spec.rb +44 -44
  78. data/tests/spec/output_spec.rb +127 -127
  79. data/tests/spec/psrp/fragment_spec.rb +62 -62
  80. data/tests/spec/psrp/message_data/base_spec.rb +13 -13
  81. data/tests/spec/psrp/message_data/error_record_spec.rb +41 -41
  82. data/tests/spec/psrp/message_data/pipeline_host_call_spec.rb +25 -25
  83. data/tests/spec/psrp/message_data/pipeline_output_spec.rb +32 -32
  84. data/tests/spec/psrp/message_data/pipeline_state_spec.rb +40 -40
  85. data/tests/spec/psrp/message_data/runspace_pool_host_call_spec.rb +25 -25
  86. data/tests/spec/psrp/message_data/runspacepool_state_spec.rb +16 -16
  87. data/tests/spec/psrp/message_data/session_capability_spec.rb +30 -30
  88. data/tests/spec/psrp/message_data_spec.rb +35 -35
  89. data/tests/spec/psrp/message_defragmenter_spec.rb +47 -47
  90. data/tests/spec/psrp/message_fragmenter_spec.rb +105 -105
  91. data/tests/spec/psrp/powershell_output_decoder_spec.rb +100 -100
  92. data/tests/spec/psrp/psrp_message_spec.rb +70 -70
  93. data/tests/spec/psrp/recieve_response_reader_spec.rb +172 -172
  94. data/tests/spec/psrp/uuid_spec.rb +28 -28
  95. data/tests/spec/response_handler_spec.rb +61 -61
  96. data/tests/spec/shells/base_spec.rb +202 -202
  97. data/tests/spec/shells/cmd_spec.rb +75 -75
  98. data/tests/spec/shells/powershell_spec.rb +175 -175
  99. data/tests/spec/spec_helper.rb +47 -47
  100. data/tests/spec/stubs/clixml/error_record.xml.erb +84 -84
  101. data/tests/spec/stubs/clixml/pipeline_state.xml.erb +88 -88
  102. data/tests/spec/stubs/responses/get_command_output_response.xml.erb +13 -13
  103. data/tests/spec/stubs/responses/get_command_output_response_not_done.xml.erb +10 -10
  104. data/tests/spec/stubs/responses/get_powershell_keepalive_response.xml.erb +10 -10
  105. data/tests/spec/stubs/responses/get_powershell_output_response.xml.erb +12 -12
  106. data/tests/spec/stubs/responses/get_powershell_output_response_not_done.xml.erb +9 -9
  107. data/tests/spec/stubs/responses/open_shell_v1.xml +19 -19
  108. data/tests/spec/stubs/responses/open_shell_v2.xml +20 -20
  109. data/tests/spec/stubs/responses/soap_fault_v1.xml +36 -36
  110. data/tests/spec/stubs/responses/soap_fault_v2.xml +42 -42
  111. data/tests/spec/stubs/responses/wmi_error_v2.xml +41 -41
  112. data/tests/spec/wsmv/cleanup_command_spec.rb +22 -22
  113. data/tests/spec/wsmv/close_shell_spec.rb +17 -17
  114. data/tests/spec/wsmv/command_output_decoder_spec.rb +37 -37
  115. data/tests/spec/wsmv/command_output_spec.rb +45 -45
  116. data/tests/spec/wsmv/command_spec.rb +19 -19
  117. data/tests/spec/wsmv/configuration_spec.rb +17 -17
  118. data/tests/spec/wsmv/create_pipeline_spec.rb +31 -31
  119. data/tests/spec/wsmv/create_shell_spec.rb +38 -38
  120. data/tests/spec/wsmv/init_runspace_pool_spec.rb +36 -36
  121. data/tests/spec/wsmv/keep_alive_spec.rb +21 -21
  122. data/tests/spec/wsmv/receive_response_reader_spec.rb +123 -123
  123. data/tests/spec/wsmv/send_data_spec.rb +30 -30
  124. data/tests/spec/wsmv/wql_query_spec.rb +13 -13
  125. data/tests/spec/wsmv/write_stdin_spec.rb +22 -22
  126. data/winrm.gemspec +42 -42
  127. metadata +2 -2
@@ -1,96 +1,96 @@
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
- require_relative '../wsmv/soap'
19
-
20
- module WinRM
21
- # Handles the raw WinRM HTTP response. Returns the body as an XML doc
22
- # or raises the appropriate WinRM error if the response is an error.
23
- class ResponseHandler
24
- # @param [String] The raw unparsed response body, if any
25
- # @param [Integer] The HTTP response status code
26
- def initialize(response_body, status_code)
27
- @response_body = response_body
28
- @status_code = status_code
29
- end
30
-
31
- # Processes the response from the WinRM service and either returns an XML
32
- # doc or raises an appropriate error.
33
- #
34
- # @returns [REXML::Document] The parsed response body
35
- def parse_to_xml
36
- raise_if_error
37
- response_xml
38
- end
39
-
40
- private
41
-
42
- def response_xml
43
- @response_xml ||= REXML::Document.new(@response_body)
44
- rescue REXML::ParseException => e
45
- raise WinRMHTTPTransportError.new(
46
- "Unable to parse WinRM response: #{e.message}", @status_code)
47
- end
48
-
49
- def raise_if_error
50
- return if @status_code == 200
51
- raise_if_auth_error
52
- raise_if_wsman_fault
53
- raise_if_wmi_error
54
- raise_transport_error
55
- end
56
-
57
- def raise_if_auth_error
58
- raise WinRMAuthorizationError if @status_code == 401
59
- end
60
-
61
- def raise_if_wsman_fault
62
- soap_errors = REXML::XPath.match(
63
- response_xml,
64
- "//#{WinRM::WSMV::SOAP::NS_SOAP_ENV}:Body/#{WinRM::WSMV::SOAP::NS_SOAP_ENV}:Fault/*")
65
- return if soap_errors.empty?
66
- fault = REXML::XPath.first(
67
- soap_errors,
68
- "//#{WinRM::WSMV::SOAP::NS_WSMAN_FAULT}:WSManFault")
69
- raise WinRMWSManFault.new(fault.to_s, fault.attributes['Code']) unless fault.nil?
70
- end
71
-
72
- def raise_if_wmi_error
73
- soap_errors = REXML::XPath.match(
74
- response_xml,
75
- "//#{WinRM::WSMV::SOAP::NS_SOAP_ENV}:Body/#{WinRM::WSMV::SOAP::NS_SOAP_ENV}:Fault/*")
76
- return if soap_errors.empty?
77
-
78
- error = REXML::XPath.first(
79
- soap_errors,
80
- "//#{WinRM::WSMV::SOAP::NS_WSMAN_MSFT}:MSFT_WmiError")
81
- return if error.nil?
82
-
83
- error_code = REXML::XPath.first(
84
- error,
85
- "//#{WinRM::WSMV::SOAP::NS_WSMAN_MSFT}:error_Code").text
86
- raise WinRMWMIError.new(error.to_s, error_code)
87
- end
88
-
89
- def raise_transport_error
90
- raise WinRMHTTPTransportError.new(
91
- "Bad HTTP response returned from server. Body(if present):#{@response_body}",
92
- @status_code
93
- )
94
- end
95
- end
96
- 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
+ require_relative '../wsmv/soap'
19
+
20
+ module WinRM
21
+ # Handles the raw WinRM HTTP response. Returns the body as an XML doc
22
+ # or raises the appropriate WinRM error if the response is an error.
23
+ class ResponseHandler
24
+ # @param [String] The raw unparsed response body, if any
25
+ # @param [Integer] The HTTP response status code
26
+ def initialize(response_body, status_code)
27
+ @response_body = response_body
28
+ @status_code = status_code
29
+ end
30
+
31
+ # Processes the response from the WinRM service and either returns an XML
32
+ # doc or raises an appropriate error.
33
+ #
34
+ # @returns [REXML::Document] The parsed response body
35
+ def parse_to_xml
36
+ raise_if_error
37
+ response_xml
38
+ end
39
+
40
+ private
41
+
42
+ def response_xml
43
+ @response_xml ||= REXML::Document.new(@response_body)
44
+ rescue REXML::ParseException => e
45
+ raise WinRMHTTPTransportError.new(
46
+ "Unable to parse WinRM response: #{e.message}", @status_code)
47
+ end
48
+
49
+ def raise_if_error
50
+ return if @status_code == 200
51
+ raise_if_auth_error
52
+ raise_if_wsman_fault
53
+ raise_if_wmi_error
54
+ raise_transport_error
55
+ end
56
+
57
+ def raise_if_auth_error
58
+ raise WinRMAuthorizationError if @status_code == 401
59
+ end
60
+
61
+ def raise_if_wsman_fault
62
+ soap_errors = REXML::XPath.match(
63
+ response_xml,
64
+ "//#{WinRM::WSMV::SOAP::NS_SOAP_ENV}:Body/#{WinRM::WSMV::SOAP::NS_SOAP_ENV}:Fault/*")
65
+ return if soap_errors.empty?
66
+ fault = REXML::XPath.first(
67
+ soap_errors,
68
+ "//#{WinRM::WSMV::SOAP::NS_WSMAN_FAULT}:WSManFault")
69
+ raise WinRMWSManFault.new(fault.to_s, fault.attributes['Code']) unless fault.nil?
70
+ end
71
+
72
+ def raise_if_wmi_error
73
+ soap_errors = REXML::XPath.match(
74
+ response_xml,
75
+ "//#{WinRM::WSMV::SOAP::NS_SOAP_ENV}:Body/#{WinRM::WSMV::SOAP::NS_SOAP_ENV}:Fault/*")
76
+ return if soap_errors.empty?
77
+
78
+ error = REXML::XPath.first(
79
+ soap_errors,
80
+ "//#{WinRM::WSMV::SOAP::NS_WSMAN_MSFT}:MSFT_WmiError")
81
+ return if error.nil?
82
+
83
+ error_code = REXML::XPath.first(
84
+ error,
85
+ "//#{WinRM::WSMV::SOAP::NS_WSMAN_MSFT}:error_Code").text
86
+ raise WinRMWMIError.new(error.to_s, error_code)
87
+ end
88
+
89
+ def raise_transport_error
90
+ raise WinRMHTTPTransportError.new(
91
+ "Bad HTTP response returned from server. Body(if present):#{@response_body}",
92
+ @status_code
93
+ )
94
+ end
95
+ end
96
+ end
@@ -1,424 +1,424 @@
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 'httpclient'
18
- require_relative 'response_handler'
19
-
20
- module WinRM
21
- module HTTP
22
- # A generic HTTP transport that utilized HTTPClient to send messages back and forth.
23
- # This backend will maintain state for every WinRMWebService instance that is instantiated so it
24
- # is possible to use GSSAPI with Keep-Alive.
25
- class HttpTransport
26
- attr_reader :endpoint
27
-
28
- def initialize(endpoint, options)
29
- @endpoint = endpoint.is_a?(String) ? URI.parse(endpoint) : endpoint
30
- @httpcli = HTTPClient.new(agent_name: 'Ruby WinRM Client')
31
- @logger = Logging.logger[self]
32
- @httpcli.receive_timeout = options[:receive_timeout]
33
- end
34
-
35
- # Sends the SOAP payload to the WinRM service and returns the service's
36
- # SOAP response. If an error occurrs an appropriate error is raised.
37
- #
38
- # @param [String] The XML SOAP message
39
- # @returns [REXML::Document] The parsed response body
40
- def send_request(message)
41
- ssl_peer_fingerprint_verification!
42
- log_soap_message(message)
43
- hdr = { 'Content-Type' => 'application/soap+xml;charset=UTF-8',
44
- 'Content-Length' => message.bytesize }
45
- # We need to add this header if using Client Certificate authentication
46
- unless @httpcli.ssl_config.client_cert.nil?
47
- hdr['Authorization'] = 'http://schemas.dmtf.org/wbem/wsman/1/wsman/secprofile/https/mutual'
48
- end
49
-
50
- resp = @httpcli.post(@endpoint, message, hdr)
51
- log_soap_message(resp.http_body.content)
52
- verify_ssl_fingerprint(resp.peer_cert)
53
- handler = WinRM::ResponseHandler.new(resp.http_body.content, resp.status)
54
- handler.parse_to_xml
55
- end
56
-
57
- # We'll need this to force basic authentication if desired
58
- def basic_auth_only!
59
- auths = @httpcli.www_auth.instance_variable_get('@authenticator')
60
- auths.delete_if { |i| i.scheme !~ /basic/i }
61
- end
62
-
63
- # Disable SSPI Auth
64
- def no_sspi_auth!
65
- auths = @httpcli.www_auth.instance_variable_get('@authenticator')
66
- auths.delete_if { |i| i.is_a? HTTPClient::SSPINegotiateAuth }
67
- end
68
-
69
- # Disable SSL Peer Verification
70
- def no_ssl_peer_verification!
71
- @httpcli.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE
72
- end
73
-
74
- # SSL Peer Fingerprint Verification prior to connecting
75
- def ssl_peer_fingerprint_verification!
76
- return unless @ssl_peer_fingerprint && ! @ssl_peer_fingerprint_verified
77
-
78
- with_untrusted_ssl_connection do |connection|
79
- connection_cert = connection.peer_cert_chain.last
80
- verify_ssl_fingerprint(connection_cert)
81
- end
82
- @logger.info("initial ssl fingerprint #{@ssl_peer_fingerprint} verified\n")
83
- @ssl_peer_fingerprint_verified = true
84
- no_ssl_peer_verification!
85
- end
86
-
87
- # Connect without verification to retrieve untrusted ssl context
88
- def with_untrusted_ssl_connection
89
- noverify_peer_context = OpenSSL::SSL::SSLContext.new
90
- noverify_peer_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
91
- tcp_connection = TCPSocket.new(@endpoint.host, @endpoint.port)
92
- begin
93
- ssl_connection = OpenSSL::SSL::SSLSocket.new(tcp_connection, noverify_peer_context)
94
- ssl_connection.connect
95
- yield ssl_connection
96
- ensure
97
- tcp_connection.close
98
- end
99
- end
100
-
101
- # compare @ssl_peer_fingerprint to current ssl context
102
- def verify_ssl_fingerprint(cert)
103
- return unless @ssl_peer_fingerprint
104
- conn_fingerprint = OpenSSL::Digest::SHA1.new(cert.to_der).to_s
105
- return unless @ssl_peer_fingerprint.casecmp(conn_fingerprint) != 0
106
- raise "ssl fingerprint mismatch!!!!\n"
107
- end
108
-
109
- protected
110
-
111
- def body(message, length, type = 'application/HTTP-SPNEGO-session-encrypted')
112
- [
113
- '--Encrypted Boundary',
114
- "Content-Type: #{type}",
115
- "OriginalContent: type=application/soap+xml;charset=UTF-8;Length=#{length}",
116
- '--Encrypted Boundary',
117
- 'Content-Type: application/octet-stream',
118
- "#{message}--Encrypted Boundary--"
119
- ].join("\r\n").concat("\r\n")
120
- end
121
-
122
- def log_soap_message(message)
123
- return unless @logger.debug?
124
-
125
- xml_msg = REXML::Document.new(message)
126
- formatter = REXML::Formatters::Pretty.new(2)
127
- formatter.compact = true
128
- formatter.write(xml_msg, @logger)
129
- @logger.debug("\n")
130
- rescue StandardError => e
131
- @logger.debug("Couldn't log SOAP request/response: #{e.message} - #{message}")
132
- end
133
- end
134
-
135
- # Plain text, insecure, HTTP transport
136
- class HttpPlaintext < HttpTransport
137
- def initialize(endpoint, user, pass, opts)
138
- super(endpoint, opts)
139
- @httpcli.set_auth(nil, user, pass)
140
- no_sspi_auth! if opts[:disable_sspi]
141
- basic_auth_only! if opts[:basic_auth_only]
142
- no_ssl_peer_verification! if opts[:no_ssl_peer_verification]
143
- end
144
- end
145
-
146
- # NTLM/Negotiate, secure, HTTP transport
147
- class HttpNegotiate < HttpTransport
148
- def initialize(endpoint, user, pass, opts)
149
- super(endpoint, opts)
150
- require 'rubyntlm'
151
- no_sspi_auth!
152
-
153
- user_parts = user.split('\\')
154
- if user_parts.length > 1
155
- opts[:domain] = user_parts[0]
156
- user = user_parts[1]
157
- end
158
-
159
- @ntlmcli = Net::NTLM::Client.new(user, pass, opts)
160
- @retryable = true
161
- no_ssl_peer_verification! if opts[:no_ssl_peer_verification]
162
- @ssl_peer_fingerprint = opts[:ssl_peer_fingerprint]
163
- @httpcli.ssl_config.set_trust_ca(opts[:ca_trust_path]) if opts[:ca_trust_path]
164
- end
165
-
166
- def send_request(message, auth_header = nil)
167
- ssl_peer_fingerprint_verification!
168
- auth_header = init_auth if @ntlmcli.session.nil?
169
- log_soap_message(message)
170
-
171
- hdr = {
172
- 'Content-Type' => 'multipart/encrypted;'\
173
- 'protocol="application/HTTP-SPNEGO-session-encrypted";boundary="Encrypted Boundary"'
174
- }
175
- hdr.merge!(auth_header) if auth_header
176
-
177
- resp = @httpcli.post(@endpoint, body(seal(message), message.bytesize), hdr)
178
- verify_ssl_fingerprint(resp.peer_cert)
179
- if resp.status == 401 && @retryable
180
- @retryable = false
181
- send_request(message, init_auth)
182
- else
183
- @retryable = true
184
- decrypted_body = winrm_decrypt(resp.body)
185
- log_soap_message(decrypted_body)
186
- WinRM::ResponseHandler.new(decrypted_body, resp.status).parse_to_xml
187
- end
188
- end
189
-
190
- private
191
-
192
- def seal(message)
193
- emessage = @ntlmcli.session.seal_message message
194
- signature = @ntlmcli.session.sign_message message
195
- "\x10\x00\x00\x00#{signature}#{emessage}"
196
- end
197
-
198
- def winrm_decrypt(str)
199
- return '' if str.empty?
200
- str.force_encoding('BINARY')
201
- str.sub!(%r{^.*Content-Type: application\/octet-stream\r\n(.*)--Encrypted.*$}m, '\1')
202
-
203
- signature = str[4..19]
204
- message = @ntlmcli.session.unseal_message str[20..-1]
205
- if @ntlmcli.session.verify_signature(signature, message)
206
- message
207
- else
208
- raise WinRMHTTPTransportError, 'Could not decrypt NTLM message.'
209
- end
210
- end
211
-
212
- def init_auth
213
- @logger.debug "Initializing Negotiate for #{@endpoint}"
214
- auth1 = @ntlmcli.init_context
215
- hdr = {
216
- 'Authorization' => "Negotiate #{auth1.encode64}",
217
- 'Content-Type' => 'application/soap+xml;charset=UTF-8'
218
- }
219
- @logger.debug 'Sending HTTP POST for Negotiate Authentication'
220
- r = @httpcli.post(@endpoint, '', hdr)
221
- verify_ssl_fingerprint(r.peer_cert)
222
- auth_header = r.header['WWW-Authenticate'].pop
223
- unless auth_header
224
- msg = "Unable to parse authorization header. Headers: #{r.headers}\r\nBody: #{r.body}"
225
- raise WinRMHTTPTransportError.new(msg, r.status_code)
226
- end
227
- itok = auth_header.split.last
228
- binding = r.peer_cert.nil? ? nil : Net::NTLM::ChannelBinding.create(r.peer_cert)
229
- auth3 = @ntlmcli.init_context(itok, binding)
230
- { 'Authorization' => "Negotiate #{auth3.encode64}" }
231
- end
232
- end
233
-
234
- # Uses SSL to secure the transport
235
- class BasicAuthSSL < HttpTransport
236
- def initialize(endpoint, user, pass, opts)
237
- super(endpoint, opts)
238
- @httpcli.set_auth(endpoint, user, pass)
239
- basic_auth_only!
240
- no_ssl_peer_verification! if opts[:no_ssl_peer_verification]
241
- @ssl_peer_fingerprint = opts[:ssl_peer_fingerprint]
242
- @httpcli.ssl_config.set_trust_ca(opts[:ca_trust_path]) if opts[:ca_trust_path]
243
- end
244
- end
245
-
246
- # Uses Client Certificate to authenticate and SSL to secure the transport
247
- class ClientCertAuthSSL < HttpTransport
248
- def initialize(endpoint, client_cert, client_key, key_pass, opts)
249
- super(endpoint, opts)
250
- @httpcli.ssl_config.set_client_cert_file(client_cert, client_key, key_pass)
251
- @httpcli.www_auth.instance_variable_set('@authenticator', [])
252
- no_ssl_peer_verification! if opts[:no_ssl_peer_verification]
253
- @ssl_peer_fingerprint = opts[:ssl_peer_fingerprint]
254
- @httpcli.ssl_config.set_trust_ca(opts[:ca_trust_path]) if opts[:ca_trust_path]
255
- end
256
- end
257
-
258
- # Uses Kerberos/GSSAPI to authenticate and encrypt messages
259
- class HttpGSSAPI < HttpTransport
260
- # @param [String,URI] endpoint the WinRM webservice endpoint
261
- # @param [String] realm the Kerberos realm we are authenticating to
262
- # @param [String<optional>] service the service name, default is HTTP
263
- def initialize(endpoint, realm, opts, service = nil)
264
- require 'gssapi'
265
- require 'gssapi/extensions'
266
-
267
- super(endpoint, opts)
268
- # Remove the GSSAPI auth from HTTPClient because we are doing our own thing
269
- no_sspi_auth!
270
- service ||= 'HTTP'
271
- @service = "#{service}/#{@endpoint.host}@#{realm}"
272
- init_krb
273
- end
274
-
275
- # Sends the SOAP payload to the WinRM service and returns the service's
276
- # SOAP response. If an error occurrs an appropriate error is raised.
277
- #
278
- # @param [String] The XML SOAP message
279
- # @returns [REXML::Document] The parsed response body
280
- def send_request(message)
281
- resp = send_kerberos_request(message)
282
-
283
- if resp.status == 401
284
- @logger.debug 'Got 401 - reinitializing Kerberos and retrying one more time'
285
- init_krb
286
- resp = send_kerberos_request(message)
287
- end
288
-
289
- handler = WinRM::ResponseHandler.new(winrm_decrypt(resp.http_body.content), resp.status)
290
- handler.parse_to_xml
291
- end
292
-
293
- private
294
-
295
- # Sends the SOAP payload to the WinRM service and returns the service's
296
- # HTTP response.
297
- #
298
- # @param [String] The XML SOAP message
299
- # @returns [Object] The HTTP response object
300
- def send_kerberos_request(message)
301
- log_soap_message(message)
302
- original_length = message.bytesize
303
- pad_len, emsg = winrm_encrypt(message)
304
- req_length = original_length + pad_len
305
- hdr = {
306
- 'Connection' => 'Keep-Alive',
307
- 'Content-Type' => 'multipart/encrypted;' \
308
- 'protocol="application/HTTP-Kerberos-session-encrypted";boundary="Encrypted Boundary"'
309
- }
310
-
311
- resp = @httpcli.post(
312
- @endpoint,
313
- body(emsg, req_length, 'application/HTTP-Kerberos-session-encrypted'),
314
- hdr
315
- )
316
- log_soap_message(resp.http_body.content)
317
- resp
318
- end
319
-
320
- def init_krb
321
- @logger.debug "Initializing Kerberos for #{@service}"
322
- @gsscli = GSSAPI::Simple.new(@endpoint.host, @service)
323
- token = @gsscli.init_context
324
- auth = Base64.strict_encode64 token
325
-
326
- hdr = {
327
- 'Authorization' => "Kerberos #{auth}",
328
- 'Connection' => 'Keep-Alive',
329
- 'Content-Type' => 'application/soap+xml;charset=UTF-8'
330
- }
331
- @logger.debug 'Sending HTTP POST for Kerberos Authentication'
332
- r = @httpcli.post(@endpoint, '', hdr)
333
- itok = r.header['WWW-Authenticate'].pop
334
- itok = itok.split.last
335
- itok = Base64.strict_decode64(itok)
336
- @gsscli.init_context(itok)
337
- end
338
-
339
- # @return [String] the encrypted request string
340
- def winrm_encrypt(str)
341
- @logger.debug "Encrypting SOAP message:\n#{str}"
342
- iov = iov_pointer
343
-
344
- iov0 = create_iov(iov.address, 0, :header)[:buffer]
345
- iov1 = create_iov(iov.address, 1, :data, str)[:buffer]
346
- iov2 = create_iov(iov.address, 2, :padding)[:buffer]
347
-
348
- gss_wrap(iov)
349
-
350
- token = [iov0.length].pack('L')
351
- token += iov0.value
352
- token += iov1.value
353
- pad_len = iov2.length
354
- token += iov2.value if pad_len > 0
355
- [pad_len, token]
356
- end
357
-
358
- # @return [String] the unencrypted response string
359
- def winrm_decrypt(str)
360
- @logger.debug "Decrypting SOAP message:\n#{str}"
361
-
362
- str.force_encoding('BINARY')
363
- str.sub!(%r{^.*Content-Type: application\/octet-stream\r\n(.*)--Encrypted.*$}m, '\1')
364
- iov_data = str.unpack("LA#{str.unpack('L').first}A*")
365
-
366
- iov = iov_pointer
367
-
368
- create_iov(iov.address, 0, :header, iov_data[1])
369
- ret = create_iov(iov.address, 1, :data, iov_data[2])[:buffer].value
370
- create_iov(iov.address, 2, :data)
371
-
372
- maj_stat = gss_unwrap(iov)
373
-
374
- @logger.debug "SOAP message decrypted (MAJ: #{maj_stat}, " \
375
- "MIN: #{min_stat.read_int}):\n#{ret}"
376
-
377
- ret
378
- end
379
-
380
- def iov_pointer
381
- FFI::MemoryPointer.new(GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * 3)
382
- end
383
-
384
- def gss_unwrap(iov)
385
- min_stat = FFI::MemoryPointer.new :uint32
386
- conf_state = FFI::MemoryPointer.new :uint32
387
- conf_state.write_int(1)
388
- qop_state = FFI::MemoryPointer.new :uint32
389
- qop_state.write_int(0)
390
-
391
- GSSAPI::LibGSSAPI.gss_unwrap_iov(
392
- min_stat, @gsscli.context, conf_state, qop_state, iov, 3)
393
- end
394
-
395
- def gss_wrap(iov)
396
- GSSAPI::LibGSSAPI.gss_wrap_iov(
397
- FFI::MemoryPointer.new(:uint32),
398
- @gsscli.context,
399
- 1,
400
- GSSAPI::LibGSSAPI::GSS_C_QOP_DEFAULT,
401
- FFI::MemoryPointer.new(:uint32),
402
- iov,
403
- 3)
404
- end
405
-
406
- def create_iov(address, offset, type, buffer = nil)
407
- iov = GSSAPI::LibGSSAPI::GssIOVBufferDesc.new(
408
- FFI::Pointer.new(address + (GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * offset)))
409
- case type
410
- when :data
411
- iov[:type] = GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_DATA
412
- when :header
413
- iov[:type] = (GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_HEADER | \
414
- GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_FLAG_ALLOCATE)
415
- when :padding
416
- iov[:type] = (GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_PADDING | \
417
- GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_FLAG_ALLOCATE)
418
- end
419
- iov[:buffer].value = buffer if buffer
420
- iov
421
- end
422
- end
423
- end
424
- 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 'httpclient'
18
+ require_relative 'response_handler'
19
+
20
+ module WinRM
21
+ module HTTP
22
+ # A generic HTTP transport that utilized HTTPClient to send messages back and forth.
23
+ # This backend will maintain state for every WinRMWebService instance that is instantiated so it
24
+ # is possible to use GSSAPI with Keep-Alive.
25
+ class HttpTransport
26
+ attr_reader :endpoint
27
+
28
+ def initialize(endpoint, options)
29
+ @endpoint = endpoint.is_a?(String) ? URI.parse(endpoint) : endpoint
30
+ @httpcli = HTTPClient.new(agent_name: 'Ruby WinRM Client')
31
+ @logger = Logging.logger[self]
32
+ @httpcli.receive_timeout = options[:receive_timeout]
33
+ end
34
+
35
+ # Sends the SOAP payload to the WinRM service and returns the service's
36
+ # SOAP response. If an error occurrs an appropriate error is raised.
37
+ #
38
+ # @param [String] The XML SOAP message
39
+ # @returns [REXML::Document] The parsed response body
40
+ def send_request(message)
41
+ ssl_peer_fingerprint_verification!
42
+ log_soap_message(message)
43
+ hdr = { 'Content-Type' => 'application/soap+xml;charset=UTF-8',
44
+ 'Content-Length' => message.bytesize }
45
+ # We need to add this header if using Client Certificate authentication
46
+ unless @httpcli.ssl_config.client_cert.nil?
47
+ hdr['Authorization'] = 'http://schemas.dmtf.org/wbem/wsman/1/wsman/secprofile/https/mutual'
48
+ end
49
+
50
+ resp = @httpcli.post(@endpoint, message, hdr)
51
+ log_soap_message(resp.http_body.content)
52
+ verify_ssl_fingerprint(resp.peer_cert)
53
+ handler = WinRM::ResponseHandler.new(resp.http_body.content, resp.status)
54
+ handler.parse_to_xml
55
+ end
56
+
57
+ # We'll need this to force basic authentication if desired
58
+ def basic_auth_only!
59
+ auths = @httpcli.www_auth.instance_variable_get('@authenticator')
60
+ auths.delete_if { |i| i.scheme !~ /basic/i }
61
+ end
62
+
63
+ # Disable SSPI Auth
64
+ def no_sspi_auth!
65
+ auths = @httpcli.www_auth.instance_variable_get('@authenticator')
66
+ auths.delete_if { |i| i.is_a? HTTPClient::SSPINegotiateAuth }
67
+ end
68
+
69
+ # Disable SSL Peer Verification
70
+ def no_ssl_peer_verification!
71
+ @httpcli.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE
72
+ end
73
+
74
+ # SSL Peer Fingerprint Verification prior to connecting
75
+ def ssl_peer_fingerprint_verification!
76
+ return unless @ssl_peer_fingerprint && ! @ssl_peer_fingerprint_verified
77
+
78
+ with_untrusted_ssl_connection do |connection|
79
+ connection_cert = connection.peer_cert_chain.last
80
+ verify_ssl_fingerprint(connection_cert)
81
+ end
82
+ @logger.info("initial ssl fingerprint #{@ssl_peer_fingerprint} verified\n")
83
+ @ssl_peer_fingerprint_verified = true
84
+ no_ssl_peer_verification!
85
+ end
86
+
87
+ # Connect without verification to retrieve untrusted ssl context
88
+ def with_untrusted_ssl_connection
89
+ noverify_peer_context = OpenSSL::SSL::SSLContext.new
90
+ noverify_peer_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
91
+ tcp_connection = TCPSocket.new(@endpoint.host, @endpoint.port)
92
+ begin
93
+ ssl_connection = OpenSSL::SSL::SSLSocket.new(tcp_connection, noverify_peer_context)
94
+ ssl_connection.connect
95
+ yield ssl_connection
96
+ ensure
97
+ tcp_connection.close
98
+ end
99
+ end
100
+
101
+ # compare @ssl_peer_fingerprint to current ssl context
102
+ def verify_ssl_fingerprint(cert)
103
+ return unless @ssl_peer_fingerprint
104
+ conn_fingerprint = OpenSSL::Digest::SHA1.new(cert.to_der).to_s
105
+ return unless @ssl_peer_fingerprint.casecmp(conn_fingerprint) != 0
106
+ raise "ssl fingerprint mismatch!!!!\n"
107
+ end
108
+
109
+ protected
110
+
111
+ def body(message, length, type = 'application/HTTP-SPNEGO-session-encrypted')
112
+ [
113
+ '--Encrypted Boundary',
114
+ "Content-Type: #{type}",
115
+ "OriginalContent: type=application/soap+xml;charset=UTF-8;Length=#{length}",
116
+ '--Encrypted Boundary',
117
+ 'Content-Type: application/octet-stream',
118
+ "#{message}--Encrypted Boundary--"
119
+ ].join("\r\n").concat("\r\n")
120
+ end
121
+
122
+ def log_soap_message(message)
123
+ return unless @logger.debug?
124
+
125
+ xml_msg = REXML::Document.new(message)
126
+ formatter = REXML::Formatters::Pretty.new(2)
127
+ formatter.compact = true
128
+ formatter.write(xml_msg, @logger)
129
+ @logger.debug("\n")
130
+ rescue StandardError => e
131
+ @logger.debug("Couldn't log SOAP request/response: #{e.message} - #{message}")
132
+ end
133
+ end
134
+
135
+ # Plain text, insecure, HTTP transport
136
+ class HttpPlaintext < HttpTransport
137
+ def initialize(endpoint, user, pass, opts)
138
+ super(endpoint, opts)
139
+ @httpcli.set_auth(nil, user, pass)
140
+ no_sspi_auth! if opts[:disable_sspi]
141
+ basic_auth_only! if opts[:basic_auth_only]
142
+ no_ssl_peer_verification! if opts[:no_ssl_peer_verification]
143
+ end
144
+ end
145
+
146
+ # NTLM/Negotiate, secure, HTTP transport
147
+ class HttpNegotiate < HttpTransport
148
+ def initialize(endpoint, user, pass, opts)
149
+ super(endpoint, opts)
150
+ require 'rubyntlm'
151
+ no_sspi_auth!
152
+
153
+ user_parts = user.split('\\')
154
+ if user_parts.length > 1
155
+ opts[:domain] = user_parts[0]
156
+ user = user_parts[1]
157
+ end
158
+
159
+ @ntlmcli = Net::NTLM::Client.new(user, pass, opts)
160
+ @retryable = true
161
+ no_ssl_peer_verification! if opts[:no_ssl_peer_verification]
162
+ @ssl_peer_fingerprint = opts[:ssl_peer_fingerprint]
163
+ @httpcli.ssl_config.set_trust_ca(opts[:ca_trust_path]) if opts[:ca_trust_path]
164
+ end
165
+
166
+ def send_request(message, auth_header = nil)
167
+ ssl_peer_fingerprint_verification!
168
+ auth_header = init_auth if @ntlmcli.session.nil?
169
+ log_soap_message(message)
170
+
171
+ hdr = {
172
+ 'Content-Type' => 'multipart/encrypted;'\
173
+ 'protocol="application/HTTP-SPNEGO-session-encrypted";boundary="Encrypted Boundary"'
174
+ }
175
+ hdr.merge!(auth_header) if auth_header
176
+
177
+ resp = @httpcli.post(@endpoint, body(seal(message), message.bytesize), hdr)
178
+ verify_ssl_fingerprint(resp.peer_cert)
179
+ if resp.status == 401 && @retryable
180
+ @retryable = false
181
+ send_request(message, init_auth)
182
+ else
183
+ @retryable = true
184
+ decrypted_body = winrm_decrypt(resp.body)
185
+ log_soap_message(decrypted_body)
186
+ WinRM::ResponseHandler.new(decrypted_body, resp.status).parse_to_xml
187
+ end
188
+ end
189
+
190
+ private
191
+
192
+ def seal(message)
193
+ emessage = @ntlmcli.session.seal_message message
194
+ signature = @ntlmcli.session.sign_message message
195
+ "\x10\x00\x00\x00#{signature}#{emessage}"
196
+ end
197
+
198
+ def winrm_decrypt(str)
199
+ return '' if str.empty?
200
+ str.force_encoding('BINARY')
201
+ str.sub!(%r{^.*Content-Type: application\/octet-stream\r\n(.*)--Encrypted.*$}m, '\1')
202
+
203
+ signature = str[4..19]
204
+ message = @ntlmcli.session.unseal_message str[20..-1]
205
+ if @ntlmcli.session.verify_signature(signature, message)
206
+ message
207
+ else
208
+ raise WinRMHTTPTransportError, 'Could not decrypt NTLM message.'
209
+ end
210
+ end
211
+
212
+ def init_auth
213
+ @logger.debug "Initializing Negotiate for #{@endpoint}"
214
+ auth1 = @ntlmcli.init_context
215
+ hdr = {
216
+ 'Authorization' => "Negotiate #{auth1.encode64}",
217
+ 'Content-Type' => 'application/soap+xml;charset=UTF-8'
218
+ }
219
+ @logger.debug 'Sending HTTP POST for Negotiate Authentication'
220
+ r = @httpcli.post(@endpoint, '', hdr)
221
+ verify_ssl_fingerprint(r.peer_cert)
222
+ auth_header = r.header['WWW-Authenticate'].pop
223
+ unless auth_header
224
+ msg = "Unable to parse authorization header. Headers: #{r.headers}\r\nBody: #{r.body}"
225
+ raise WinRMHTTPTransportError.new(msg, r.status_code)
226
+ end
227
+ itok = auth_header.split.last
228
+ binding = r.peer_cert.nil? ? nil : Net::NTLM::ChannelBinding.create(r.peer_cert)
229
+ auth3 = @ntlmcli.init_context(itok, binding)
230
+ { 'Authorization' => "Negotiate #{auth3.encode64}" }
231
+ end
232
+ end
233
+
234
+ # Uses SSL to secure the transport
235
+ class BasicAuthSSL < HttpTransport
236
+ def initialize(endpoint, user, pass, opts)
237
+ super(endpoint, opts)
238
+ @httpcli.set_auth(endpoint, user, pass)
239
+ basic_auth_only!
240
+ no_ssl_peer_verification! if opts[:no_ssl_peer_verification]
241
+ @ssl_peer_fingerprint = opts[:ssl_peer_fingerprint]
242
+ @httpcli.ssl_config.set_trust_ca(opts[:ca_trust_path]) if opts[:ca_trust_path]
243
+ end
244
+ end
245
+
246
+ # Uses Client Certificate to authenticate and SSL to secure the transport
247
+ class ClientCertAuthSSL < HttpTransport
248
+ def initialize(endpoint, client_cert, client_key, key_pass, opts)
249
+ super(endpoint, opts)
250
+ @httpcli.ssl_config.set_client_cert_file(client_cert, client_key, key_pass)
251
+ @httpcli.www_auth.instance_variable_set('@authenticator', [])
252
+ no_ssl_peer_verification! if opts[:no_ssl_peer_verification]
253
+ @ssl_peer_fingerprint = opts[:ssl_peer_fingerprint]
254
+ @httpcli.ssl_config.set_trust_ca(opts[:ca_trust_path]) if opts[:ca_trust_path]
255
+ end
256
+ end
257
+
258
+ # Uses Kerberos/GSSAPI to authenticate and encrypt messages
259
+ class HttpGSSAPI < HttpTransport
260
+ # @param [String,URI] endpoint the WinRM webservice endpoint
261
+ # @param [String] realm the Kerberos realm we are authenticating to
262
+ # @param [String<optional>] service the service name, default is HTTP
263
+ def initialize(endpoint, realm, opts, service = nil)
264
+ require 'gssapi'
265
+ require 'gssapi/extensions'
266
+
267
+ super(endpoint, opts)
268
+ # Remove the GSSAPI auth from HTTPClient because we are doing our own thing
269
+ no_sspi_auth!
270
+ service ||= 'HTTP'
271
+ @service = "#{service}/#{@endpoint.host}@#{realm}"
272
+ init_krb
273
+ end
274
+
275
+ # Sends the SOAP payload to the WinRM service and returns the service's
276
+ # SOAP response. If an error occurrs an appropriate error is raised.
277
+ #
278
+ # @param [String] The XML SOAP message
279
+ # @returns [REXML::Document] The parsed response body
280
+ def send_request(message)
281
+ resp = send_kerberos_request(message)
282
+
283
+ if resp.status == 401
284
+ @logger.debug 'Got 401 - reinitializing Kerberos and retrying one more time'
285
+ init_krb
286
+ resp = send_kerberos_request(message)
287
+ end
288
+
289
+ handler = WinRM::ResponseHandler.new(winrm_decrypt(resp.http_body.content), resp.status)
290
+ handler.parse_to_xml
291
+ end
292
+
293
+ private
294
+
295
+ # Sends the SOAP payload to the WinRM service and returns the service's
296
+ # HTTP response.
297
+ #
298
+ # @param [String] The XML SOAP message
299
+ # @returns [Object] The HTTP response object
300
+ def send_kerberos_request(message)
301
+ log_soap_message(message)
302
+ original_length = message.bytesize
303
+ pad_len, emsg = winrm_encrypt(message)
304
+ req_length = original_length + pad_len
305
+ hdr = {
306
+ 'Connection' => 'Keep-Alive',
307
+ 'Content-Type' => 'multipart/encrypted;' \
308
+ 'protocol="application/HTTP-Kerberos-session-encrypted";boundary="Encrypted Boundary"'
309
+ }
310
+
311
+ resp = @httpcli.post(
312
+ @endpoint,
313
+ body(emsg, req_length, 'application/HTTP-Kerberos-session-encrypted'),
314
+ hdr
315
+ )
316
+ log_soap_message(resp.http_body.content)
317
+ resp
318
+ end
319
+
320
+ def init_krb
321
+ @logger.debug "Initializing Kerberos for #{@service}"
322
+ @gsscli = GSSAPI::Simple.new(@endpoint.host, @service)
323
+ token = @gsscli.init_context
324
+ auth = Base64.strict_encode64 token
325
+
326
+ hdr = {
327
+ 'Authorization' => "Kerberos #{auth}",
328
+ 'Connection' => 'Keep-Alive',
329
+ 'Content-Type' => 'application/soap+xml;charset=UTF-8'
330
+ }
331
+ @logger.debug 'Sending HTTP POST for Kerberos Authentication'
332
+ r = @httpcli.post(@endpoint, '', hdr)
333
+ itok = r.header['WWW-Authenticate'].pop
334
+ itok = itok.split.last
335
+ itok = Base64.strict_decode64(itok)
336
+ @gsscli.init_context(itok)
337
+ end
338
+
339
+ # @return [String] the encrypted request string
340
+ def winrm_encrypt(str)
341
+ @logger.debug "Encrypting SOAP message:\n#{str}"
342
+ iov = iov_pointer
343
+
344
+ iov0 = create_iov(iov.address, 0, :header)[:buffer]
345
+ iov1 = create_iov(iov.address, 1, :data, str)[:buffer]
346
+ iov2 = create_iov(iov.address, 2, :padding)[:buffer]
347
+
348
+ gss_wrap(iov)
349
+
350
+ token = [iov0.length].pack('L')
351
+ token += iov0.value
352
+ token += iov1.value
353
+ pad_len = iov2.length
354
+ token += iov2.value if pad_len > 0
355
+ [pad_len, token]
356
+ end
357
+
358
+ # @return [String] the unencrypted response string
359
+ def winrm_decrypt(str)
360
+ @logger.debug "Decrypting SOAP message:\n#{str}"
361
+
362
+ str.force_encoding('BINARY')
363
+ str.sub!(%r{^.*Content-Type: application\/octet-stream\r\n(.*)--Encrypted.*$}m, '\1')
364
+ iov_data = str.unpack("LA#{str.unpack('L').first}A*")
365
+
366
+ iov = iov_pointer
367
+
368
+ create_iov(iov.address, 0, :header, iov_data[1])
369
+ ret = create_iov(iov.address, 1, :data, iov_data[2])[:buffer].value
370
+ create_iov(iov.address, 2, :data)
371
+
372
+ maj_stat = gss_unwrap(iov)
373
+
374
+ @logger.debug "SOAP message decrypted (MAJ: #{maj_stat}, " \
375
+ "MIN: #{min_stat.read_int}):\n#{ret}"
376
+
377
+ ret
378
+ end
379
+
380
+ def iov_pointer
381
+ FFI::MemoryPointer.new(GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * 3)
382
+ end
383
+
384
+ def gss_unwrap(iov)
385
+ min_stat = FFI::MemoryPointer.new :uint32
386
+ conf_state = FFI::MemoryPointer.new :uint32
387
+ conf_state.write_int(1)
388
+ qop_state = FFI::MemoryPointer.new :uint32
389
+ qop_state.write_int(0)
390
+
391
+ GSSAPI::LibGSSAPI.gss_unwrap_iov(
392
+ min_stat, @gsscli.context, conf_state, qop_state, iov, 3)
393
+ end
394
+
395
+ def gss_wrap(iov)
396
+ GSSAPI::LibGSSAPI.gss_wrap_iov(
397
+ FFI::MemoryPointer.new(:uint32),
398
+ @gsscli.context,
399
+ 1,
400
+ GSSAPI::LibGSSAPI::GSS_C_QOP_DEFAULT,
401
+ FFI::MemoryPointer.new(:uint32),
402
+ iov,
403
+ 3)
404
+ end
405
+
406
+ def create_iov(address, offset, type, buffer = nil)
407
+ iov = GSSAPI::LibGSSAPI::GssIOVBufferDesc.new(
408
+ FFI::Pointer.new(address + (GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * offset)))
409
+ case type
410
+ when :data
411
+ iov[:type] = GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_DATA
412
+ when :header
413
+ iov[:type] = (GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_HEADER | \
414
+ GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_FLAG_ALLOCATE)
415
+ when :padding
416
+ iov[:type] = (GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_PADDING | \
417
+ GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_FLAG_ALLOCATE)
418
+ end
419
+ iov[:buffer].value = buffer if buffer
420
+ iov
421
+ end
422
+ end
423
+ end
424
+ end # WinRM