winrm 2.2.3 → 2.3.0

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