Class: WinRM::WinRMWebService

Inherits:
Object
  • Object
show all
Defined in:
lib/winrm/winrm_service.rb

Overview

This is the main class that does the SOAP request/response logic. There are a few helper classes, but pretty much everything comes through here first.

Constant Summary collapse

DEFAULT_TIMEOUT =
'PT60S'
DEFAULT_MAX_ENV_SIZE =
153600
DEFAULT_LOCALE =
'en-US'

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(endpoint, transport = :kerberos, opts = {}) ⇒ WinRMWebService

Returns a new instance of WinRMWebService.

Parameters:

  • endpoint (String, URI)

    the WinRM webservice endpoint

  • transport (Symbol) (defaults to: :kerberos)

    either :kerberos(default)/:ssl/:plaintext

  • opts (Hash) (defaults to: {})

    Misc opts for the various transports. @see WinRM::HTTP::HttpTransport @see WinRM::HTTP::HttpGSSAPI @see WinRM::HTTP::HttpSSL



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/winrm/winrm_service.rb', line 37

def initialize(endpoint, transport = :kerberos, opts = {})
  @endpoint = endpoint
  @timeout = DEFAULT_TIMEOUT
  @max_env_sz = DEFAULT_MAX_ENV_SIZE
  @locale = DEFAULT_LOCALE
  @logger = Logging.logger[self]
  case transport
  when :kerberos
    require 'gssapi'
    require 'gssapi/extensions'
    @xfer = HTTP::HttpGSSAPI.new(endpoint, opts[:realm], opts[:service], opts[:keytab], opts)
  when :plaintext
    @xfer = HTTP::HttpPlaintext.new(endpoint, opts[:user], opts[:pass], opts)
  when :ssl
    @xfer = HTTP::HttpSSL.new(endpoint, opts[:user], opts[:pass], opts[:ca_trust_path], opts)
  else
    raise "Invalid transport '#{transport}' specified, expected: kerberos, plaintext, ssl."
  end
end

Instance Attribute Details

#endpointObject (readonly)

Returns the value of attribute endpoint.



29
30
31
# File 'lib/winrm/winrm_service.rb', line 29

def endpoint
  @endpoint
end

#timeoutObject (readonly)

Returns the value of attribute timeout.



29
30
31
# File 'lib/winrm/winrm_service.rb', line 29

def timeout
  @timeout
end

Instance Method Details

#cleanup_command(shell_id, command_id) ⇒ true

Clean-up after a command.

Parameters:

  • shell_id (String)

    The shell id on the remote machine. See #open_shell

  • command_id (String)

    The command id on the remote machine. See #run_command

Returns:

  • (true)

    This should have more error checking but it just returns true for now.

See Also:



261
262
263
264
265
266
267
268
269
270
271
272
273
274
# File 'lib/winrm/winrm_service.rb', line 261

def cleanup_command(shell_id, command_id)
  # Signal the Command references to terminate (close stdout/stderr)
  body = { "#{NS_WIN_SHELL}:Code" => 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/signal/terminate' }
  builder = Builder::XmlMarkup.new
  builder.instruct!(:xml, :encoding => 'UTF-8')
  builder.tag! :env, :Envelope, namespaces do |env|
    env.tag!(:env, :Header) { |h| h << Gyoku.xml(merge_headers(header,resource_uri_cmd,action_signal,selector_shell_id(shell_id))) }
    env.tag!(:env, :Body) do |env_body|
      env_body.tag!("#{NS_WIN_SHELL}:Signal", {'CommandId' => command_id}) { |cl| cl << Gyoku.xml(body) }
    end
  end
  resp = send_message(builder.target!)
  true
end

#close_shell(shell_id) ⇒ true

Close the shell

Parameters:

  • shell_id (String)

    The shell id on the remote machine. See #open_shell

Returns:

  • (true)

    This should have more error checking but it just returns true for now.



279
280
281
282
283
284
285
286
287
288
289
290
# File 'lib/winrm/winrm_service.rb', line 279

def close_shell(shell_id)
  builder = Builder::XmlMarkup.new
  builder.instruct!(:xml, :encoding => 'UTF-8')

  builder.tag!('env:Envelope', namespaces) do |env|
    env.tag!('env:Header') { |h| h << Gyoku.xml(merge_headers(header,resource_uri_cmd,action_delete,selector_shell_id(shell_id))) }
    env.tag!('env:Body')
  end

  resp = send_message(builder.target!)
  true
end

#get_command_output(shell_id, command_id, &block) ⇒ Hash

Get the Output of the given shell and command

Parameters:

  • shell_id (String)

    The shell id on the remote machine. See #open_shell

  • command_id (String)

    The command id on the remote machine. See #run_command

Returns:

  • (Hash)

    Returns a Hash with a key :exitcode and :data. Data is an Array of Hashes where the cooresponding key is either :stdout or :stderr. The reason it is in an Array so so we can get the output in the order it ocurrs on the console.



212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'lib/winrm/winrm_service.rb', line 212

def get_command_output(shell_id, command_id, &block)
  body = { "#{NS_WIN_SHELL}:DesiredStream" => 'stdout stderr',
    :attributes! => {"#{NS_WIN_SHELL}:DesiredStream" => {'CommandId' => command_id}}}

  builder = Builder::XmlMarkup.new
  builder.instruct!(:xml, :encoding => 'UTF-8')
  builder.tag! :env, :Envelope, namespaces do |env|
    env.tag!(:env, :Header) { |h| h << Gyoku.xml(merge_headers(header,resource_uri_cmd,action_receive,selector_shell_id(shell_id))) }
    env.tag!(:env, :Body) do |env_body|
      env_body.tag!("#{NS_WIN_SHELL}:Receive") { |cl| cl << Gyoku.xml(body) }
    end
  end

  resp_doc = nil
  request_msg = builder.target!
  done_elems = []
  output = Output.new

  while done_elems.empty?
    resp_doc = send_get_output_message(request_msg)

    REXML::XPath.match(resp_doc, "//#{NS_WIN_SHELL}:Stream").each do |n|
      next if n.text.nil? || n.text.empty?
      stream = { n.attributes['Name'].to_sym => Base64.decode64(n.text).force_encoding('utf-8') }
      output[:data] << stream
      yield stream[:stdout], stream[:stderr] if block_given?
    end

    # We may need to get additional output if the stream has not finished.
    # The CommandState will change from Running to Done like so:
    # @example
    #   from...
    #   <rsp:CommandState CommandId="..." State="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Running"/>
    #   to...
    #   <rsp:CommandState CommandId="..." State="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done">
    #     <rsp:ExitCode>0</rsp:ExitCode>
    #   </rsp:CommandState>
    done_elems = REXML::XPath.match(resp_doc, "//*[@State='http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done']")
  end

  output[:exitcode] = REXML::XPath.first(resp_doc, "//#{NS_WIN_SHELL}:ExitCode").text.to_i
  output
end

#locale(locale) ⇒ Object

Set the locale

Parameters:

  • locale (String)

    the locale to set for future messages

See Also:



84
85
86
# File 'lib/winrm/winrm_service.rb', line 84

def locale(locale)
  @locale = locale
end

#max_env_size(byte_sz) ⇒ Object

Max envelope size

Parameters:

  • byte_sz (Fixnum)

    the max size in bytes to allow for the response

See Also:



77
78
79
# File 'lib/winrm/winrm_service.rb', line 77

def max_env_size(byte_sz)
  @max_env_sz = byte_sz
end

#open_shell(shell_opts = {}, &block) ⇒ String

Create a Shell on the destination host

Parameters:

  • shell_opts (Hash<optional>) (defaults to: {})

    additional shell options you can pass

Options Hash (shell_opts):

  • :i_stream (String)

    Which input stream to open. Leave this alone unless you know what you’re doing (default: stdin)

  • :o_stream (String)

    Which output stream to open. Leave this alone unless you know what you’re doing (default: stdout stderr)

  • :working_directory (String)

    the directory to create the shell in

  • :env_vars (Hash)

    environment variables to set for the shell. For instance; :env_vars => => ‘val1’, :myvar2 => ‘var2’

Returns:

  • (String)

    The ShellId from the SOAP response. This is our open shell instance on the remote machine.



96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/winrm/winrm_service.rb', line 96

def open_shell(shell_opts = {}, &block)
  i_stream = shell_opts.has_key?(:i_stream) ? shell_opts[:i_stream] : 'stdin'
  o_stream = shell_opts.has_key?(:o_stream) ? shell_opts[:o_stream] : 'stdout stderr'
  codepage = shell_opts.has_key?(:codepage) ? shell_opts[:codepage] : 65001 # utf8 as default codepage (from https://msdn.microsoft.com/en-us/library/dd317756(VS.85).aspx)
  noprofile = shell_opts.has_key?(:noprofile) ? shell_opts[:noprofile] : 'FALSE'
  h_opts = { "#{NS_WSMAN_DMTF}:OptionSet" => { "#{NS_WSMAN_DMTF}:Option" => [noprofile, codepage],
    :attributes! => {"#{NS_WSMAN_DMTF}:Option" => {'Name' => ['WINRS_NOPROFILE','WINRS_CODEPAGE']}}}}
  shell_body = {
    "#{NS_WIN_SHELL}:InputStreams" => i_stream,
    "#{NS_WIN_SHELL}:OutputStreams" => o_stream
  }
  shell_body["#{NS_WIN_SHELL}:WorkingDirectory"] = shell_opts[:working_directory] if shell_opts.has_key?(:working_directory)
  shell_body["#{NS_WIN_SHELL}:IdleTimeOut"] = shell_opts[:idle_timeout] if(shell_opts.has_key?(:idle_timeout) && shell_opts[:idle_timeout].is_a?(String))
  if(shell_opts.has_key?(:env_vars) && shell_opts[:env_vars].is_a?(Hash))
    keys = shell_opts[:env_vars].keys
    vals = shell_opts[:env_vars].values
    shell_body["#{NS_WIN_SHELL}:Environment"] = {
      "#{NS_WIN_SHELL}:Variable" => vals,
      :attributes! => {"#{NS_WIN_SHELL}:Variable" => {'Name' => keys}}
    }
  end
  builder = Builder::XmlMarkup.new
  builder.instruct!(:xml, :encoding => 'UTF-8')
  builder.tag! :env, :Envelope, namespaces do |env|
    env.tag!(:env, :Header) { |h| h << Gyoku.xml(merge_headers(header,resource_uri_cmd,action_create,h_opts)) }
    env.tag! :env, :Body do |body|
      body.tag!("#{NS_WIN_SHELL}:Shell") { |s| s << Gyoku.xml(shell_body)}
    end
  end

  resp_doc = send_message(builder.target!)
  shell_id = REXML::XPath.first(resp_doc, "//*[@Name='ShellId']").text

  if block_given?
    begin
      yield shell_id
    ensure
      close_shell(shell_id)
    end
  else
    shell_id
  end
end

#run_cmd(command, arguments = [], &block) ⇒ Hash Also known as: cmd

Run a CMD command

Parameters:

  • command (String)

    The command to run on the remote system

  • arguments (Array <String>) (defaults to: [])

    arguments to the command

  • an (String)

    existing and open shell id to reuse

Returns:

  • (Hash)

    :stdout and :stderr



297
298
299
300
301
302
303
304
305
# File 'lib/winrm/winrm_service.rb', line 297

def run_cmd(command, arguments = [], &block)
  command_output = nil
  open_shell do |shell_id|
    run_command(shell_id, command, arguments) do |command_id|
      command_output = get_command_output(shell_id, command_id, &block)
    end
  end
  command_output
end

#run_command(shell_id, command, arguments = [], cmd_opts = {}, &block) ⇒ String

Run a command on a machine with an open shell

Parameters:

  • shell_id (String)

    The shell id on the remote machine. See #open_shell

  • command (String)

    The command to run on the remote machine

  • arguments (Array<String>) (defaults to: [])

    An array of arguments for this command

Returns:

  • (String)

    The CommandId from the SOAP response. This is the ID we need to query in order to get output.



145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/winrm/winrm_service.rb', line 145

def run_command(shell_id, command, arguments = [], cmd_opts = {}, &block)
  consolemode = cmd_opts.has_key?(:console_mode_stdin) ? cmd_opts[:console_mode_stdin] : 'TRUE'
  skipcmd     = cmd_opts.has_key?(:skip_cmd_shell) ? cmd_opts[:skip_cmd_shell] : 'FALSE'

  h_opts = { "#{NS_WSMAN_DMTF}:OptionSet" => {
    "#{NS_WSMAN_DMTF}:Option" => [consolemode, skipcmd],
    :attributes! => {"#{NS_WSMAN_DMTF}:Option" => {'Name' => ['WINRS_CONSOLEMODE_STDIN','WINRS_SKIP_CMD_SHELL']}}}
  }
  body = { "#{NS_WIN_SHELL}:Command" => "\"#{command}\"", "#{NS_WIN_SHELL}:Arguments" => arguments}

  builder = Builder::XmlMarkup.new
  builder.instruct!(:xml, :encoding => 'UTF-8')
  builder.tag! :env, :Envelope, namespaces do |env|
    env.tag!(:env, :Header) { |h| h << Gyoku.xml(merge_headers(header,resource_uri_cmd,action_command,h_opts,selector_shell_id(shell_id))) }
    env.tag!(:env, :Body) do |env_body|
      env_body.tag!("#{NS_WIN_SHELL}:CommandLine") { |cl| cl << Gyoku.xml(body) }
    end
  end

  # Grab the command element and unescape any single quotes - issue 69
  xml = builder.target!
  escaped_cmd = /<#{NS_WIN_SHELL}:Command>(.+)<\/#{NS_WIN_SHELL}:Command>/m.match(xml)[1]
  xml[escaped_cmd] = escaped_cmd.gsub(/&#39;/, "'")

  resp_doc = send_message(xml)
  command_id = REXML::XPath.first(resp_doc, "//#{NS_WIN_SHELL}:CommandId").text

  if block_given?
    begin
      yield command_id
    ensure
      cleanup_command(shell_id, command_id)
    end
  else
    command_id
  end
end

#run_powershell_script(script_file, &block) ⇒ Hash Also known as: powershell

Run a Powershell script that resides on the local box.

Parameters:

  • script_file (IO, String)

    an IO reference for reading the Powershell script or the actual file contents

  • an (String)

    existing and open shell id to reuse

Returns:

  • (Hash)

    :stdout and :stderr



313
314
315
316
317
318
# File 'lib/winrm/winrm_service.rb', line 313

def run_powershell_script(script_file, &block)
  # if an IO object is passed read it..otherwise assume the contents of the file were passed
  script_text = script_file.respond_to?(:read) ? script_file.read : script_file
  script = WinRM::PowershellScript.new(script_text)
  run_cmd("powershell -encodedCommand #{script.encoded()}", &block)
end

#run_wql(wql) ⇒ Hash Also known as: wql

Run a WQL Query

Parameters:

  • wql (String)

    The WQL query

Returns:

  • (Hash)

    Returns a Hash that contain the key/value pairs returned from the query.

See Also:



326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
# File 'lib/winrm/winrm_service.rb', line 326

def run_wql(wql)

  body = { "#{NS_WSMAN_DMTF}:OptimizeEnumeration" => nil,
    "#{NS_WSMAN_DMTF}:MaxElements" => '32000',
    "#{NS_WSMAN_DMTF}:Filter" => wql,
    :attributes! => { "#{NS_WSMAN_DMTF}:Filter" => {'Dialect' => 'http://schemas.microsoft.com/wbem/wsman/1/WQL'}}
  }

  builder = Builder::XmlMarkup.new
  builder.instruct!(:xml, :encoding => 'UTF-8')
  builder.tag! :env, :Envelope, namespaces do |env|
    env.tag!(:env, :Header) { |h| h << Gyoku.xml(merge_headers(header,resource_uri_wmi,action_enumerate)) }
    env.tag!(:env, :Body) do |env_body|
      env_body.tag!("#{NS_ENUM}:Enumerate") { |en| en << Gyoku.xml(body) }
    end
  end

  resp = send_message(builder.target!)
  parser = Nori.new(:parser => :rexml, :advanced_typecasting => false, :convert_tags_to => lambda { |tag| tag.snakecase.to_sym }, :strip_namespaces => true)
  hresp = parser.parse(resp.to_s)[:envelope][:body]
  
  # Normalize items so the type always has an array even if it's just a single item.
  items = {}
  if hresp[:enumerate_response][:items]
    hresp[:enumerate_response][:items].each_pair do |k,v|
      if v.is_a?(Array)
        items[k] = v
      else
        items[k] = [v]
      end
    end
  end
  items
end

#set_timeout(op_timeout_sec, receive_timeout_sec = nil) ⇒ String Also known as: op_timeout

Operation timeout.

Unless specified the client receive timeout will be 10s + the operation timeout.

Parameters:

  • The (Fixnum)

    number of seconds to set the WinRM operation timeout

  • The (Fixnum)

    number of seconds to set the Ruby receive timeout

Returns:

  • (String)

    The ISO 8601 formatted operation timeout

See Also:



67
68
69
70
71
# File 'lib/winrm/winrm_service.rb', line 67

def set_timeout(op_timeout_sec, receive_timeout_sec=nil)
  @timeout = Iso8601Duration.sec_to_dur(op_timeout_sec)
  @xfer.receive_timeout = receive_timeout_sec || op_timeout_sec + 10
  @timeout
end

#toggle_nori_type_casting(to) ⇒ Object



362
363
364
365
# File 'lib/winrm/winrm_service.rb', line 362

def toggle_nori_type_casting(to)
  @logger.warn('toggle_nori_type_casting is deprecated and has no effect, ' +
    'please remove calls to it')
end

#write_stdin(shell_id, command_id, stdin) ⇒ Object



183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/winrm/winrm_service.rb', line 183

def write_stdin(shell_id, command_id, stdin)
  # Signal the Command references to terminate (close stdout/stderr)
  body = {
    "#{NS_WIN_SHELL}:Send" => {
      "#{NS_WIN_SHELL}:Stream" => {
        "@Name" => 'stdin',
        "@CommandId" => command_id,
        :content! => Base64.encode64(stdin)
      }
    }
  }
  builder = Builder::XmlMarkup.new
  builder.instruct!(:xml, :encoding => 'UTF-8')
  builder.tag! :env, :Envelope, namespaces do |env|
    env.tag!(:env, :Header) { |h| h << Gyoku.xml(merge_headers(header,resource_uri_cmd,action_send,selector_shell_id(shell_id))) }
    env.tag!(:env, :Body) do |env_body|
      env_body << Gyoku.xml(body)
    end
  end
  resp = send_message(builder.target!)
  true
end