winrm 1.8.1 → 2.0.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 (139) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +10 -11
  3. data/.rubocop.yml +26 -22
  4. data/.travis.yml +11 -12
  5. data/Gemfile +3 -9
  6. data/LICENSE +202 -202
  7. data/README.md +232 -215
  8. data/Rakefile +34 -36
  9. data/Vagrantfile +6 -9
  10. data/WinrmAppveyor.psm1 +31 -31
  11. data/appveyor.yml +51 -51
  12. data/bin/rwinrm +97 -97
  13. data/changelog.md +86 -86
  14. data/lib/winrm.rb +39 -42
  15. data/lib/winrm/connection.rb +82 -0
  16. data/lib/winrm/connection_opts.rb +87 -0
  17. data/lib/winrm/{exceptions/exceptions.rb → exceptions.rb} +76 -57
  18. data/lib/winrm/http/response_handler.rb +96 -82
  19. data/lib/winrm/http/transport.rb +424 -435
  20. data/lib/winrm/http/transport_factory.rb +68 -0
  21. data/lib/winrm/output.rb +59 -43
  22. data/lib/winrm/psrp/create_pipeline.xml.erb +167 -0
  23. data/lib/winrm/psrp/fragment.rb +70 -0
  24. data/lib/winrm/psrp/init_runspace_pool.xml.erb +224 -0
  25. data/lib/winrm/psrp/message.rb +130 -0
  26. data/lib/winrm/psrp/message_data.rb +41 -0
  27. data/lib/winrm/psrp/message_data/base.rb +49 -0
  28. data/lib/winrm/psrp/message_data/error_record.rb +68 -0
  29. data/lib/winrm/psrp/message_data/pipeline_host_call.rb +32 -0
  30. data/lib/winrm/psrp/message_data/pipeline_output.rb +49 -0
  31. data/lib/winrm/psrp/message_data/runspacepool_host_call.rb +32 -0
  32. data/lib/winrm/psrp/message_data/runspacepool_state.rb +39 -0
  33. data/lib/winrm/psrp/message_data/session_capability.rb +36 -0
  34. data/lib/winrm/psrp/message_defragmenter.rb +62 -0
  35. data/lib/winrm/psrp/message_factory.rb +75 -0
  36. data/lib/winrm/psrp/message_fragmenter.rb +60 -0
  37. data/lib/winrm/psrp/powershell_output_decoder.rb +120 -0
  38. data/lib/winrm/psrp/receive_response_reader.rb +93 -0
  39. data/lib/winrm/psrp/session_capability.xml.erb +7 -0
  40. data/lib/winrm/psrp/uuid.rb +40 -0
  41. data/lib/winrm/shells/base.rb +175 -0
  42. data/lib/winrm/shells/cmd.rb +65 -0
  43. data/lib/winrm/shells/power_shell.rb +201 -0
  44. data/lib/winrm/shells/retryable.rb +45 -0
  45. data/lib/winrm/shells/shell_factory.rb +50 -0
  46. data/lib/winrm/version.rb +7 -7
  47. data/lib/winrm/wsmv/base.rb +59 -0
  48. data/lib/winrm/wsmv/cleanup_command.rb +61 -0
  49. data/lib/winrm/wsmv/close_shell.rb +50 -0
  50. data/lib/winrm/wsmv/command.rb +101 -0
  51. data/lib/winrm/wsmv/command_output.rb +76 -0
  52. data/lib/winrm/wsmv/command_output_decoder.rb +55 -0
  53. data/lib/winrm/wsmv/configuration.rb +46 -0
  54. data/lib/winrm/wsmv/create_pipeline.rb +66 -0
  55. data/lib/winrm/wsmv/create_shell.rb +119 -0
  56. data/lib/winrm/wsmv/header.rb +203 -0
  57. data/lib/winrm/wsmv/init_runspace_pool.rb +95 -0
  58. data/lib/winrm/wsmv/iso8601_duration.rb +60 -0
  59. data/lib/winrm/wsmv/keep_alive.rb +68 -0
  60. data/lib/winrm/wsmv/receive_response_reader.rb +128 -0
  61. data/lib/winrm/wsmv/send_data.rb +68 -0
  62. data/lib/winrm/wsmv/soap.rb +51 -0
  63. data/lib/winrm/wsmv/wql_query.rb +79 -0
  64. data/lib/winrm/wsmv/write_stdin.rb +88 -0
  65. data/preamble +17 -17
  66. data/{spec → tests/integration}/auth_timeout_spec.rb +18 -16
  67. data/{spec → tests/integration}/cmd_spec.rb +104 -102
  68. data/{spec → tests/integration}/config-example.yml +16 -19
  69. data/{spec → tests/integration}/issue_59_spec.rb +26 -23
  70. data/tests/integration/powershell_spec.rb +154 -0
  71. data/{spec → tests/integration}/spec_helper.rb +65 -73
  72. data/{spec → tests/integration}/transport_spec.rb +99 -139
  73. data/{spec → tests/integration}/wql_spec.rb +16 -14
  74. data/{spec → tests}/matchers.rb +60 -74
  75. data/tests/spec/configuration_spec.rb +93 -0
  76. data/tests/spec/connection_spec.rb +39 -0
  77. data/{spec → tests/spec}/exception_spec.rb +50 -50
  78. data/tests/spec/http/transport_factory_spec.rb +68 -0
  79. data/tests/spec/http/transport_spec.rb +44 -0
  80. data/{spec → tests/spec}/output_spec.rb +127 -110
  81. data/tests/spec/psrp/fragment_spec.rb +62 -0
  82. data/tests/spec/psrp/message_data/base_spec.rb +13 -0
  83. data/tests/spec/psrp/message_data/error_record_spec.rb +41 -0
  84. data/tests/spec/psrp/message_data/pipeline_host_call_spec.rb +25 -0
  85. data/tests/spec/psrp/message_data/pipeline_output_spec.rb +32 -0
  86. data/tests/spec/psrp/message_data/runspace_pool_host_call_spec.rb +25 -0
  87. data/tests/spec/psrp/message_data/runspacepool_state_spec.rb +16 -0
  88. data/tests/spec/psrp/message_data/session_capability_spec.rb +30 -0
  89. data/tests/spec/psrp/message_data_spec.rb +35 -0
  90. data/tests/spec/psrp/message_defragmenter_spec.rb +47 -0
  91. data/tests/spec/psrp/message_fragmenter_spec.rb +105 -0
  92. data/tests/spec/psrp/powershell_output_decoder_spec.rb +84 -0
  93. data/tests/spec/psrp/psrp_message_spec.rb +70 -0
  94. data/tests/spec/psrp/recieve_response_reader_spec.rb +154 -0
  95. data/tests/spec/psrp/uuid_spec.rb +28 -0
  96. data/{spec → tests/spec}/response_handler_spec.rb +61 -61
  97. data/tests/spec/shells/base_spec.rb +202 -0
  98. data/tests/spec/shells/cmd_spec.rb +75 -0
  99. data/tests/spec/shells/powershell_spec.rb +175 -0
  100. data/tests/spec/spec_helper.rb +47 -0
  101. data/tests/spec/stubs/clixml/error_record.xml.erb +84 -0
  102. data/{spec → tests/spec}/stubs/responses/get_command_output_response.xml.erb +13 -13
  103. data/tests/spec/stubs/responses/get_command_output_response_not_done.xml.erb +10 -0
  104. data/tests/spec/stubs/responses/get_powershell_keepalive_response.xml.erb +10 -0
  105. data/tests/spec/stubs/responses/get_powershell_output_response.xml.erb +12 -0
  106. data/tests/spec/stubs/responses/get_powershell_output_response_not_done.xml.erb +9 -0
  107. data/{spec → tests/spec}/stubs/responses/open_shell_v1.xml +19 -19
  108. data/{spec → tests/spec}/stubs/responses/open_shell_v2.xml +20 -20
  109. data/{spec → tests/spec}/stubs/responses/soap_fault_v1.xml +36 -36
  110. data/{spec → tests/spec}/stubs/responses/soap_fault_v2.xml +42 -42
  111. data/{spec → tests/spec}/stubs/responses/wmi_error_v2.xml +41 -41
  112. data/tests/spec/wsmv/cleanup_command_spec.rb +22 -0
  113. data/tests/spec/wsmv/close_shell_spec.rb +17 -0
  114. data/{spec → tests/spec/wsmv}/command_output_decoder_spec.rb +37 -37
  115. data/tests/spec/wsmv/command_output_spec.rb +45 -0
  116. data/tests/spec/wsmv/command_spec.rb +19 -0
  117. data/tests/spec/wsmv/configuration_spec.rb +17 -0
  118. data/tests/spec/wsmv/create_pipeline_spec.rb +31 -0
  119. data/tests/spec/wsmv/create_shell_spec.rb +38 -0
  120. data/tests/spec/wsmv/init_runspace_pool_spec.rb +36 -0
  121. data/tests/spec/wsmv/keep_alive_spec.rb +21 -0
  122. data/tests/spec/wsmv/receive_response_reader_spec.rb +123 -0
  123. data/tests/spec/wsmv/send_data_spec.rb +30 -0
  124. data/tests/spec/wsmv/wql_query_spec.rb +13 -0
  125. data/tests/spec/wsmv/write_stdin_spec.rb +22 -0
  126. data/winrm.gemspec +42 -40
  127. metadata +140 -38
  128. data/.rspec +0 -3
  129. data/lib/winrm/command_executor.rb +0 -243
  130. data/lib/winrm/command_output_decoder.rb +0 -53
  131. data/lib/winrm/helpers/iso8601_duration.rb +0 -58
  132. data/lib/winrm/helpers/powershell_script.rb +0 -42
  133. data/lib/winrm/soap_provider.rb +0 -39
  134. data/lib/winrm/winrm_service.rb +0 -550
  135. data/spec/command_executor_spec.rb +0 -475
  136. data/spec/issue_184_spec.rb +0 -67
  137. data/spec/powershell_spec.rb +0 -97
  138. data/spec/winrm_options_spec.rb +0 -76
  139. data/spec/winrm_primitives_spec.rb +0 -51
@@ -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,40 @@
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
+ module WinRM
18
+ module PSRP
19
+ # UUID helper methods
20
+ module UUID
21
+ # Format a UUID into a GUID compatible byte array for Windows
22
+ #
23
+ # https://msdn.microsoft.com/en-us/library/windows/desktop/aa373931(v=vs.85).aspx
24
+ # typedef struct _GUID {
25
+ # DWORD Data1;
26
+ # WORD Data2;
27
+ # WORD Data3;
28
+ # BYTE Data4[8];
29
+ # } GUID;
30
+ #
31
+ # @param uuid [String] Canonical hex format with hypens.
32
+ # @return [Array<byte>] UUID in a Windows GUID compatible byte array layout.
33
+ def uuid_to_windows_guid_bytes(uuid)
34
+ return [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] unless uuid
35
+ b = uuid.scan(/[0-9a-fA-F]{2}/).map { |x| x.to_i(16) }
36
+ b[0..3].reverse + b[4..5].reverse + b[6..7].reverse + b[8..15]
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,175 @@
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_relative 'retryable'
18
+ require_relative '../http/transport'
19
+ require_relative '../wsmv/cleanup_command'
20
+ require_relative '../wsmv/close_shell'
21
+ require_relative '../wsmv/command'
22
+ require_relative '../wsmv/command_output'
23
+ require_relative '../wsmv/receive_response_reader'
24
+ require_relative '../wsmv/create_shell'
25
+ require_relative '../wsmv/soap'
26
+
27
+ module WinRM
28
+ module Shells
29
+ # Base class for remote shell
30
+ class Base
31
+ TOO_MANY_COMMANDS = '2150859174'.freeze
32
+ ERROR_OPERATION_ABORTED = '995'.freeze
33
+
34
+ FAULTS_FOR_RESET = [
35
+ '2150858843', # Shell has been closed
36
+ '2147943418', # Error reading registry key
37
+ TOO_MANY_COMMANDS, # Maximum commands per user exceeded
38
+ ].freeze
39
+
40
+ include Retryable
41
+
42
+ # Create a new Cmd 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
+ @connection_opts = connection_opts
48
+ @transport = transport
49
+ @logger = logger
50
+ end
51
+
52
+ # @return [String] shell id of the currently opn shell or nil if shell is closed
53
+ attr_reader :shell_id
54
+
55
+ # @return [String] uri that SOAP calls use to identify shell type
56
+ attr_reader :shell_uri
57
+
58
+ # @return [ConnectionOpts] connection options of the shell
59
+ attr_reader :connection_opts
60
+
61
+ # @return [WinRM::HTTP::HttpTransport] transport used to talk with endpoint
62
+ attr_reader :transport
63
+
64
+ # @return [Logger] logger used for diagnostic messages
65
+ attr_reader :logger
66
+
67
+ # Runs the specified command with optional arguments
68
+ # @param command [String] The command or executable to run
69
+ # @param arguments [Array] The optional command arguments
70
+ # @param block [&block] The optional callback for any realtime output
71
+ # @yieldparam [string] standard out response text
72
+ # @yieldparam [string] standard error response text
73
+ # @yieldreturn [WinRM::Output] The command output
74
+ def run(command, arguments = [], &block)
75
+ with_command_shell(command, arguments) do |shell, cmd|
76
+ response_reader.read_output(command_output_message(shell, cmd), &block)
77
+ end
78
+ end
79
+
80
+ # Closes the shell if one is open
81
+ def close
82
+ return unless shell_id
83
+ self.class.close_shell(connection_opts, transport, shell_id)
84
+ remove_finalizer
85
+ @shell_id = nil
86
+ end
87
+
88
+ protected
89
+
90
+ def send_command(_command, _arguments)
91
+ raise NotImplementedError
92
+ end
93
+
94
+ def response_reader
95
+ raise NotImplementedError
96
+ end
97
+
98
+ def open_shell
99
+ raise NotImplementedError
100
+ end
101
+
102
+ def out_streams
103
+ raise NotImplementedError
104
+ end
105
+
106
+ def command_output_message(shell_id, command_id)
107
+ cmd_out_opts = {
108
+ shell_id: shell_id,
109
+ command_id: command_id,
110
+ shell_uri: shell_uri,
111
+ out_streams: out_streams
112
+ }
113
+ WinRM::WSMV::CommandOutput.new(connection_opts, cmd_out_opts)
114
+ end
115
+
116
+ def with_command_shell(command, arguments = [])
117
+ tries ||= 2
118
+
119
+ open unless shell_id
120
+ command_id = send_command(command, arguments)
121
+ logger.debug("[WinRM] creating command_id: #{command_id} on shell_id #{shell_id}")
122
+ yield shell_id, command_id
123
+ rescue WinRMWSManFault => e
124
+ raise unless FAULTS_FOR_RESET.include?(e.fault_code) && (tries -= 1) > 0
125
+ reset_on_error(e)
126
+ retry
127
+ ensure
128
+ cleanup_command(command_id) if command_id
129
+ end
130
+
131
+ private
132
+
133
+ def reset_on_error(error)
134
+ close if error.fault_code == TOO_MANY_COMMANDS
135
+ logger.debug('[WinRM] opening new shell since the current one was deleted')
136
+ @shell_id = nil
137
+ end
138
+
139
+ def cleanup_command(command_id)
140
+ logger.debug("[WinRM] cleaning up command_id: #{command_id} on shell_id #{shell_id}")
141
+ cleanup_msg = WinRM::WSMV::CleanupCommand.new(
142
+ connection_opts,
143
+ shell_uri: shell_uri,
144
+ shell_id: shell_id,
145
+ command_id: command_id)
146
+ transport.send_request(cleanup_msg.build)
147
+ rescue WinRMWSManFault => e
148
+ raise unless e.fault_code == ERROR_OPERATION_ABORTED
149
+ rescue WinRMHTTPTransportError => t
150
+ # dont let the cleanup raise so we dont lose any errors from the command
151
+ logger.info("[WinRM] #{t.status_code} returned in cleanup with error: #{t.message}")
152
+ end
153
+
154
+ def open
155
+ close
156
+ retryable(connection_opts[:retry_limit], connection_opts[:retry_delay]) do
157
+ logger.debug("[WinRM] opening remote shell on #{connection_opts[:endpoint]}")
158
+ @shell_id = open_shell
159
+ end
160
+ logger.debug("[WinRM] remote shell created with shell_id: #{shell_id}")
161
+ add_finalizer
162
+ end
163
+
164
+ def add_finalizer
165
+ ObjectSpace.define_finalizer(
166
+ self,
167
+ self.class.finalize(connection_opts, transport, shell_id))
168
+ end
169
+
170
+ def remove_finalizer
171
+ ObjectSpace.undefine_finalizer(self)
172
+ end
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,65 @@
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_relative 'base'
18
+
19
+ module WinRM
20
+ module Shells
21
+ # Proxy to a remote cmd.exe shell
22
+ class Cmd < Base
23
+ include WinRM::WSMV::SOAP
24
+ class << self
25
+ def finalize(connection_opts, transport, shell_id)
26
+ proc { Cmd.close_shell(connection_opts, transport, shell_id) }
27
+ end
28
+
29
+ def close_shell(connection_opts, transport, shell_id)
30
+ msg = WinRM::WSMV::CloseShell.new(connection_opts, shell_id: shell_id)
31
+ transport.send_request(msg.build)
32
+ end
33
+ end
34
+
35
+ protected
36
+
37
+ def send_command(command, arguments)
38
+ cmd_msg = WinRM::WSMV::Command.new(
39
+ connection_opts,
40
+ shell_id: shell_id,
41
+ command: command,
42
+ arguments: arguments
43
+ )
44
+ resp_doc = transport.send_request(cmd_msg.build)
45
+ command_id = REXML::XPath.first(resp_doc, "//#{NS_WIN_SHELL}:CommandId").text
46
+ logger.debug("[WinRM] Command created for #{command} with id: #{command_id}")
47
+ command_id
48
+ end
49
+
50
+ def response_reader
51
+ @response_reader ||= WinRM::WSMV::ReceiveResponseReader.new(transport, logger)
52
+ end
53
+
54
+ def open_shell
55
+ msg = WinRM::WSMV::CreateShell.new(connection_opts)
56
+ resp_doc = transport.send_request(msg.build)
57
+ REXML::XPath.first(resp_doc, "//*[@Name='ShellId']").text
58
+ end
59
+
60
+ def out_streams
61
+ %w(stdout stderr)
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,201 @@
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
+ require_relative 'base'
19
+ require_relative '../psrp/message_fragmenter'
20
+ require_relative '../psrp/receive_response_reader'
21
+ require_relative '../wsmv/configuration'
22
+ require_relative '../wsmv/create_pipeline'
23
+ require_relative '../wsmv/send_data'
24
+ require_relative '../wsmv/init_runspace_pool'
25
+ require_relative '../wsmv/keep_alive'
26
+
27
+ module WinRM
28
+ module Shells
29
+ # Proxy to a remote PowerShell instance
30
+ class Powershell < Base
31
+ include WinRM::WSMV::SOAP
32
+
33
+ class << self
34
+ def finalize(connection_opts, transport, shell_id)
35
+ proc { PowerShell.close_shell(connection_opts, transport, shell_id) }
36
+ end
37
+
38
+ def close_shell(connection_opts, transport, shell_id)
39
+ msg = WinRM::WSMV::CloseShell.new(
40
+ connection_opts,
41
+ shell_id: shell_id,
42
+ shell_uri: WinRM::WSMV::Header::RESOURCE_URI_POWERSHELL
43
+ )
44
+ transport.send_request(msg.build)
45
+ end
46
+ end
47
+
48
+ # Create a new powershell shell
49
+ # @param connection_opts [ConnectionOpts] The WinRM connection options
50
+ # @param transport [HttpTransport] The WinRM SOAP transport
51
+ # @param logger [Logger] The logger to log diagnostic messages to
52
+ def initialize(connection_opts, transport, logger)
53
+ super
54
+ @shell_uri = WinRM::WSMV::Header::RESOURCE_URI_POWERSHELL
55
+ end
56
+
57
+ # Runs the specified command
58
+ # @param command [String] The powershell script to run
59
+ # @param block [&block] The optional callback for any realtime output
60
+ # @yield [Message] PSRP Message in response
61
+ # @yieldreturn [Array<Message>] All messages in response
62
+ def send_pipeline_command(command, &block)
63
+ with_command_shell(command) do |shell, cmd|
64
+ response_reader.read_message(command_output_message(shell, cmd), true, &block)
65
+ end
66
+ end
67
+
68
+ # calculate the maimum fragment size so that they will be as large as possible yet
69
+ # no greater than the max_envelope_size_kb on the end point. To calculate this
70
+ # threshold, we:
71
+ # - determine the maximum number of bytes accepted on the endpoint
72
+ # - subtract the non-fragment characters in the SOAP envelope
73
+ # - determine the number of bytes that could be base64 encded to the above length
74
+ # - subtract the fragment header bytes (ids, length, etc)
75
+ def max_fragment_blob_size
76
+ @max_fragment_blob_size ||= begin
77
+ fragment_header_length = 21
78
+
79
+ begin
80
+ max_fragment_bytes = (max_envelope_size_kb * 1024) - empty_pipeline_envelope.length
81
+ base64_deflated(max_fragment_bytes) - fragment_header_length
82
+ rescue WinRMWSManFault => e
83
+ # A non administrator user will encounter an access denied
84
+ # error attempting to query winrm configuration.
85
+ # we will assin a small default and adjust to a protocol
86
+ # appropriate max length when that info is available
87
+ raise unless e.fault_code == '5'
88
+ WinRM::PSRP::MessageFragmenter::DEFAULT_BLOB_LENGTH
89
+ end
90
+ end
91
+ end
92
+
93
+ protected
94
+
95
+ def response_reader
96
+ @response_reader ||= WinRM::PSRP::ReceiveResponseReader.new(transport, logger)
97
+ end
98
+
99
+ def send_command(command, _arguments)
100
+ command_id = SecureRandom.uuid.to_s.upcase
101
+ message = PSRP::MessageFactory.create_pipeline_message(@runspace_id, command_id, command)
102
+ fragmenter.fragment(message) do |fragment|
103
+ command_args = [connection_opts, shell_id, command_id, fragment]
104
+ if fragment.start_fragment
105
+ resp_doc = transport.send_request(WinRM::WSMV::CreatePipeline.new(*command_args).build)
106
+ command_id = REXML::XPath.first(resp_doc, "//#{NS_WIN_SHELL}:CommandId").text
107
+ else
108
+ transport.send_request(WinRM::WSMV::SendData.new(*command_args).build)
109
+ end
110
+ end
111
+
112
+ logger.debug("[WinRM] Command created for #{command} with id: #{command_id}")
113
+ command_id
114
+ end
115
+
116
+ def open_shell
117
+ @runspace_id = SecureRandom.uuid.to_s.upcase
118
+ runspace_msg = WinRM::WSMV::InitRunspacePool.new(
119
+ connection_opts,
120
+ @runspace_id,
121
+ open_shell_payload(@runspace_id)
122
+ )
123
+ resp_doc = transport.send_request(runspace_msg.build)
124
+ shell_id = REXML::XPath.first(resp_doc, "//*[@Name='ShellId']").text
125
+ wait_for_running(shell_id)
126
+ shell_id
127
+ end
128
+
129
+ def out_streams
130
+ %w(stdout)
131
+ end
132
+
133
+ private
134
+
135
+ def base64_deflated(inflated_length)
136
+ inflated_length / 4 * 3
137
+ end
138
+
139
+ def empty_pipeline_envelope
140
+ WinRM::WSMV::CreatePipeline.new(
141
+ connection_opts,
142
+ '00000000-0000-0000-0000-000000000000',
143
+ '00000000-0000-0000-0000-000000000000'
144
+ ).build
145
+ end
146
+
147
+ def max_envelope_size_kb
148
+ @max_envelope_size_kb ||= begin
149
+ config_msg = WinRM::WSMV::Configuration.new(connection_opts)
150
+ msg = config_msg.build
151
+ resp_doc = transport.send_request(msg)
152
+ REXML::XPath.first(resp_doc, "//#{NS_WSMAN_CONF}:MaxEnvelopeSizekb").text.to_i
153
+ end
154
+ end
155
+
156
+ def open_shell_payload(shell_id)
157
+ [
158
+ WinRM::PSRP::MessageFactory.session_capability_message(shell_id),
159
+ WinRM::PSRP::MessageFactory.init_runspace_pool_message(shell_id)
160
+ ].map do |message|
161
+ fragmenter.fragment(message).bytes
162
+ end.flatten
163
+ end
164
+
165
+ def wait_for_running(shell_id)
166
+ state = WinRM::PSRP::MessageData::RunspacepoolState::OPENING
167
+ keepalive_msg = WinRM::WSMV::KeepAlive.new(connection_opts, shell_id)
168
+
169
+ # 2 is "openned". if we start issuing commands while in "openning" the runspace
170
+ # seems to hang
171
+ until state == WinRM::PSRP::MessageData::RunspacepoolState::OPENED
172
+ response_reader.read_message(keepalive_msg) do |message|
173
+ logger.debug("[WinRM] polling for pipeline state. message: #{message.inspect}")
174
+ parsed = message.parsed_data
175
+ case parsed
176
+ when WinRM::PSRP::MessageData::RunspacepoolState
177
+ state = parsed.runspace_state
178
+ when WinRM::PSRP::MessageData::SessionCapability
179
+ # if the user lacks admin privileges, we cannot query the MaxEnvelopeSizeKB
180
+ # on the server and will assign to a "best effort" default based on protocol version
181
+ if fragmenter.max_blob_length == WinRM::PSRP::MessageFragmenter::DEFAULT_BLOB_LENGTH
182
+ fragmenter.max_blob_length = default_protocol_envelope_size(parsed.protocol_version)
183
+ end
184
+ end
185
+ end
186
+ end
187
+ end
188
+
189
+ # Powershell v2.0 has a protocol version of 2.1
190
+ # which defaults to a 150 MaxEnvelopeSizeKB
191
+ # later versions default to 500
192
+ def default_protocol_envelope_size(protocol_version)
193
+ protocol_version > '2.1' ? 512000 : 153600
194
+ end
195
+
196
+ def fragmenter
197
+ @fragmenter ||= WinRM::PSRP::MessageFragmenter.new(max_fragment_blob_size)
198
+ end
199
+ end
200
+ end
201
+ end