#!/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
