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.
- checksums.yaml +7 -0
- data/LICENSE +202 -0
- data/README.md +277 -0
- data/bin/rwinrm +90 -0
- data/lib/winrm/connection.rb +84 -0
- data/lib/winrm/connection_opts.rb +92 -0
- data/lib/winrm/exceptions.rb +88 -0
- data/lib/winrm/http/response_handler.rb +127 -0
- data/lib/winrm/http/transport.rb +466 -0
- data/lib/winrm/http/transport_factory.rb +64 -0
- data/lib/winrm/output.rb +58 -0
- data/lib/winrm/psrp/create_pipeline.xml.erb +167 -0
- data/lib/winrm/psrp/fragment.rb +68 -0
- data/lib/winrm/psrp/init_runspace_pool.xml.erb +224 -0
- data/lib/winrm/psrp/message.rb +128 -0
- data/lib/winrm/psrp/message_data/base.rb +47 -0
- data/lib/winrm/psrp/message_data/error_record.rb +68 -0
- data/lib/winrm/psrp/message_data/pipeline_host_call.rb +30 -0
- data/lib/winrm/psrp/message_data/pipeline_output.rb +48 -0
- data/lib/winrm/psrp/message_data/pipeline_state.rb +38 -0
- data/lib/winrm/psrp/message_data/runspacepool_host_call.rb +30 -0
- data/lib/winrm/psrp/message_data/runspacepool_state.rb +37 -0
- data/lib/winrm/psrp/message_data/session_capability.rb +34 -0
- data/lib/winrm/psrp/message_data.rb +40 -0
- data/lib/winrm/psrp/message_defragmenter.rb +62 -0
- data/lib/winrm/psrp/message_factory.rb +86 -0
- data/lib/winrm/psrp/message_fragmenter.rb +58 -0
- data/lib/winrm/psrp/powershell_output_decoder.rb +142 -0
- data/lib/winrm/psrp/receive_response_reader.rb +95 -0
- data/lib/winrm/psrp/session_capability.xml.erb +7 -0
- data/lib/winrm/psrp/uuid.rb +39 -0
- data/lib/winrm/shells/base.rb +192 -0
- data/lib/winrm/shells/cmd.rb +59 -0
- data/lib/winrm/shells/power_shell.rb +202 -0
- data/lib/winrm/shells/retryable.rb +44 -0
- data/lib/winrm/shells/shell_factory.rb +56 -0
- data/lib/winrm/version.rb +5 -0
- data/lib/winrm/wsmv/base.rb +57 -0
- data/lib/winrm/wsmv/cleanup_command.rb +60 -0
- data/lib/winrm/wsmv/close_shell.rb +49 -0
- data/lib/winrm/wsmv/command.rb +100 -0
- data/lib/winrm/wsmv/command_output.rb +75 -0
- data/lib/winrm/wsmv/command_output_decoder.rb +54 -0
- data/lib/winrm/wsmv/configuration.rb +44 -0
- data/lib/winrm/wsmv/create_pipeline.rb +64 -0
- data/lib/winrm/wsmv/create_shell.rb +115 -0
- data/lib/winrm/wsmv/header.rb +213 -0
- data/lib/winrm/wsmv/init_runspace_pool.rb +96 -0
- data/lib/winrm/wsmv/iso8601_duration.rb +58 -0
- data/lib/winrm/wsmv/keep_alive.rb +66 -0
- data/lib/winrm/wsmv/receive_response_reader.rb +128 -0
- data/lib/winrm/wsmv/send_data.rb +66 -0
- data/lib/winrm/wsmv/soap.rb +49 -0
- data/lib/winrm/wsmv/wql_pull.rb +54 -0
- data/lib/winrm/wsmv/wql_query.rb +98 -0
- data/lib/winrm/wsmv/write_stdin.rb +86 -0
- data/lib/winrm.rb +37 -0
- 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,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
|