winrm 2.2.3 → 2.3.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 (116) hide show
  1. checksums.yaml +5 -5
  2. data/.rubocop.yml +13 -1
  3. data/.travis.yml +10 -11
  4. data/Gemfile +2 -3
  5. data/README.md +1 -1
  6. data/Rakefile +3 -4
  7. data/appveyor.yml +1 -2
  8. data/bin/rwinrm +90 -97
  9. data/changelog.md +5 -0
  10. data/lib/winrm.rb +3 -5
  11. data/lib/winrm/connection.rb +84 -86
  12. data/lib/winrm/connection_opts.rb +90 -91
  13. data/lib/winrm/exceptions.rb +14 -2
  14. data/lib/winrm/http/response_handler.rb +127 -96
  15. data/lib/winrm/http/transport.rb +462 -427
  16. data/lib/winrm/http/transport_factory.rb +1 -5
  17. data/lib/winrm/output.rb +1 -2
  18. data/lib/winrm/psrp/fragment.rb +0 -2
  19. data/lib/winrm/psrp/message.rb +1 -3
  20. data/lib/winrm/psrp/message_data.rb +0 -2
  21. data/lib/winrm/psrp/message_data/base.rb +0 -2
  22. data/lib/winrm/psrp/message_data/error_record.rb +0 -2
  23. data/lib/winrm/psrp/message_data/pipeline_host_call.rb +0 -2
  24. data/lib/winrm/psrp/message_data/pipeline_output.rb +48 -54
  25. data/lib/winrm/psrp/message_data/pipeline_state.rb +0 -2
  26. data/lib/winrm/psrp/message_data/runspacepool_host_call.rb +0 -2
  27. data/lib/winrm/psrp/message_data/runspacepool_state.rb +0 -2
  28. data/lib/winrm/psrp/message_data/session_capability.rb +0 -2
  29. data/lib/winrm/psrp/message_defragmenter.rb +2 -2
  30. data/lib/winrm/psrp/message_factory.rb +2 -3
  31. data/lib/winrm/psrp/message_fragmenter.rb +1 -3
  32. data/lib/winrm/psrp/powershell_output_decoder.rb +0 -2
  33. data/lib/winrm/psrp/receive_response_reader.rb +3 -5
  34. data/lib/winrm/psrp/uuid.rb +1 -2
  35. data/lib/winrm/shells/base.rb +6 -4
  36. data/lib/winrm/shells/cmd.rb +63 -65
  37. data/lib/winrm/shells/power_shell.rb +207 -202
  38. data/lib/winrm/shells/retryable.rb +44 -45
  39. data/lib/winrm/shells/shell_factory.rb +0 -2
  40. data/lib/winrm/version.rb +1 -3
  41. data/lib/winrm/wsmv/base.rb +0 -2
  42. data/lib/winrm/wsmv/cleanup_command.rb +1 -2
  43. data/lib/winrm/wsmv/close_shell.rb +1 -2
  44. data/lib/winrm/wsmv/command.rb +2 -3
  45. data/lib/winrm/wsmv/command_output.rb +2 -3
  46. data/lib/winrm/wsmv/command_output_decoder.rb +1 -2
  47. data/lib/winrm/wsmv/configuration.rb +0 -2
  48. data/lib/winrm/wsmv/create_pipeline.rb +0 -2
  49. data/lib/winrm/wsmv/create_shell.rb +2 -6
  50. data/lib/winrm/wsmv/header.rb +213 -215
  51. data/lib/winrm/wsmv/init_runspace_pool.rb +96 -95
  52. data/lib/winrm/wsmv/iso8601_duration.rb +0 -2
  53. data/lib/winrm/wsmv/keep_alive.rb +0 -2
  54. data/lib/winrm/wsmv/receive_response_reader.rb +128 -126
  55. data/lib/winrm/wsmv/send_data.rb +0 -2
  56. data/lib/winrm/wsmv/soap.rb +0 -2
  57. data/lib/winrm/wsmv/wql_pull.rb +54 -56
  58. data/lib/winrm/wsmv/wql_query.rb +98 -99
  59. data/lib/winrm/wsmv/write_stdin.rb +0 -2
  60. data/tests/integration/auth_timeout_spec.rb +0 -1
  61. data/tests/integration/cmd_spec.rb +2 -3
  62. data/tests/integration/issue_59_spec.rb +0 -1
  63. data/tests/integration/powershell_spec.rb +4 -5
  64. data/tests/integration/spec_helper.rb +3 -6
  65. data/tests/integration/transport_spec.rb +0 -1
  66. data/tests/integration/wql_spec.rb +33 -34
  67. data/tests/matchers.rb +2 -3
  68. data/tests/spec/configuration_spec.rb +0 -1
  69. data/tests/spec/connection_spec.rb +0 -2
  70. data/tests/spec/exception_spec.rb +0 -1
  71. data/tests/spec/http/transport_factory_spec.rb +1 -3
  72. data/tests/spec/http/transport_spec.rb +0 -1
  73. data/tests/spec/output_spec.rb +4 -3
  74. data/tests/spec/psrp/fragment_spec.rb +0 -2
  75. data/tests/spec/psrp/message_data/base_spec.rb +0 -2
  76. data/tests/spec/psrp/message_data/error_record_spec.rb +0 -2
  77. data/tests/spec/psrp/message_data/pipeline_host_call_spec.rb +0 -2
  78. data/tests/spec/psrp/message_data/pipeline_output_spec.rb +0 -2
  79. data/tests/spec/psrp/message_data/pipeline_state_spec.rb +0 -2
  80. data/tests/spec/psrp/message_data/runspace_pool_host_call_spec.rb +0 -2
  81. data/tests/spec/psrp/message_data/runspacepool_state_spec.rb +0 -2
  82. data/tests/spec/psrp/message_data/session_capability_spec.rb +0 -2
  83. data/tests/spec/psrp/message_data_spec.rb +0 -2
  84. data/tests/spec/psrp/message_defragmenter_spec.rb +0 -2
  85. data/tests/spec/psrp/message_fragmenter_spec.rb +0 -2
  86. data/tests/spec/psrp/powershell_output_decoder_spec.rb +0 -2
  87. data/tests/spec/psrp/psrp_message_spec.rb +10 -7
  88. data/tests/spec/psrp/recieve_response_reader_spec.rb +0 -2
  89. data/tests/spec/psrp/uuid_spec.rb +2 -2
  90. data/tests/spec/response_handler_spec.rb +69 -61
  91. data/tests/spec/shells/base_spec.rb +7 -5
  92. data/tests/spec/shells/cmd_spec.rb +4 -4
  93. data/tests/spec/shells/powershell_spec.rb +221 -175
  94. data/tests/spec/spec_helper.rb +0 -1
  95. data/tests/spec/stubs/responses/get_omi_command_output_response.xml.erb +23 -0
  96. data/tests/spec/stubs/responses/get_omi_command_output_response_not_done.xml.erb +24 -0
  97. data/tests/spec/stubs/responses/get_omi_config_response.xml +45 -0
  98. data/tests/spec/stubs/responses/get_omi_powershell_keepalive_response.xml.erb +33 -0
  99. data/tests/spec/stubs/responses/open_shell_omi.xml +43 -0
  100. data/tests/spec/stubs/responses/soap_fault_omi.xml +31 -0
  101. data/tests/spec/wsmv/cleanup_command_spec.rb +0 -2
  102. data/tests/spec/wsmv/close_shell_spec.rb +0 -2
  103. data/tests/spec/wsmv/command_output_decoder_spec.rb +0 -2
  104. data/tests/spec/wsmv/command_output_spec.rb +1 -3
  105. data/tests/spec/wsmv/command_spec.rb +0 -2
  106. data/tests/spec/wsmv/configuration_spec.rb +0 -2
  107. data/tests/spec/wsmv/create_pipeline_spec.rb +2 -3
  108. data/tests/spec/wsmv/create_shell_spec.rb +6 -5
  109. data/tests/spec/wsmv/init_runspace_pool_spec.rb +38 -36
  110. data/tests/spec/wsmv/keep_alive_spec.rb +4 -4
  111. data/tests/spec/wsmv/receive_response_reader_spec.rb +124 -123
  112. data/tests/spec/wsmv/send_data_spec.rb +4 -4
  113. data/tests/spec/wsmv/wql_query_spec.rb +11 -13
  114. data/tests/spec/wsmv/write_stdin_spec.rb +0 -2
  115. data/winrm.gemspec +11 -12
  116. metadata +73 -67
@@ -1,91 +1,90 @@
1
- # -*- encoding: utf-8 -*-
2
- #
3
- # Copyright 2016 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 'securerandom'
18
-
19
- module WinRM
20
- # WinRM connection options, provides defaults and validation.
21
- class ConnectionOpts < Hash
22
- DEFAULT_OPERATION_TIMEOUT = 60
23
- DEFAULT_RECEIVE_TIMEOUT = DEFAULT_OPERATION_TIMEOUT + 10
24
- DEFAULT_MAX_ENV_SIZE = 153600
25
- DEFAULT_LOCALE = 'en-US'.freeze
26
- DEFAULT_RETRY_DELAY = 10
27
- DEFAULT_RETRY_LIMIT = 3
28
-
29
- class << self
30
- def create_with_defaults(overrides)
31
- config = default.merge(overrides)
32
- config = ensure_receive_timeout_is_greater_than_operation_timeout(config)
33
- config.validate
34
- config
35
- end
36
-
37
- private
38
-
39
- def ensure_receive_timeout_is_greater_than_operation_timeout(config)
40
- if config[:receive_timeout] < config[:operation_timeout]
41
- config[:receive_timeout] = config[:operation_timeout] + 10
42
- end
43
- config
44
- end
45
-
46
- def default
47
- config = ConnectionOpts.new
48
- config[:session_id] = SecureRandom.uuid.to_s.upcase
49
- config[:transport] = :negotiate
50
- config[:locale] = DEFAULT_LOCALE
51
- config[:max_envelope_size] = DEFAULT_MAX_ENV_SIZE
52
- config[:operation_timeout] = DEFAULT_OPERATION_TIMEOUT
53
- config[:receive_timeout] = DEFAULT_RECEIVE_TIMEOUT
54
- config[:retry_delay] = DEFAULT_RETRY_DELAY
55
- config[:retry_limit] = DEFAULT_RETRY_LIMIT
56
- config
57
- end
58
- end
59
-
60
- def validate
61
- validate_required_fields
62
- validate_data_types
63
- end
64
-
65
- private
66
-
67
- def validate_required_fields
68
- raise 'endpoint is a required option' unless self[:endpoint]
69
- if self[:client_cert]
70
- raise 'path to client key is required' unless self[:client_key]
71
- else
72
- raise 'user is a required option' unless self[:user]
73
- raise 'password is a required option' unless self[:password]
74
- end
75
- end
76
-
77
- def validate_data_types
78
- validate_integer(:retry_limit)
79
- validate_integer(:retry_delay)
80
- validate_integer(:max_envelope_size)
81
- validate_integer(:operation_timeout)
82
- validate_integer(:receive_timeout, self[:operation_timeout])
83
- end
84
-
85
- def validate_integer(key, min = 0)
86
- value = self[key]
87
- raise "#{key} must be a Integer" unless value && value.is_a?(Integer)
88
- raise "#{key} must be greater than #{min}" unless value > min
89
- end
90
- end
91
- end
1
+ # Copyright 2016 Shawn Neal <sneal@sneal.net>
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require 'securerandom'
16
+
17
+ module WinRM
18
+ # WinRM connection options, provides defaults and validation.
19
+ class ConnectionOpts < Hash
20
+ DEFAULT_OPERATION_TIMEOUT = 60
21
+ DEFAULT_RECEIVE_TIMEOUT = DEFAULT_OPERATION_TIMEOUT + 10
22
+ DEFAULT_MAX_ENV_SIZE = 153600
23
+ DEFAULT_LOCALE = 'en-US'.freeze
24
+ DEFAULT_RETRY_DELAY = 10
25
+ DEFAULT_RETRY_LIMIT = 3
26
+
27
+ class << self
28
+ def create_with_defaults(overrides)
29
+ config = default.merge(overrides)
30
+ config = ensure_receive_timeout_is_greater_than_operation_timeout(config)
31
+ config.validate
32
+ config
33
+ end
34
+
35
+ private
36
+
37
+ def ensure_receive_timeout_is_greater_than_operation_timeout(config)
38
+ if config[:receive_timeout] < config[:operation_timeout]
39
+ config[:receive_timeout] = config[:operation_timeout] + 10
40
+ end
41
+ config
42
+ end
43
+
44
+ def default
45
+ config = ConnectionOpts.new
46
+ config[:session_id] = SecureRandom.uuid.to_s.upcase
47
+ config[:transport] = :negotiate
48
+ config[:locale] = DEFAULT_LOCALE
49
+ config[:max_envelope_size] = DEFAULT_MAX_ENV_SIZE
50
+ config[:operation_timeout] = DEFAULT_OPERATION_TIMEOUT
51
+ config[:receive_timeout] = DEFAULT_RECEIVE_TIMEOUT
52
+ config[:retry_delay] = DEFAULT_RETRY_DELAY
53
+ config[:retry_limit] = DEFAULT_RETRY_LIMIT
54
+ config
55
+ end
56
+ end
57
+
58
+ def validate
59
+ validate_required_fields
60
+ validate_data_types
61
+ end
62
+
63
+ private
64
+
65
+ def validate_required_fields
66
+ raise 'endpoint is a required option' unless self[:endpoint]
67
+
68
+ if self[:client_cert]
69
+ raise 'path to client key is required' unless self[:client_key]
70
+ else
71
+ raise 'user is a required option' unless self[:user]
72
+ raise 'password is a required option' unless self[:password]
73
+ end
74
+ end
75
+
76
+ def validate_data_types
77
+ validate_integer(:retry_limit)
78
+ validate_integer(:retry_delay)
79
+ validate_integer(:max_envelope_size)
80
+ validate_integer(:operation_timeout)
81
+ validate_integer(:receive_timeout, self[:operation_timeout])
82
+ end
83
+
84
+ def validate_integer(key, min = 0)
85
+ value = self[key]
86
+ raise "#{key} must be a Integer" unless value && value.is_a?(Integer)
87
+ raise "#{key} must be greater than #{min}" unless value > min
88
+ end
89
+ end
90
+ end
@@ -1,5 +1,3 @@
1
- # encoding: UTF-8
2
- #
3
1
  # Copyright 2010 Dan Wanek <dan.wanek@gmail.com>
4
2
  #
5
3
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -52,6 +50,20 @@ module WinRM
52
50
  end
53
51
  end
54
52
 
53
+ # A Fault returned in the SOAP response. The XML node contains Code, SubCode and Reason
54
+ class WinRMSoapFault < WinRMError
55
+ attr_reader :code
56
+ attr_reader :subcode
57
+ attr_reader :reason
58
+
59
+ def initialize(code, subcode, reason)
60
+ @code = code
61
+ @subcode = subcode
62
+ @reason = reason
63
+ super("[SOAP ERROR CODE: #{code} (#{subcode})]: #{reason}")
64
+ end
65
+ end
66
+
55
67
  # A Fault returned in the SOAP response. The XML node is a MSFT_WmiError
56
68
  class WinRMWMIError < WinRMError
57
69
  attr_reader :error_code
@@ -1,96 +1,127 @@
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
+ # Copyright 2014 Shawn Neal <sneal@sneal.net>
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require 'rexml/document'
16
+ require_relative '../wsmv/soap'
17
+
18
+ module WinRM
19
+ # Handles the raw WinRM HTTP response. Returns the body as an XML doc
20
+ # or raises the appropriate WinRM error if the response is an error.
21
+ class ResponseHandler
22
+ # @param [String] The raw unparsed response body, if any
23
+ # @param [Integer] The HTTP response status code
24
+ def initialize(response_body, status_code)
25
+ @response_body = response_body
26
+ @status_code = status_code
27
+ end
28
+
29
+ # Processes the response from the WinRM service and either returns an XML
30
+ # doc or raises an appropriate error.
31
+ #
32
+ # @returns [REXML::Document] The parsed response body
33
+ def parse_to_xml
34
+ raise_if_error
35
+ response_xml
36
+ end
37
+
38
+ private
39
+
40
+ def response_xml
41
+ @response_xml ||= REXML::Document.new(@response_body)
42
+ rescue REXML::ParseException => e
43
+ raise WinRMHTTPTransportError.new(
44
+ "Unable to parse WinRM response: #{e.message}", @status_code
45
+ )
46
+ end
47
+
48
+ def raise_if_error
49
+ return if @status_code == 200
50
+
51
+ raise_if_auth_error
52
+ raise_if_wsman_fault
53
+ raise_if_wmi_error
54
+ raise_if_soap_fault
55
+ raise_transport_error
56
+ end
57
+
58
+ def raise_if_auth_error
59
+ raise WinRMAuthorizationError if @status_code == 401
60
+ end
61
+
62
+ def raise_if_wsman_fault
63
+ soap_errors = REXML::XPath.match(
64
+ response_xml,
65
+ "//*[local-name() = 'Envelope']/*[local-name() = 'Body']/*[local-name() = 'Fault']/*"
66
+ )
67
+ return if soap_errors.empty?
68
+
69
+ fault = REXML::XPath.first(
70
+ soap_errors,
71
+ "//*[local-name() = 'WSManFault']"
72
+ )
73
+ raise WinRMWSManFault.new(fault.to_s, fault.attributes['Code']) unless fault.nil?
74
+ end
75
+
76
+ def raise_if_wmi_error
77
+ soap_errors = REXML::XPath.match(
78
+ response_xml,
79
+ "//*[local-name() = 'Envelope']/*[local-name() = 'Body']/*[local-name() = 'Fault']/*"
80
+ )
81
+ return if soap_errors.empty?
82
+
83
+ error = REXML::XPath.first(
84
+ soap_errors,
85
+ "//*[local-name() = 'MSFT_WmiError']"
86
+ )
87
+ return if error.nil?
88
+
89
+ error_code = REXML::XPath.first(
90
+ error,
91
+ "//*[local-name() = 'error_Code']"
92
+ ).text
93
+ raise WinRMWMIError.new(error.to_s, error_code)
94
+ end
95
+
96
+ def raise_if_soap_fault
97
+ soap_errors = REXML::XPath.match(
98
+ response_xml,
99
+ "//*[local-name() = 'Envelope']/*[local-name() = 'Body']/*[local-name() = 'Fault']/*"
100
+ )
101
+ return if soap_errors.empty?
102
+
103
+ code = REXML::XPath.first(
104
+ soap_errors,
105
+ "//*[local-name() = 'Code']/*[local-name() = 'Value']/text()"
106
+ )
107
+ subcode = REXML::XPath.first(
108
+ soap_errors,
109
+ "//*[local-name() = 'Subcode']/*[local-name() = 'Value']/text()"
110
+ )
111
+ reason = REXML::XPath.first(
112
+ soap_errors,
113
+ "//*[local-name() = 'Reason']/*[local-name() = 'Text']/text()"
114
+ )
115
+
116
+ raise WinRMSoapFault.new(code, subcode, reason) unless
117
+ code.nil? && subcode.nil? && reason.nil?
118
+ end
119
+
120
+ def raise_transport_error
121
+ raise WinRMHTTPTransportError.new(
122
+ "Bad HTTP response returned from server. Body(if present):#{@response_body}",
123
+ @status_code
124
+ )
125
+ end
126
+ end
127
+ end
@@ -1,427 +1,462 @@
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
- no_ssl_peer_verification! if opts[:no_ssl_peer_verification]
273
- init_krb
274
- end
275
-
276
- # Sends the SOAP payload to the WinRM service and returns the service's
277
- # SOAP response. If an error occurrs an appropriate error is raised.
278
- #
279
- # @param [String] The XML SOAP message
280
- # @returns [REXML::Document] The parsed response body
281
- def send_request(message)
282
- resp = send_kerberos_request(message)
283
-
284
- if resp.status == 401
285
- @logger.debug 'Got 401 - reinitializing Kerberos and retrying one more time'
286
- init_krb
287
- resp = send_kerberos_request(message)
288
- end
289
-
290
- handler = WinRM::ResponseHandler.new(winrm_decrypt(resp.http_body.content), resp.status)
291
- handler.parse_to_xml
292
- end
293
-
294
- private
295
-
296
- # Sends the SOAP payload to the WinRM service and returns the service's
297
- # HTTP response.
298
- #
299
- # @param [String] The XML SOAP message
300
- # @returns [Object] The HTTP response object
301
- def send_kerberos_request(message)
302
- log_soap_message(message)
303
- original_length = message.bytesize
304
- pad_len, emsg = winrm_encrypt(message)
305
- req_length = original_length + pad_len
306
- hdr = {
307
- 'Connection' => 'Keep-Alive',
308
- 'Content-Type' => 'multipart/encrypted;' \
309
- 'protocol="application/HTTP-Kerberos-session-encrypted";boundary="Encrypted Boundary"'
310
- }
311
-
312
- resp = @httpcli.post(
313
- @endpoint,
314
- body(emsg, req_length, 'application/HTTP-Kerberos-session-encrypted'),
315
- hdr
316
- )
317
- log_soap_message(resp.http_body.content)
318
- resp
319
- end
320
-
321
- def init_krb
322
- @logger.debug "Initializing Kerberos for #{@service}"
323
- @gsscli = GSSAPI::Simple.new(@endpoint.host, @service)
324
- token = @gsscli.init_context
325
- auth = Base64.strict_encode64 token
326
-
327
- hdr = {
328
- 'Authorization' => "Kerberos #{auth}",
329
- 'Connection' => 'Keep-Alive',
330
- 'Content-Type' => 'application/soap+xml;charset=UTF-8'
331
- }
332
- @logger.debug 'Sending HTTP POST for Kerberos Authentication'
333
- r = @httpcli.post(@endpoint, '', hdr)
334
- itok = r.header['WWW-Authenticate'].pop
335
- itok = itok.split.last
336
- itok = Base64.strict_decode64(itok)
337
- @gsscli.init_context(itok)
338
- end
339
-
340
- # rubocop:disable Metrics/MethodLength
341
- # rubocop:disable Metrics/AbcSize
342
-
343
- # @return [String] the encrypted request string
344
- def winrm_encrypt(str)
345
- @logger.debug "Encrypting SOAP message:\n#{str}"
346
- iov_cnt = 3
347
- iov = FFI::MemoryPointer.new(GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * iov_cnt)
348
-
349
- iov0 = GSSAPI::LibGSSAPI::GssIOVBufferDesc.new(FFI::Pointer.new(iov.address))
350
- iov0[:type] = (GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_HEADER | \
351
- GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_FLAG_ALLOCATE)
352
-
353
- iov1 = GSSAPI::LibGSSAPI::GssIOVBufferDesc.new(
354
- FFI::Pointer.new(iov.address + (GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * 1)))
355
- iov1[:type] = GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_DATA
356
- iov1[:buffer].value = str
357
-
358
- iov2 = GSSAPI::LibGSSAPI::GssIOVBufferDesc.new(
359
- FFI::Pointer.new(iov.address + (GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * 2)))
360
- iov2[:type] = (GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_PADDING | \
361
- GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_FLAG_ALLOCATE)
362
-
363
- conf_state = FFI::MemoryPointer.new :uint32
364
- min_stat = FFI::MemoryPointer.new :uint32
365
-
366
- GSSAPI::LibGSSAPI.gss_wrap_iov(
367
- min_stat,
368
- @gsscli.context,
369
- 1,
370
- GSSAPI::LibGSSAPI::GSS_C_QOP_DEFAULT,
371
- conf_state,
372
- iov,
373
- iov_cnt)
374
-
375
- token = [iov0[:buffer].length].pack('L')
376
- token += iov0[:buffer].value
377
- token += iov1[:buffer].value
378
- pad_len = iov2[:buffer].length
379
- token += iov2[:buffer].value if pad_len > 0
380
- [pad_len, token]
381
- end
382
-
383
- # @return [String] the unencrypted response string
384
- def winrm_decrypt(str)
385
- @logger.debug "Decrypting SOAP message:\n#{str}"
386
- iov_cnt = 3
387
- iov = FFI::MemoryPointer.new(GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * iov_cnt)
388
-
389
- iov0 = GSSAPI::LibGSSAPI::GssIOVBufferDesc.new(FFI::Pointer.new(iov.address))
390
- iov0[:type] = (GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_HEADER | \
391
- GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_FLAG_ALLOCATE)
392
-
393
- iov1 = GSSAPI::LibGSSAPI::GssIOVBufferDesc.new(
394
- FFI::Pointer.new(iov.address + (GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * 1)))
395
- iov1[:type] = GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_DATA
396
-
397
- iov2 = GSSAPI::LibGSSAPI::GssIOVBufferDesc.new(
398
- FFI::Pointer.new(iov.address + (GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * 2)))
399
- iov2[:type] = GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_DATA
400
-
401
- str.force_encoding('BINARY')
402
- str.sub!(%r{^.*Content-Type: application\/octet-stream\r\n(.*)--Encrypted.*$}m, '\1')
403
-
404
- len = str.unpack('L').first
405
- iov_data = str.unpack("LA#{len}A*")
406
- iov0[:buffer].value = iov_data[1]
407
- iov1[:buffer].value = iov_data[2]
408
-
409
- min_stat = FFI::MemoryPointer.new :uint32
410
- conf_state = FFI::MemoryPointer.new :uint32
411
- conf_state.write_int(1)
412
- qop_state = FFI::MemoryPointer.new :uint32
413
- qop_state.write_int(0)
414
-
415
- maj_stat = GSSAPI::LibGSSAPI.gss_unwrap_iov(
416
- min_stat, @gsscli.context, conf_state, qop_state, iov, iov_cnt)
417
-
418
- @logger.debug "SOAP message decrypted (MAJ: #{maj_stat}, " \
419
- "MIN: #{min_stat.read_int}):\n#{iov1[:buffer].value}"
420
-
421
- iov1[:buffer].value
422
- end
423
- # rubocop:enable Metrics/MethodLength
424
- # rubocop:enable Metrics/AbcSize
425
- end
426
- end
427
- end # WinRM
1
+ # Copyright 2010 Dan Wanek <dan.wanek@gmail.com>
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require 'httpclient'
16
+ require_relative 'response_handler'
17
+
18
+ module WinRM
19
+ module HTTP
20
+ # A generic HTTP transport that utilized HTTPClient to send messages back and forth.
21
+ # This backend will maintain state for every WinRMWebService instance that is instantiated so it
22
+ # is possible to use GSSAPI with Keep-Alive.
23
+ class HttpTransport
24
+ attr_reader :endpoint
25
+
26
+ def initialize(endpoint, options)
27
+ @endpoint = endpoint.is_a?(String) ? URI.parse(endpoint) : endpoint
28
+ @httpcli = HTTPClient.new(agent_name: 'Ruby WinRM Client')
29
+ @logger = Logging.logger[self]
30
+ @httpcli.receive_timeout = options[:receive_timeout]
31
+ end
32
+
33
+ # Sends the SOAP payload to the WinRM service and returns the service's
34
+ # SOAP response. If an error occurrs an appropriate error is raised.
35
+ #
36
+ # @param [String] The XML SOAP message
37
+ # @returns [REXML::Document] The parsed response body
38
+ def send_request(message)
39
+ ssl_peer_fingerprint_verification!
40
+ log_soap_message(message)
41
+ hdr = { 'Content-Type' => 'application/soap+xml;charset=UTF-8',
42
+ 'Content-Length' => message.bytesize }
43
+ # We need to add this header if using Client Certificate authentication
44
+ unless @httpcli.ssl_config.client_cert.nil?
45
+ hdr['Authorization'] = 'http://schemas.dmtf.org/wbem/wsman/1/wsman/secprofile/https/mutual'
46
+ end
47
+
48
+ resp = @httpcli.post(@endpoint, message, hdr)
49
+ log_soap_message(resp.http_body.content)
50
+ verify_ssl_fingerprint(resp.peer_cert)
51
+ handler = WinRM::ResponseHandler.new(resp.http_body.content, resp.status)
52
+ handler.parse_to_xml
53
+ end
54
+
55
+ # We'll need this to force basic authentication if desired
56
+ def basic_auth_only!
57
+ auths = @httpcli.www_auth.instance_variable_get('@authenticator')
58
+ auths.delete_if { |i| i.scheme !~ /basic/i }
59
+ end
60
+
61
+ # Disable SSPI Auth
62
+ def no_sspi_auth!
63
+ auths = @httpcli.www_auth.instance_variable_get('@authenticator')
64
+ auths.delete_if { |i| i.is_a? HTTPClient::SSPINegotiateAuth }
65
+ end
66
+
67
+ # Disable SSL Peer Verification
68
+ def no_ssl_peer_verification!
69
+ @httpcli.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE
70
+ end
71
+
72
+ # SSL Peer Fingerprint Verification prior to connecting
73
+ def ssl_peer_fingerprint_verification!
74
+ return unless @ssl_peer_fingerprint && !@ssl_peer_fingerprint_verified
75
+
76
+ with_untrusted_ssl_connection do |connection|
77
+ connection_cert = connection.peer_cert_chain.last
78
+ verify_ssl_fingerprint(connection_cert)
79
+ end
80
+ @logger.info("initial ssl fingerprint #{@ssl_peer_fingerprint} verified\n")
81
+ @ssl_peer_fingerprint_verified = true
82
+ no_ssl_peer_verification!
83
+ end
84
+
85
+ # Connect without verification to retrieve untrusted ssl context
86
+ def with_untrusted_ssl_connection
87
+ noverify_peer_context = OpenSSL::SSL::SSLContext.new
88
+ noverify_peer_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
89
+ tcp_connection = TCPSocket.new(@endpoint.host, @endpoint.port)
90
+ begin
91
+ ssl_connection = OpenSSL::SSL::SSLSocket.new(tcp_connection, noverify_peer_context)
92
+ ssl_connection.connect
93
+ yield ssl_connection
94
+ ensure
95
+ tcp_connection.close
96
+ end
97
+ end
98
+
99
+ # compare @ssl_peer_fingerprint to current ssl context
100
+ def verify_ssl_fingerprint(cert)
101
+ return unless @ssl_peer_fingerprint
102
+
103
+ conn_fingerprint = OpenSSL::Digest::SHA1.new(cert.to_der).to_s
104
+ return unless @ssl_peer_fingerprint.casecmp(conn_fingerprint) != 0
105
+
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)
167
+ ssl_peer_fingerprint_verification!
168
+ 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
+
176
+ resp = @httpcli.post(@endpoint, body(seal(message), message.bytesize), hdr)
177
+ verify_ssl_fingerprint(resp.peer_cert)
178
+ if resp.status == 401 && @retryable
179
+ @retryable = false
180
+ init_auth
181
+ send_request(message)
182
+ else
183
+ @retryable = true
184
+ decrypted_body = winrm_decrypt(resp)
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(resp)
199
+ # OMI server doesn't always respond to encrypted messages with encrypted responses over SSL
200
+ return resp.body if resp.header['Content-Type'].first =~ %r{\Aapplication\/soap\+xml}i
201
+ return '' if resp.body.empty?
202
+
203
+ str = resp.body.force_encoding('BINARY')
204
+ str.sub!(%r{^.*Content-Type: application\/octet-stream\r\n(.*)--Encrypted.*$}m, '\1')
205
+
206
+ signature = str[4..19]
207
+ message = @ntlmcli.session.unseal_message str[20..-1]
208
+ return message if @ntlmcli.session.verify_signature(signature, message)
209
+
210
+ raise WinRMHTTPTransportError, 'Could not decrypt NTLM message.'
211
+ end
212
+
213
+ def issue_challenge_response(negotiate)
214
+ auth_header = {
215
+ 'Authorization' => "Negotiate #{negotiate.encode64}",
216
+ 'Content-Type' => 'application/soap+xml;charset=UTF-8'
217
+ }
218
+
219
+ # OMI Server on Linux requires an empty payload with the new auth header to proceed
220
+ # because the config check for max payload size will otherwise break the auth handshake
221
+ # given the OMI server does not support that check
222
+ @httpcli.post(@endpoint, '', auth_header)
223
+
224
+ # return an empty hash of headers for subsequent requests to use
225
+ {}
226
+ end
227
+
228
+ def init_auth
229
+ @logger.debug "Initializing Negotiate for #{@endpoint}"
230
+ auth1 = @ntlmcli.init_context
231
+ hdr = {
232
+ 'Authorization' => "Negotiate #{auth1.encode64}",
233
+ 'Content-Type' => 'application/soap+xml;charset=UTF-8'
234
+ }
235
+ @logger.debug 'Sending HTTP POST for Negotiate Authentication'
236
+ r = @httpcli.post(@endpoint, '', hdr)
237
+ verify_ssl_fingerprint(r.peer_cert)
238
+ auth_header = r.header['WWW-Authenticate'].pop
239
+ unless auth_header
240
+ msg = "Unable to parse authorization header. Headers: #{r.headers}\r\nBody: #{r.body}"
241
+ raise WinRMHTTPTransportError.new(msg, r.status_code)
242
+ end
243
+ itok = auth_header.split.last
244
+ auth3 = @ntlmcli.init_context(itok, channel_binding(r))
245
+ issue_challenge_response(auth3)
246
+ end
247
+
248
+ def channel_binding(response)
249
+ if response.peer_cert.nil?
250
+ nil
251
+ else
252
+ cert = if RUBY_PLATFORM == 'java'
253
+ OpenSSL::X509::Certificate.new(response.peer_cert.cert.getEncoded)
254
+ else
255
+ response.peer_cert
256
+ end
257
+ Net::NTLM::ChannelBinding.create(OpenSSL::X509::Certificate.new(cert))
258
+ end
259
+ end
260
+ end
261
+
262
+ # Uses SSL to secure the transport
263
+ class BasicAuthSSL < HttpTransport
264
+ def initialize(endpoint, user, pass, opts)
265
+ super(endpoint, opts)
266
+ @httpcli.set_auth(endpoint, user, pass)
267
+ basic_auth_only!
268
+ no_ssl_peer_verification! if opts[:no_ssl_peer_verification]
269
+ @ssl_peer_fingerprint = opts[:ssl_peer_fingerprint]
270
+ @httpcli.ssl_config.set_trust_ca(opts[:ca_trust_path]) if opts[:ca_trust_path]
271
+ end
272
+ end
273
+
274
+ # Uses Client Certificate to authenticate and SSL to secure the transport
275
+ class ClientCertAuthSSL < HttpTransport
276
+ def initialize(endpoint, client_cert, client_key, key_pass, opts)
277
+ super(endpoint, opts)
278
+ @httpcli.ssl_config.set_client_cert_file(client_cert, client_key, key_pass)
279
+ @httpcli.www_auth.instance_variable_set('@authenticator', [])
280
+ no_ssl_peer_verification! if opts[:no_ssl_peer_verification]
281
+ @ssl_peer_fingerprint = opts[:ssl_peer_fingerprint]
282
+ @httpcli.ssl_config.set_trust_ca(opts[:ca_trust_path]) if opts[:ca_trust_path]
283
+ end
284
+ end
285
+
286
+ # Uses Kerberos/GSSAPI to authenticate and encrypt messages
287
+ class HttpGSSAPI < HttpTransport
288
+ # @param [String,URI] endpoint the WinRM webservice endpoint
289
+ # @param [String] realm the Kerberos realm we are authenticating to
290
+ # @param [String<optional>] service the service name, default is HTTP
291
+ def initialize(endpoint, realm, opts, service = nil)
292
+ require 'gssapi'
293
+ require 'gssapi/extensions'
294
+
295
+ super(endpoint, opts)
296
+ # Remove the GSSAPI auth from HTTPClient because we are doing our own thing
297
+ no_sspi_auth!
298
+ service ||= 'HTTP'
299
+ @service = "#{service}/#{@endpoint.host}@#{realm}"
300
+ no_ssl_peer_verification! if opts[:no_ssl_peer_verification]
301
+ init_krb
302
+ end
303
+
304
+ # Sends the SOAP payload to the WinRM service and returns the service's
305
+ # SOAP response. If an error occurrs an appropriate error is raised.
306
+ #
307
+ # @param [String] The XML SOAP message
308
+ # @returns [REXML::Document] The parsed response body
309
+ def send_request(message)
310
+ resp = send_kerberos_request(message)
311
+
312
+ if resp.status == 401
313
+ @logger.debug 'Got 401 - reinitializing Kerberos and retrying one more time'
314
+ init_krb
315
+ resp = send_kerberos_request(message)
316
+ end
317
+
318
+ handler = WinRM::ResponseHandler.new(winrm_decrypt(resp.http_body.content), resp.status)
319
+ handler.parse_to_xml
320
+ end
321
+
322
+ private
323
+
324
+ # Sends the SOAP payload to the WinRM service and returns the service's
325
+ # HTTP response.
326
+ #
327
+ # @param [String] The XML SOAP message
328
+ # @returns [Object] The HTTP response object
329
+ def send_kerberos_request(message)
330
+ log_soap_message(message)
331
+ original_length = message.bytesize
332
+ pad_len, emsg = winrm_encrypt(message)
333
+ req_length = original_length + pad_len
334
+ hdr = {
335
+ 'Connection' => 'Keep-Alive',
336
+ 'Content-Type' => 'multipart/encrypted;' \
337
+ 'protocol="application/HTTP-Kerberos-session-encrypted";boundary="Encrypted Boundary"'
338
+ }
339
+
340
+ resp = @httpcli.post(
341
+ @endpoint,
342
+ body(emsg, req_length, 'application/HTTP-Kerberos-session-encrypted'),
343
+ hdr
344
+ )
345
+ log_soap_message(resp.http_body.content)
346
+ resp
347
+ end
348
+
349
+ def init_krb
350
+ @logger.debug "Initializing Kerberos for #{@service}"
351
+ @gsscli = GSSAPI::Simple.new(@endpoint.host, @service)
352
+ token = @gsscli.init_context
353
+ auth = Base64.strict_encode64 token
354
+
355
+ hdr = {
356
+ 'Authorization' => "Kerberos #{auth}",
357
+ 'Connection' => 'Keep-Alive',
358
+ 'Content-Type' => 'application/soap+xml;charset=UTF-8'
359
+ }
360
+ @logger.debug 'Sending HTTP POST for Kerberos Authentication'
361
+ r = @httpcli.post(@endpoint, '', hdr)
362
+ itok = r.header['WWW-Authenticate'].pop
363
+ itok = itok.split.last
364
+ itok = Base64.strict_decode64(itok)
365
+ @gsscli.init_context(itok)
366
+ end
367
+
368
+ # rubocop:disable Metrics/MethodLength
369
+ # rubocop:disable Metrics/AbcSize
370
+
371
+ # @return [String] the encrypted request string
372
+ def winrm_encrypt(str)
373
+ @logger.debug "Encrypting SOAP message:\n#{str}"
374
+ iov_cnt = 3
375
+ iov = FFI::MemoryPointer.new(GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * iov_cnt)
376
+
377
+ iov0 = GSSAPI::LibGSSAPI::GssIOVBufferDesc.new(FFI::Pointer.new(iov.address))
378
+ iov0[:type] = (GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_HEADER | \
379
+ GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_FLAG_ALLOCATE)
380
+
381
+ iov1 = GSSAPI::LibGSSAPI::GssIOVBufferDesc.new(
382
+ FFI::Pointer.new(iov.address + (GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * 1))
383
+ )
384
+ iov1[:type] = GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_DATA
385
+ iov1[:buffer].value = str
386
+
387
+ iov2 = GSSAPI::LibGSSAPI::GssIOVBufferDesc.new(
388
+ FFI::Pointer.new(iov.address + (GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * 2))
389
+ )
390
+ iov2[:type] = (GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_PADDING | \
391
+ GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_FLAG_ALLOCATE)
392
+
393
+ conf_state = FFI::MemoryPointer.new :uint32
394
+ min_stat = FFI::MemoryPointer.new :uint32
395
+
396
+ GSSAPI::LibGSSAPI.gss_wrap_iov(
397
+ min_stat,
398
+ @gsscli.context,
399
+ 1,
400
+ GSSAPI::LibGSSAPI::GSS_C_QOP_DEFAULT,
401
+ conf_state,
402
+ iov,
403
+ iov_cnt
404
+ )
405
+
406
+ token = [iov0[:buffer].length].pack('L')
407
+ token += iov0[:buffer].value
408
+ token += iov1[:buffer].value
409
+ pad_len = iov2[:buffer].length
410
+ token += iov2[:buffer].value if pad_len > 0
411
+ [pad_len, token]
412
+ end
413
+
414
+ # @return [String] the unencrypted response string
415
+ def winrm_decrypt(str)
416
+ @logger.debug "Decrypting SOAP message:\n#{str}"
417
+ iov_cnt = 3
418
+ iov = FFI::MemoryPointer.new(GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * iov_cnt)
419
+
420
+ iov0 = GSSAPI::LibGSSAPI::GssIOVBufferDesc.new(FFI::Pointer.new(iov.address))
421
+ iov0[:type] = (GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_HEADER | \
422
+ GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_FLAG_ALLOCATE)
423
+
424
+ iov1 = GSSAPI::LibGSSAPI::GssIOVBufferDesc.new(
425
+ FFI::Pointer.new(iov.address + (GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * 1))
426
+ )
427
+ iov1[:type] = GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_DATA
428
+
429
+ iov2 = GSSAPI::LibGSSAPI::GssIOVBufferDesc.new(
430
+ FFI::Pointer.new(iov.address + (GSSAPI::LibGSSAPI::GssIOVBufferDesc.size * 2))
431
+ )
432
+ iov2[:type] = GSSAPI::LibGSSAPI::GSS_IOV_BUFFER_TYPE_DATA
433
+
434
+ str.force_encoding('BINARY')
435
+ str.sub!(%r{^.*Content-Type: application\/octet-stream\r\n(.*)--Encrypted.*$}m, '\1')
436
+
437
+ len = str.unpack('L').first
438
+ iov_data = str.unpack("LA#{len}A*")
439
+ iov0[:buffer].value = iov_data[1]
440
+ iov1[:buffer].value = iov_data[2]
441
+
442
+ min_stat = FFI::MemoryPointer.new :uint32
443
+ conf_state = FFI::MemoryPointer.new :uint32
444
+ conf_state.write_int(1)
445
+ qop_state = FFI::MemoryPointer.new :uint32
446
+ qop_state.write_int(0)
447
+
448
+ maj_stat = GSSAPI::LibGSSAPI.gss_unwrap_iov(
449
+ min_stat, @gsscli.context, conf_state, qop_state, iov, iov_cnt
450
+ )
451
+
452
+ @logger.debug "SOAP message decrypted (MAJ: #{maj_stat}, " \
453
+ "MIN: #{min_stat.read_int}):\n#{iov1[:buffer].value}"
454
+
455
+ iov1[:buffer].value
456
+ end
457
+ # rubocop:enable Metrics/MethodLength
458
+ # rubocop:enable Metrics/AbcSize
459
+ end
460
+ end
461
+ end
462
+ # WinRM