303 lines
8.9 KiB
Ruby
303 lines
8.9 KiB
Ruby
#!/usr/bin/ruby
|
|
#
|
|
# winrs.rb
|
|
#
|
|
# Windows Remote Shell
|
|
#
|
|
# See http://msdn.microsoft.com/en-us/library/cc251731.aspx (Remote Shell Examples)
|
|
# for details on the SOAP protocol
|
|
#
|
|
|
|
require 'rexml/document'
|
|
require 'openwsman'
|
|
require 'getoptlong'
|
|
|
|
def usage msg=nil
|
|
if msg
|
|
STDERR.puts "Error: #{msg}"
|
|
STDERR.puts
|
|
end
|
|
STDERR.puts "Usage:"
|
|
STDERR.puts
|
|
STDERR.puts "winrs [-U|--url <host-url>] [<cmd>]"
|
|
STDERR.puts "winrs [-s|--scheme http|https] [-h|--host <host>] [-u|--user <user>] [-p|--password <password>] [-P|--port port] [<cmd>]"
|
|
STDERR.puts
|
|
STDERR.puts "If <cmd> is given as a command line argument, winrs exists after executing <cmd>."
|
|
STDERR.puts "Else winrs runs interactively, accepting and executing command until Ctrl-D is pressed."
|
|
exit 1
|
|
end
|
|
|
|
def handle_fault client, result
|
|
unless result
|
|
if client.last_error != 0
|
|
STDERR.puts "Client connection to #{client.scheme}://#{client.user}:#{client.password}@#{client.host}:#{client.port}/#{client.path} failed with #{client.last_error} HTTP #{client.response_code}, Fault: #{client.fault_string}"
|
|
exit 1
|
|
end
|
|
if client.response_code != 200
|
|
STDERR.puts "Client requested result #{client.response_code}, Fault: #{client.fault_string}"
|
|
exit 1
|
|
end
|
|
STDERR.puts "Client action failed for unknown reason"
|
|
exit 1
|
|
end
|
|
if result.fault?
|
|
fault = Openwsman::Fault.new result
|
|
STDERR.puts "Fault code #{fault.code}, subcode #{fault.subcode}"
|
|
STDERR.puts "\treason #{fault.reason}"
|
|
STDERR.puts "\tdetail #{fault.detail}"
|
|
exit 1
|
|
end
|
|
end
|
|
|
|
#
|
|
# Argument parsing
|
|
#
|
|
|
|
opts = GetoptLong.new(
|
|
[ "-U", "--url", GetoptLong::REQUIRED_ARGUMENT ],
|
|
[ "-h", "--host", GetoptLong::REQUIRED_ARGUMENT ],
|
|
[ "-u", "--user", GetoptLong::REQUIRED_ARGUMENT ],
|
|
[ "-p", "--password", GetoptLong::REQUIRED_ARGUMENT ],
|
|
[ "-P", "--port", GetoptLong::REQUIRED_ARGUMENT ],
|
|
[ "-s", "--scheme", GetoptLong::REQUIRED_ARGUMENT ],
|
|
[ "-?", "--help", GetoptLong::NO_ARGUMENT ],
|
|
[ "-d", "--debug", GetoptLong::NO_ARGUMENT ]
|
|
)
|
|
|
|
options = { }
|
|
url = nil
|
|
opts.each do |opt,arg|
|
|
case opt
|
|
when "-?"
|
|
usage
|
|
exit 0
|
|
when "-U"
|
|
usage "-U|--url invalid, --host|--user|--password|--port already given" unless options.empty?
|
|
url = arg
|
|
when "-h"
|
|
usage "-h|--host invalid, --url already given" unless url.nil?
|
|
options[:host] = arg
|
|
when "-u"
|
|
usage "-u|--user invalid, --url already given" unless url.nil?
|
|
options[:user] = arg
|
|
when "-p"
|
|
usage "-p|--password invalid, --url already given" unless url.nil?
|
|
options[:password] = arg
|
|
when "-P"
|
|
usage "-P|--port invalid, --url already given" unless url.nil?
|
|
options[:port] = arg.to_i
|
|
when "-s"
|
|
usage "-s|--scheme invalid, --url already given" unless url.nil?
|
|
options[:scheme] = arg
|
|
when "-d"
|
|
Openwsman::debug = 99
|
|
end
|
|
end
|
|
|
|
options = { :port => 5985, :scheme => "http" }.merge(options)
|
|
|
|
commands = ARGV.empty? ? nil : ARGV
|
|
|
|
|
|
client = if url
|
|
Openwsman::Client.new url
|
|
elsif options.empty?
|
|
usage
|
|
else
|
|
Openwsman::Client.new(options[:host], options[:port], "/wsman", options[:scheme], options[:user], options[:password])
|
|
end
|
|
|
|
#
|
|
# Client connection
|
|
#
|
|
|
|
client.transport.timeout = 120
|
|
client.transport.auth_method = Openwsman::BASIC_AUTH_STR
|
|
# https
|
|
# client.transport.verify_peer = 0
|
|
# client.transport.verify_host = 0
|
|
|
|
options = Openwsman::ClientOptions.new
|
|
options.set_dump_request if Openwsman::debug == 99
|
|
options.timeout = 60 * 1000 # 60 seconds
|
|
uri = "http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd"
|
|
|
|
#
|
|
# Start shell
|
|
#
|
|
|
|
service = "Themes"
|
|
options.add_selector( "Name", service )
|
|
|
|
options.add_option "WINRS_NOPROFILE","FALSE"
|
|
options.add_option "WINRS_CODEPAGE", 437
|
|
|
|
# instance values
|
|
instance = { "InputStreams" => "stdin", "OutputStreams" => "stdout stderr" }
|
|
|
|
namespace = "http://schemas.microsoft.com/wbem/wsman/1/windows/shell"
|
|
data = Openwsman::XmlDoc.new("Shell", namespace)
|
|
root = data.root
|
|
instance.each do |key,value|
|
|
root.add namespace, key, value
|
|
end
|
|
|
|
s = data.to_xml
|
|
result = client.create( options, uri, s, s.size, "utf-8" )
|
|
# returns something like
|
|
# <s:Body>
|
|
# <x:ResourceCreated>
|
|
# <a:Address>http://10.120.5.37:5985/wsman</a:Address>
|
|
# <a:ReferenceParameters>
|
|
# <w:ResourceURI>http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd</w:ResourceURI>
|
|
# <w:SelectorSet>
|
|
# <w:Selector Name="ShellId">3D5D8879-98EA-49B7-9A33-6842EC0D35D0</w:Selector>
|
|
# </w:SelectorSet>
|
|
# </a:ReferenceParameters>
|
|
# </x:ResourceCreated>
|
|
# </s:Body>
|
|
handle_fault client, result
|
|
|
|
shell_id = result.find(nil, "Selector")
|
|
raise "No shell id returned" unless shell_id
|
|
# puts "Shell ID: #{shell_id}"
|
|
command_id = nil
|
|
|
|
#
|
|
# Run command(s)
|
|
#
|
|
|
|
loop do
|
|
if commands
|
|
break if commands.empty?
|
|
cmd = commands.shift
|
|
else
|
|
print "WinRS> "
|
|
STDOUT.flush
|
|
cmd = gets
|
|
break if cmd.nil?
|
|
cmd.chomp!
|
|
next if cmd.empty?
|
|
end
|
|
|
|
# issue command
|
|
options.options = { "WINRS_CONSOLEMODE_STDIN" => "TRUE", "WINRS_SKIP_CMD_SHELL" => "FALSE" }
|
|
options.selectors = { "ShellId" => shell_id }
|
|
data = Openwsman::XmlDoc.new("CommandLine", namespace)
|
|
root = data.root
|
|
root.add namespace, "Command", cmd
|
|
result = client.invoke( options, uri, "http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Command", data)
|
|
handle_fault client, result
|
|
|
|
command_id = result.find(namespace, "CommandId")
|
|
raise "No command id returned" unless command_id
|
|
command_id = command_id.text
|
|
# puts "Command ID: #{command_id}"
|
|
|
|
#
|
|
# Request stdout/stderr
|
|
#
|
|
|
|
options.options = { }
|
|
# keep ShellId selector
|
|
data = Openwsman::XmlDoc.new("Receive", namespace)
|
|
root = data.root
|
|
node = root.add namespace, "DesiredStream", "stdout stderr"
|
|
node.attr_add nil, "CommandId", command_id
|
|
result = client.invoke( options, uri, "http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Receive", data)
|
|
handle_fault client, result
|
|
|
|
#
|
|
# Receive response
|
|
#
|
|
|
|
response = result.find(namespace, "ReceiveResponse")
|
|
unless response
|
|
STDERR.puts "***Err: No ReceiveResponse in: #{result.to_xml}"
|
|
next
|
|
end
|
|
response.each do |node|
|
|
cmd_id = node.attr "CommandId"
|
|
if cmd_id.nil?
|
|
STDERR.puts "***Err: No CommandId in ReceiveResponse node: #{node.to_xml}"
|
|
next
|
|
end
|
|
if cmd_id.value != command_id
|
|
STDERR.puts "***Err: Wrong CommandId in ReceiveResponse node. Expected #{command_id}, found #{cmd_id.value}"
|
|
next
|
|
end
|
|
# puts "Node: #{node.to_xml}"
|
|
case node.name
|
|
when "Stream"
|
|
stream_name = node.attr "Name"
|
|
unless stream_name
|
|
STDERR.puts "***Err: Stream node has no Name attribute: #{node.to_xml}"
|
|
next
|
|
end
|
|
stream_name = stream_name.value
|
|
str = node.text.unpack('m')[0]
|
|
case stream_name
|
|
when "stdout"
|
|
puts str
|
|
when "stderr"
|
|
STDERR.puts str
|
|
else
|
|
STDERR.puts "***Err: Unknown stream name: #{stream_name}"
|
|
end
|
|
when "CommandState"
|
|
state = node.attr "State"
|
|
unless state
|
|
STDERR.puts "***Err: CommandState node has no State attribute: #{node.to_xml}"
|
|
next
|
|
end
|
|
case state.value
|
|
when "http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done"
|
|
exit_code = node.get "ExitCode"
|
|
if exit_code
|
|
STDERR.puts "Exit code: #{exit_code.text}"
|
|
else
|
|
STDERR.puts "***Err: No exit code for 'done' command: #{node.to_xml}"
|
|
end
|
|
when "http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Running"
|
|
# unclear how to handle this
|
|
when "http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Pending"
|
|
# no-op
|
|
# WinRM 1.1 sends this with ExitCode:0
|
|
else
|
|
STDERR.puts "***Err: Unknown command state: #{state.value}"
|
|
end
|
|
else
|
|
STDERR.puts "***Err: Unknown receive response: #{node.to_xml}"
|
|
end
|
|
end # response.each
|
|
|
|
#
|
|
# terminate shell command
|
|
#
|
|
# not strictly needed for WinRM 2.0, but WinRM 1.1 requires this
|
|
#
|
|
data = Openwsman::XmlDoc.new("Signal", namespace)
|
|
root = data.root
|
|
root.attr_add nil, "CommandId", command_id
|
|
root.add namespace, "Code", "http://schemas.microsoft.com/wbem/wsman/1/windows/shell/signal/terminate"
|
|
result = client.invoke( options, uri, "http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Signal", data)
|
|
handle_fault client, result
|
|
|
|
response = result.find(namespace, "SignalResponse")
|
|
unless response
|
|
STDERR.puts "***Err: No SignalResponse in: #{result.to_xml}"
|
|
end
|
|
end
|
|
|
|
#
|
|
# delete shell resource
|
|
#
|
|
|
|
if shell_id
|
|
options.options = { }
|
|
options.selectors = { "ShellId" => shell_id }
|
|
|
|
result = client.invoke( options, uri, "http://schemas.xmlsoap.org/ws/2004/09/transfer/Delete", nil)
|
|
handle_fault client, result
|
|
end
|