chef-winrm 2.3.10

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 (58) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +202 -0
  3. data/README.md +277 -0
  4. data/bin/rwinrm +90 -0
  5. data/lib/winrm/connection.rb +84 -0
  6. data/lib/winrm/connection_opts.rb +92 -0
  7. data/lib/winrm/exceptions.rb +88 -0
  8. data/lib/winrm/http/response_handler.rb +127 -0
  9. data/lib/winrm/http/transport.rb +466 -0
  10. data/lib/winrm/http/transport_factory.rb +64 -0
  11. data/lib/winrm/output.rb +58 -0
  12. data/lib/winrm/psrp/create_pipeline.xml.erb +167 -0
  13. data/lib/winrm/psrp/fragment.rb +68 -0
  14. data/lib/winrm/psrp/init_runspace_pool.xml.erb +224 -0
  15. data/lib/winrm/psrp/message.rb +128 -0
  16. data/lib/winrm/psrp/message_data/base.rb +47 -0
  17. data/lib/winrm/psrp/message_data/error_record.rb +68 -0
  18. data/lib/winrm/psrp/message_data/pipeline_host_call.rb +30 -0
  19. data/lib/winrm/psrp/message_data/pipeline_output.rb +48 -0
  20. data/lib/winrm/psrp/message_data/pipeline_state.rb +38 -0
  21. data/lib/winrm/psrp/message_data/runspacepool_host_call.rb +30 -0
  22. data/lib/winrm/psrp/message_data/runspacepool_state.rb +37 -0
  23. data/lib/winrm/psrp/message_data/session_capability.rb +34 -0
  24. data/lib/winrm/psrp/message_data.rb +40 -0
  25. data/lib/winrm/psrp/message_defragmenter.rb +62 -0
  26. data/lib/winrm/psrp/message_factory.rb +86 -0
  27. data/lib/winrm/psrp/message_fragmenter.rb +58 -0
  28. data/lib/winrm/psrp/powershell_output_decoder.rb +142 -0
  29. data/lib/winrm/psrp/receive_response_reader.rb +95 -0
  30. data/lib/winrm/psrp/session_capability.xml.erb +7 -0
  31. data/lib/winrm/psrp/uuid.rb +39 -0
  32. data/lib/winrm/shells/base.rb +192 -0
  33. data/lib/winrm/shells/cmd.rb +59 -0
  34. data/lib/winrm/shells/power_shell.rb +202 -0
  35. data/lib/winrm/shells/retryable.rb +44 -0
  36. data/lib/winrm/shells/shell_factory.rb +56 -0
  37. data/lib/winrm/version.rb +5 -0
  38. data/lib/winrm/wsmv/base.rb +57 -0
  39. data/lib/winrm/wsmv/cleanup_command.rb +60 -0
  40. data/lib/winrm/wsmv/close_shell.rb +49 -0
  41. data/lib/winrm/wsmv/command.rb +100 -0
  42. data/lib/winrm/wsmv/command_output.rb +75 -0
  43. data/lib/winrm/wsmv/command_output_decoder.rb +54 -0
  44. data/lib/winrm/wsmv/configuration.rb +44 -0
  45. data/lib/winrm/wsmv/create_pipeline.rb +64 -0
  46. data/lib/winrm/wsmv/create_shell.rb +115 -0
  47. data/lib/winrm/wsmv/header.rb +213 -0
  48. data/lib/winrm/wsmv/init_runspace_pool.rb +96 -0
  49. data/lib/winrm/wsmv/iso8601_duration.rb +58 -0
  50. data/lib/winrm/wsmv/keep_alive.rb +66 -0
  51. data/lib/winrm/wsmv/receive_response_reader.rb +128 -0
  52. data/lib/winrm/wsmv/send_data.rb +66 -0
  53. data/lib/winrm/wsmv/soap.rb +49 -0
  54. data/lib/winrm/wsmv/wql_pull.rb +54 -0
  55. data/lib/winrm/wsmv/wql_query.rb +98 -0
  56. data/lib/winrm/wsmv/write_stdin.rb +86 -0
  57. data/lib/winrm.rb +37 -0
  58. metadata +333 -0
@@ -0,0 +1,95 @@
1
+ # Copyright 2016 Matt Wrock <matt@mattwrock.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 'nori'
16
+ require_relative 'powershell_output_decoder'
17
+ require_relative 'message_defragmenter'
18
+
19
+ module WinRM
20
+ module PSRP
21
+ # Class for reading powershell responses in Receive_Response messages
22
+ class ReceiveResponseReader < WSMV::ReceiveResponseReader
23
+ # Creates a new ReceiveResponseReader
24
+ # @param transport [HttpTransport] The WinRM SOAP transport
25
+ # @param logger [Logger] The logger to log diagnostic messages to
26
+ def initialize(transport, logger)
27
+ super
28
+ @output_decoder = PowershellOutputDecoder.new
29
+ end
30
+
31
+ # Reads PSRP messages sent in one or more receive response messages
32
+ # @param wsmv_message [WinRM::WSMV::Base] A wsmv message to send to endpoint
33
+ # @param wait_for_done_state whether to poll for a CommandState of Done
34
+ # @yield [Message] PSRP Message in response
35
+ # @yieldreturn [Array<Message>] All messages in response
36
+ def read_message(wsmv_message, wait_for_done_state = false)
37
+ messages = []
38
+ defragmenter = MessageDefragmenter.new
39
+ read_response(wsmv_message, wait_for_done_state) do |stream|
40
+ message = defragmenter.defragment(stream[:text])
41
+ next unless message
42
+
43
+ if block_given?
44
+ yield message
45
+ else
46
+ messages.push(message)
47
+ end
48
+ end
49
+ messages unless block_given?
50
+ end
51
+
52
+ # Reads streams and returns decoded output
53
+ # @param wsmv_message [WinRM::WSMV::Base] A wsmv message to send to endpoint
54
+ # @yieldparam [string] standard out response text
55
+ # @yieldparam [string] standard error response text
56
+ # @yieldreturn [WinRM::Output] The command output
57
+ def read_output(wsmv_message)
58
+ with_output do |output|
59
+ read_message(wsmv_message, true) do |message|
60
+ exit_code = find_exit_code(message)
61
+ output.exitcode = exit_code if exit_code
62
+ decoded_text = @output_decoder.decode(message)
63
+ next unless decoded_text
64
+
65
+ out = { stream_type(message) => decoded_text }
66
+ output << out
67
+ yield [out[:stdout], out[:stderr]] if block_given?
68
+ end
69
+ end
70
+ end
71
+
72
+ private
73
+
74
+ def stream_type(message)
75
+ type = :stdout
76
+ case message.type
77
+ when WinRM::PSRP::Message::MESSAGE_TYPES[:error_record]
78
+ type = :stderr
79
+ when WinRM::PSRP::Message::MESSAGE_TYPES[:pipeline_host_call]
80
+ type = :stderr if message.data.include?('WriteError')
81
+ when WinRM::PSRP::Message::MESSAGE_TYPES[:pipeline_state]
82
+ type = :stderr if message.parsed_data.pipeline_state == WinRM::PSRP::MessageData::PipelineState::FAILED
83
+ end
84
+ type
85
+ end
86
+
87
+ def find_exit_code(message)
88
+ parsed = message.parsed_data
89
+ return nil unless parsed.is_a?(MessageData::PipelineHostCall)
90
+
91
+ parsed.method_parameters[:i32].to_i if parsed.method_identifier == 'SetShouldExit'
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,7 @@
1
+ <Obj RefId="0">
2
+ <MS>
3
+ <Version N="protocolversion">2.3</Version>
4
+ <Version N="PSVersion">2.0</Version>
5
+ <Version N="SerializationVersion">1.1.0.1</Version>
6
+ </MS>
7
+ </Obj>
@@ -0,0 +1,39 @@
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
+ module WinRM
16
+ module PSRP
17
+ # UUID helper methods
18
+ module UUID
19
+ # Format a UUID into a GUID compatible byte array for Windows
20
+ #
21
+ # https://msdn.microsoft.com/en-us/library/windows/desktop/aa373931(v=vs.85).aspx
22
+ # typedef struct _GUID {
23
+ # DWORD Data1;
24
+ # WORD Data2;
25
+ # WORD Data3;
26
+ # BYTE Data4[8];
27
+ # } GUID;
28
+ #
29
+ # @param uuid [String] Canonical hex format with hypens.
30
+ # @return [Array<byte>] UUID in a Windows GUID compatible byte array layout.
31
+ def uuid_to_windows_guid_bytes(uuid)
32
+ return [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] unless uuid
33
+
34
+ b = uuid.scan(/[0-9a-fA-F]{2}/).map { |x| x.to_i(16) }
35
+ b[0..3].reverse + b[4..5].reverse + b[6..7].reverse + b[8..15]
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,192 @@
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_relative 'retryable'
16
+ require_relative '../http/transport'
17
+ require_relative '../wsmv/cleanup_command'
18
+ require_relative '../wsmv/close_shell'
19
+ require_relative '../wsmv/command'
20
+ require_relative '../wsmv/command_output'
21
+ require_relative '../wsmv/receive_response_reader'
22
+ require_relative '../wsmv/create_shell'
23
+ require_relative '../wsmv/soap'
24
+
25
+ module WinRM
26
+ module Shells
27
+ # Base class for remote shell
28
+ class Base
29
+ TOO_MANY_COMMANDS = '2150859174'.freeze
30
+ ERROR_OPERATION_ABORTED = '995'.freeze
31
+ SHELL_NOT_FOUND = '2150858843'.freeze
32
+
33
+ FAULTS_FOR_RESET = [
34
+ '2150858843', # Shell has been closed
35
+ '2147943418', # Error reading registry key
36
+ TOO_MANY_COMMANDS, # Maximum commands per user exceeded
37
+ ].freeze
38
+
39
+ include Retryable
40
+
41
+ # Create a new Cmd shell
42
+ # @param connection_opts [ConnectionOpts] The WinRM connection options
43
+ # @param transport [HttpTransport] The WinRM SOAP transport
44
+ # @param logger [Logger] The logger to log diagnostic messages to
45
+ # @param shell_opts [Hash] Options targeted for the created shell
46
+ def initialize(connection_opts, transport, logger, shell_opts = {})
47
+ @connection_opts = connection_opts
48
+ @transport = transport
49
+ @logger = logger
50
+ @shell_opts = shell_opts
51
+ end
52
+
53
+ # @return [String] shell id of the currently opn shell or nil if shell is closed
54
+ attr_reader :shell_id
55
+
56
+ # @return [String] uri that SOAP calls use to identify shell type
57
+ attr_reader :shell_uri
58
+
59
+ # @return [ConnectionOpts] connection options of the shell
60
+ attr_reader :connection_opts
61
+
62
+ # @return [WinRM::HTTP::HttpTransport] transport used to talk with endpoint
63
+ attr_reader :transport
64
+
65
+ # @return [Logger] logger used for diagnostic messages
66
+ attr_reader :logger
67
+
68
+ # @return [Hash] Options targeted for the created shell
69
+ attr_reader :shell_opts
70
+
71
+ # Runs the specified command with optional arguments
72
+ # @param command [String] The command or executable to run
73
+ # @param arguments [Array] The optional command arguments
74
+ # @param block [&block] The optional callback for any realtime output
75
+ # @yieldparam [string] standard out response text
76
+ # @yieldparam [string] standard error response text
77
+ # @yieldreturn [WinRM::Output] The command output
78
+ def run(command, arguments = [], &block)
79
+ with_command_shell(command, arguments) do |shell, cmd|
80
+ response_reader.read_output(command_output_message(shell, cmd), &block)
81
+ end
82
+ end
83
+
84
+ # Closes the shell if one is open
85
+ def close
86
+ return unless shell_id
87
+
88
+ begin
89
+ self.class.close_shell(connection_opts, transport, shell_id)
90
+ rescue WinRMWSManFault => e
91
+ raise unless [ERROR_OPERATION_ABORTED, SHELL_NOT_FOUND].include?(e.fault_code)
92
+ end
93
+ remove_finalizer
94
+ @shell_id = nil
95
+ end
96
+
97
+ def self.finalize(connection_opts, transport, shell_id)
98
+ proc { Thread.new { close_shell(connection_opts, transport, shell_id) } }
99
+ end
100
+
101
+ protected
102
+
103
+ def send_command(_command, _arguments)
104
+ raise NotImplementedError
105
+ end
106
+
107
+ def response_reader
108
+ raise NotImplementedError
109
+ end
110
+
111
+ def open_shell
112
+ raise NotImplementedError
113
+ end
114
+
115
+ def out_streams
116
+ raise NotImplementedError
117
+ end
118
+
119
+ def command_output_message(shell_id, command_id)
120
+ cmd_out_opts = {
121
+ shell_id: shell_id,
122
+ command_id: command_id,
123
+ shell_uri: shell_uri,
124
+ out_streams: out_streams
125
+ }
126
+ WinRM::WSMV::CommandOutput.new(connection_opts, cmd_out_opts)
127
+ end
128
+
129
+ def with_command_shell(command, arguments = [])
130
+ tries ||= 2
131
+
132
+ open unless shell_id
133
+ command_id = send_command(command, arguments)
134
+ logger.debug("[WinRM] creating command_id: #{command_id} on shell_id #{shell_id}")
135
+ yield shell_id, command_id
136
+ rescue WinRMWSManFault => e
137
+ raise unless FAULTS_FOR_RESET.include?(e.fault_code) && (tries -= 1) > 0
138
+
139
+ reset_on_error(e)
140
+ retry
141
+ ensure
142
+ cleanup_command(command_id) if command_id
143
+ end
144
+
145
+ private
146
+
147
+ def reset_on_error(error)
148
+ close if error.fault_code == TOO_MANY_COMMANDS
149
+ logger.debug('[WinRM] opening new shell since the current one was deleted')
150
+ @shell_id = nil
151
+ end
152
+
153
+ def cleanup_command(command_id)
154
+ return unless shell_id
155
+ logger.debug("[WinRM] cleaning up command_id: #{command_id} on shell_id #{shell_id}")
156
+ cleanup_msg = WinRM::WSMV::CleanupCommand.new(
157
+ connection_opts,
158
+ shell_uri: shell_uri,
159
+ shell_id: shell_id,
160
+ command_id: command_id
161
+ )
162
+ transport.send_request(cleanup_msg.build)
163
+ rescue WinRMWSManFault => e
164
+ raise unless [ERROR_OPERATION_ABORTED, SHELL_NOT_FOUND].include?(e.fault_code)
165
+ rescue WinRMHTTPTransportError => t
166
+ # dont let the cleanup raise so we dont lose any errors from the command
167
+ logger.info("[WinRM] #{t.status_code} returned in cleanup with error: #{t.message}")
168
+ end
169
+
170
+ def open
171
+ close
172
+ retryable(connection_opts[:retry_limit], connection_opts[:retry_delay]) do
173
+ logger.debug("[WinRM] opening remote shell on #{connection_opts[:endpoint]}")
174
+ @shell_id = open_shell
175
+ end
176
+ logger.debug("[WinRM] remote shell created with shell_id: #{shell_id}")
177
+ add_finalizer
178
+ end
179
+
180
+ def add_finalizer
181
+ ObjectSpace.define_finalizer(
182
+ self,
183
+ self.class.finalize(connection_opts, transport, shell_id)
184
+ )
185
+ end
186
+
187
+ def remove_finalizer
188
+ ObjectSpace.undefine_finalizer(self)
189
+ end
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,59 @@
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_relative 'base'
16
+
17
+ module WinRM
18
+ module Shells
19
+ # Proxy to a remote cmd.exe shell
20
+ class Cmd < Base
21
+ include WinRM::WSMV::SOAP
22
+ class << self
23
+ def close_shell(connection_opts, transport, shell_id)
24
+ msg = WinRM::WSMV::CloseShell.new(connection_opts, shell_id: shell_id)
25
+ transport.send_request(msg.build)
26
+ end
27
+ end
28
+
29
+ protected
30
+
31
+ def send_command(command, arguments)
32
+ cmd_msg = WinRM::WSMV::Command.new(
33
+ connection_opts,
34
+ shell_id: shell_id,
35
+ command: command,
36
+ arguments: arguments
37
+ )
38
+ resp_doc = transport.send_request(cmd_msg.build)
39
+ command_id = REXML::XPath.first(resp_doc, "//*[local-name() = 'CommandId']").text
40
+ logger.debug("[WinRM] Command created for #{command} with id: #{command_id}")
41
+ command_id
42
+ end
43
+
44
+ def response_reader
45
+ @response_reader ||= WinRM::WSMV::ReceiveResponseReader.new(transport, logger)
46
+ end
47
+
48
+ def open_shell
49
+ msg = WinRM::WSMV::CreateShell.new(connection_opts, shell_opts)
50
+ resp_doc = transport.send_request(msg.build)
51
+ REXML::XPath.first(resp_doc, "//*[@Name='ShellId']").text
52
+ end
53
+
54
+ def out_streams
55
+ %w[stdout stderr]
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,202 @@
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' unless defined?(SecureRandom)
16
+ require_relative 'base'
17
+ require_relative '../psrp/message_fragmenter'
18
+ require_relative '../psrp/receive_response_reader'
19
+ require_relative '../wsmv/configuration'
20
+ require_relative '../wsmv/create_pipeline'
21
+ require_relative '../wsmv/send_data'
22
+ require_relative '../wsmv/init_runspace_pool'
23
+ require_relative '../wsmv/keep_alive'
24
+
25
+ module WinRM
26
+ module Shells
27
+ # Proxy to a remote PowerShell instance
28
+ class Powershell < Base
29
+ include WinRM::WSMV::SOAP
30
+
31
+ class << self
32
+ def close_shell(connection_opts, transport, shell_id)
33
+ msg = WinRM::WSMV::CloseShell.new(
34
+ connection_opts,
35
+ shell_id: shell_id,
36
+ shell_uri: WinRM::WSMV::Header::RESOURCE_URI_POWERSHELL
37
+ )
38
+ transport.send_request(msg.build)
39
+ end
40
+ end
41
+
42
+ # Create a new powershell shell
43
+ # @param connection_opts [ConnectionOpts] The WinRM connection options
44
+ # @param transport [HttpTransport] The WinRM SOAP transport
45
+ # @param logger [Logger] The logger to log diagnostic messages to
46
+ def initialize(connection_opts, transport, logger)
47
+ super
48
+ @shell_uri = WinRM::WSMV::Header::RESOURCE_URI_POWERSHELL
49
+ end
50
+
51
+ # Runs the specified command
52
+ # @param command [String] The powershell script to run
53
+ # @param block [&block] The optional callback for any realtime output
54
+ # @yield [Message] PSRP Message in response
55
+ # @yieldreturn [Array<Message>] All messages in response
56
+ def send_pipeline_command(command, &block)
57
+ with_command_shell(command) do |shell, cmd|
58
+ response_reader.read_message(command_output_message(shell, cmd), true, &block)
59
+ end
60
+ end
61
+
62
+ # calculate the maimum fragment size so that they will be as large as possible yet
63
+ # no greater than the max_envelope_size_kb on the end point. To calculate this
64
+ # threshold, we:
65
+ # - determine the maximum number of bytes accepted on the endpoint
66
+ # - subtract the non-fragment characters in the SOAP envelope
67
+ # - determine the number of bytes that could be base64 encded to the above length
68
+ # - subtract the fragment header bytes (ids, length, etc)
69
+ def max_fragment_blob_size
70
+ @max_fragment_blob_size ||= begin
71
+ fragment_header_length = 21
72
+
73
+ begin
74
+ max_fragment_bytes = (max_envelope_size_kb * 1024) - empty_pipeline_envelope.length
75
+ base64_deflated(max_fragment_bytes) - fragment_header_length
76
+ rescue WinRMWSManFault => e
77
+ # A non administrator user will encounter an access denied
78
+ # error attempting to query winrm configuration.
79
+ # we will assin a small default and adjust to a protocol
80
+ # appropriate max length when that info is available
81
+ raise unless e.fault_code == '5'
82
+
83
+ WinRM::PSRP::MessageFragmenter::DEFAULT_BLOB_LENGTH
84
+ rescue WinRMSoapFault
85
+ WinRM::PSRP::MessageFragmenter::DEFAULT_BLOB_LENGTH
86
+ end
87
+ end
88
+ end
89
+
90
+ protected
91
+
92
+ def response_reader
93
+ @response_reader ||= WinRM::PSRP::ReceiveResponseReader.new(transport, logger)
94
+ end
95
+
96
+ def send_command(command, _arguments)
97
+ command_id = SecureRandom.uuid.to_s.upcase
98
+ command += "\r\nif (!$?) { if($LASTEXITCODE) { exit $LASTEXITCODE } else { exit 1 } }"
99
+ message = PSRP::MessageFactory.create_pipeline_message(@runspace_id, command_id, command)
100
+ fragmenter.fragment(message) do |fragment|
101
+ command_args = [connection_opts, shell_id, command_id, fragment]
102
+ if fragment.start_fragment
103
+ resp_doc = transport.send_request(WinRM::WSMV::CreatePipeline.new(*command_args).build)
104
+ command_id = REXML::XPath.first(resp_doc, "//*[local-name() = 'CommandId']").text
105
+ else
106
+ transport.send_request(WinRM::WSMV::SendData.new(*command_args).build)
107
+ end
108
+ end
109
+
110
+ logger.debug("[WinRM] Command created for #{command} with id: #{command_id}")
111
+ command_id
112
+ end
113
+
114
+ def open_shell
115
+ @runspace_id = SecureRandom.uuid.to_s.upcase
116
+ runspace_msg = WinRM::WSMV::InitRunspacePool.new(
117
+ connection_opts,
118
+ @runspace_id,
119
+ open_shell_payload(@runspace_id)
120
+ )
121
+ resp_doc = transport.send_request(runspace_msg.build)
122
+ shell_id = REXML::XPath.first(resp_doc, "//*[@Name='ShellId']").text
123
+ wait_for_running(shell_id)
124
+ shell_id
125
+ end
126
+
127
+ def out_streams
128
+ %w[stdout]
129
+ end
130
+
131
+ private
132
+
133
+ def base64_deflated(inflated_length)
134
+ inflated_length / 4 * 3
135
+ end
136
+
137
+ def empty_pipeline_envelope
138
+ WinRM::WSMV::CreatePipeline.new(
139
+ connection_opts,
140
+ '00000000-0000-0000-0000-000000000000',
141
+ '00000000-0000-0000-0000-000000000000'
142
+ ).build
143
+ end
144
+
145
+ def max_envelope_size_kb
146
+ @max_envelope_size_kb ||= begin
147
+ config_msg = WinRM::WSMV::Configuration.new(connection_opts)
148
+ msg = config_msg.build
149
+ resp_doc = transport.send_request(msg)
150
+ REXML::XPath.first(resp_doc, "//*[local-name() = 'MaxEnvelopeSizekb']").text.to_i
151
+ rescue REXML::ParseException
152
+ logger.debug("[WinRM] Endpoint doesn't support config request for MaxEnvelopeSizekb")
153
+ raise
154
+ end
155
+ end
156
+
157
+ def open_shell_payload(shell_id)
158
+ [
159
+ WinRM::PSRP::MessageFactory.session_capability_message(shell_id),
160
+ WinRM::PSRP::MessageFactory.init_runspace_pool_message(shell_id)
161
+ ].map do |message|
162
+ fragmenter.fragment(message).bytes
163
+ end.flatten
164
+ end
165
+
166
+ def wait_for_running(shell_id)
167
+ state = WinRM::PSRP::MessageData::RunspacepoolState::OPENING
168
+ keepalive_msg = WinRM::WSMV::KeepAlive.new(connection_opts, shell_id)
169
+
170
+ # 2 is "openned". if we start issuing commands while in "openning" the runspace
171
+ # seems to hang
172
+ until state == WinRM::PSRP::MessageData::RunspacepoolState::OPENED
173
+ response_reader.read_message(keepalive_msg) do |message|
174
+ logger.debug("[WinRM] polling for pipeline state. message: #{message.inspect}")
175
+ parsed = message.parsed_data
176
+ case parsed
177
+ when WinRM::PSRP::MessageData::RunspacepoolState
178
+ state = parsed.runspace_state
179
+ when WinRM::PSRP::MessageData::SessionCapability
180
+ # if the user lacks admin privileges, we cannot query the MaxEnvelopeSizeKB
181
+ # on the server and will assign to a "best effort" default based on protocol version
182
+ if fragmenter.max_blob_length == WinRM::PSRP::MessageFragmenter::DEFAULT_BLOB_LENGTH
183
+ fragmenter.max_blob_length = default_protocol_envelope_size(parsed.protocol_version)
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
189
+
190
+ # Powershell v2.0 has a protocol version of 2.1
191
+ # which defaults to a 150 MaxEnvelopeSizeKB
192
+ # later versions default to 500
193
+ def default_protocol_envelope_size(protocol_version)
194
+ protocol_version > '2.1' ? 512000 : 153600
195
+ end
196
+
197
+ def fragmenter
198
+ @fragmenter ||= WinRM::PSRP::MessageFragmenter.new(max_fragment_blob_size)
199
+ end
200
+ end
201
+ end
202
+ end
@@ -0,0 +1,44 @@
1
+ # Copyright 2016 Shawn Neal <sneal@sneal.net>
2
+ # Copyright 2015 Matt Wrock <matt@mattwrock.com>
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ require_relative '../exceptions'
17
+
18
+ module WinRM
19
+ module Shells
20
+ # Shell mixin for retrying an operation
21
+ module Retryable
22
+ RETRYABLE_EXCEPTIONS = lambda do
23
+ [
24
+ Errno::EACCES, Errno::EADDRINUSE, Errno::ECONNREFUSED, Errno::ETIMEDOUT,
25
+ Errno::ECONNRESET, Errno::ENETUNREACH, Errno::EHOSTUNREACH, ::WinRM::WinRMWSManFault,
26
+ ::WinRM::WinRMHTTPTransportError, ::WinRM::WinRMAuthorizationError,
27
+ HTTPClient::KeepAliveDisconnected, HTTPClient::ConnectTimeoutError
28
+ ].freeze
29
+ end
30
+
31
+ # Retries the operation a specified number of times with a delay between
32
+ # @param retries [Integer] The number of times to retry
33
+ # @param delay [Integer] The number of seconds to wait between retry attempts
34
+ def retryable(retries, delay)
35
+ yield
36
+ rescue *RETRYABLE_EXCEPTIONS.call
37
+ raise unless (retries -= 1) > 0
38
+
39
+ sleep(delay)
40
+ retry
41
+ end
42
+ end
43
+ end
44
+ end