blob: 6bf3e4b8499a8408cf169516ac2a311b21cc41fc [file] [log] [blame]
# 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