winrm 2.0.3 → 2.1.0

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