winrm 1.4.0 → 1.5.0
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 +4 -4
- data/.rubocop.yml +1 -0
- data/.travis.yml +3 -1
- data/README.md +45 -5
- data/appveyor.yml +51 -0
- data/bin/rwinrm +0 -0
- data/changelog.md +9 -0
- data/lib/winrm/command_executor.rb +242 -0
- data/lib/winrm/http/transport.rb +40 -3
- data/lib/winrm/version.rb +1 -1
- data/lib/winrm/winrm_service.rb +51 -14
- data/spec/command_executor_spec.rb +440 -0
- data/spec/issue_59_spec.rb +4 -4
- data/spec/powershell_spec.rb +0 -6
- data/spec/spec_helper.rb +28 -2
- metadata +6 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ad05b5c1fe16cfa1586a6a001022d0463e87d9cb
|
4
|
+
data.tar.gz: 07a2d85d2a0a9458049eaac2b23bbd68e518ae2d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c2181da8210604d6703c2f694c3388e9e8f670fba9673a3fac68b1b9d257e2f0e3412f810b3c94966a57e7035e1d1b153ee815a7b993694c71885925d6bff09e
|
7
|
+
data.tar.gz: 47d0c26bfab5a7f096091bf74f042ded6a25fe59683c83829907863176f5bbb337bb6b972fcc4e2d3d5977c150c65c81413c6044a23b75787b82ae8738e1fb38
|
data/.rubocop.yml
CHANGED
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# Windows Remote Management (WinRM) for Ruby
|
2
2
|
[](https://travis-ci.org/WinRb/WinRM)
|
3
3
|
[](http://badge.fury.io/rb/winrm)
|
4
|
+
[](https://ci.appveyor.com/project/winrb/winrm)
|
4
5
|
|
5
6
|
This is a SOAP library that uses the functionality in Windows Remote
|
6
7
|
Management(WinRM) to call native object in Windows. This includes, but is
|
@@ -20,9 +21,11 @@ require 'winrm'
|
|
20
21
|
endpoint = 'http://mywinrmhost:5985/wsman'
|
21
22
|
krb5_realm = 'EXAMPLE.COM'
|
22
23
|
winrm = WinRM::WinRMWebService.new(endpoint, :kerberos, :realm => krb5_realm)
|
23
|
-
winrm.
|
24
|
-
|
25
|
-
|
24
|
+
winrm.create_executor do |executor|
|
25
|
+
executor.run_cmd('ipconfig /all') do |stdout, stderr|
|
26
|
+
STDOUT.print stdout
|
27
|
+
STDERR.print stderr
|
28
|
+
end
|
26
29
|
end
|
27
30
|
```
|
28
31
|
|
@@ -30,6 +33,11 @@ There are various connection types you can specify upon initialization:
|
|
30
33
|
|
31
34
|
It is recommended that you <code>:disable_sspi => true</code> if you are using the plaintext or ssl transport.
|
32
35
|
|
36
|
+
### Deprecated methods
|
37
|
+
As of version 1.5.0 `WinRM::WinRMWebService` methods `cmd`, `run_cmd`, `powershell`, and `run_powershell_script` have been deprecated and will be removed from the next major version of the WinRM gem.
|
38
|
+
|
39
|
+
Use the `run_cmd` and `run_powershell_script` of the `WinRM::CommandExecutor` class instead. The `CommandExecutor` allows multiple commands to be run from the same WinRM shell providing a significant performance improvement when issuing multiple calls.
|
40
|
+
|
33
41
|
#### Plaintext
|
34
42
|
```ruby
|
35
43
|
WinRM::WinRMWebService.new(endpoint, :plaintext, :user => myuser, :pass => mypass, :disable_sspi => true)
|
@@ -52,10 +60,13 @@ WinRM::WinRMWebService.new(endpoint, :ssl, :user => myuser, :pass => mypass, :ba
|
|
52
60
|
# Enabling no_ssl_peer_verification is not recommended. HTTPS connections are still encrypted,
|
53
61
|
# but the WinRM gem is not able to detect forged replies or man in the middle attacks.
|
54
62
|
WinRM::WinRMWebService.new(endpoint, :ssl, :user => myuser, :pass => mypass, :basic_auth_only => true, :no_ssl_peer_verification => true)
|
63
|
+
|
64
|
+
# Verify against a known fingerprint
|
65
|
+
WinRM::WinRMWebService.new(endpoint, :ssl, :user => myuser, :pass => mypass, :basic_auth_only => true, :ssl_peer_fingerprint => '6C04B1A997BA19454B0CD31C65D7020A6FC2669D')
|
55
66
|
```
|
56
67
|
|
57
68
|
##### Create a self signed cert for WinRM
|
58
|
-
You may want to create a self signed certificate for servicing https WinRM connections.
|
69
|
+
You may want to create a self signed certificate for servicing https WinRM connections. You can use the following PowerShell script to create a cert and enable the WinRM HTTPS listener. Unless you are running windows server 2012 R2 or later, you must install makecert.exe from the Windows SDK, otherwise use `New-SelfSignedCertificate`.
|
59
70
|
|
60
71
|
```powershell
|
61
72
|
$hostname = $Env:ComputerName
|
@@ -63,6 +74,10 @@ $hostname = $Env:ComputerName
|
|
63
74
|
C:\"Program Files"\"Microsoft SDKs"\Windows\v7.1\Bin\makecert.exe -r -pe -n "CN=$hostname,O=vagrant" -eku 1.3.6.1.5.5.7.3.1 -ss my -sr localMachine -sky exchange -sp "Microsoft RSA SChannel Cryptographic Provider" -sy 12 "$hostname.cer"
|
64
75
|
|
65
76
|
$thumbprint = (& ls cert:LocalMachine/my).Thumbprint
|
77
|
+
|
78
|
+
# Windows 2012R2 and above can use New-SelfSignedCertificate
|
79
|
+
$thumbprint = (New-SelfSignedCertificate -DnsName $hostname -CertStoreLocation cert:\LocalMachine\my).Thumbprint
|
80
|
+
|
66
81
|
$cmd = "winrm create winrm/config/Listener?Address=*+Transport=HTTPS '@{Hostname=`"$hostname`";CertificateThumbprint=`"$thumbprint`"}'"
|
67
82
|
iex $cmd
|
68
83
|
```
|
@@ -72,6 +87,29 @@ iex $cmd
|
|
72
87
|
WinRM::WinRMWebService.new(endpoint, :kerberos, :realm => 'MYREALM.COM')
|
73
88
|
```
|
74
89
|
|
90
|
+
## Retries and opening a shell
|
91
|
+
Especially if provisioning a new machine, it's possible the winrm service is not yet running when first attempting to connect. The `WinRMWebService` accepts the options `:retry_limit` and `:retry_delay` to specify the maximum number of attempts to make and how long to wait in between. These default to 3 attempts and a 10 second delay.
|
92
|
+
```ruby
|
93
|
+
WinRM::WinRMWebService.new(endpoint, :ssl, :user => myuser, :pass => mypass, :retry_limit => 30, :retry_delay => 10)
|
94
|
+
```
|
95
|
+
|
96
|
+
## Logging
|
97
|
+
The `WinRMWebService` exposes a `logger` attribute and uses the [logging](https://rubygems.org/gems/logging) gem to manage logging behavior. By default this appends to `STDOUT` and has a level of `:warn`, but one can adjust the level or add additional appenders.
|
98
|
+
```ruby
|
99
|
+
winrm = WinRM::WinRMWebService.new(endpoint, :ssl, :user => myuser, :pass => mypass)
|
100
|
+
|
101
|
+
# suppress warnings
|
102
|
+
winrm.logger.warn = :error
|
103
|
+
|
104
|
+
# Log to a file
|
105
|
+
winrm.logger.add_appenders(Logging.appenders.file('error.log'))
|
106
|
+
```
|
107
|
+
|
108
|
+
If a consuming application uses its own logger that complies to the logging API, you can simply swap it in:
|
109
|
+
```ruby
|
110
|
+
winrm.logger = my_logger
|
111
|
+
```
|
112
|
+
|
75
113
|
## Troubleshooting
|
76
114
|
You may have some errors like ```WinRM::WinRMAuthorizationError```.
|
77
115
|
You can run the following commands on the server to try to solve the problem:
|
@@ -82,6 +120,8 @@ winrm set winrm/config/service @{AllowUnencrypted="true"}
|
|
82
120
|
```
|
83
121
|
You can read more about that on issue [#29](https://github.com/WinRb/WinRM/issues/29)
|
84
122
|
|
123
|
+
Also see [this post](http://www.hurryupandwait.io/blog/understanding-and-troubleshooting-winrm-connection-and-authentication-a-thrill-seekers-guide-to-adventure) for more general tips related to winrm connection and authentication issues.
|
124
|
+
|
85
125
|
|
86
126
|
## Current features
|
87
127
|
|
@@ -128,7 +168,7 @@ Once you have the dependencies, you can run the unit tests with `rake`:
|
|
128
168
|
$ bundle exec rake spec
|
129
169
|
```
|
130
170
|
|
131
|
-
To run the integration tests you will need a Windows box with the WinRM service properly configured. Its easiest to use a Vagrant Windows box.
|
171
|
+
To run the integration tests you will need a Windows box with the WinRM service properly configured. Its easiest to use a Vagrant Windows box (mwrock/Windows2012R2 is public on [atlas](https://atlas.hashicorp.com/mwrock/boxes/Windows2012R2) with an evaluation version of Windows 2012 R2).
|
132
172
|
|
133
173
|
1. Create a Windows VM with WinRM configured (see above).
|
134
174
|
2. Copy the config-example.yml to config.yml - edit this file with your WinRM connection details.
|
data/appveyor.yml
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
version: "master-{build}"
|
2
|
+
|
3
|
+
os: Windows Server 2012 R2
|
4
|
+
platform:
|
5
|
+
- x64
|
6
|
+
|
7
|
+
environment:
|
8
|
+
winrm_user: test_user
|
9
|
+
winrm_pass: Pass@word1
|
10
|
+
|
11
|
+
matrix:
|
12
|
+
- ruby_version: "21"
|
13
|
+
winrm_endpoint: http://localhost:5985/wsman
|
14
|
+
|
15
|
+
- ruby_version: "21"
|
16
|
+
winrm_auth_type: ssl
|
17
|
+
winrm_endpoint: https://localhost:5986/wsman
|
18
|
+
winrm_no_ssl_peer_verification: true
|
19
|
+
|
20
|
+
- ruby_version: "21"
|
21
|
+
winrm_auth_type: ssl
|
22
|
+
winrm_endpoint: https://localhost:5986/wsman
|
23
|
+
use_ssl_peer_fingerprint: true
|
24
|
+
|
25
|
+
clone_folder: c:\projects\winrm
|
26
|
+
clone_depth: 1
|
27
|
+
branches:
|
28
|
+
only:
|
29
|
+
- master
|
30
|
+
|
31
|
+
install:
|
32
|
+
- ps: net user /add $env:winrm_user $env:winrm_pass
|
33
|
+
- ps: net localgroup administrators $env:winrm_user /add
|
34
|
+
- ps: $env:winrm_cert = (New-SelfSignedCertificate -DnsName localhost -CertStoreLocation cert:\localmachine\my).Thumbprint
|
35
|
+
- ps: winrm create winrm/config/Listener?Address=*+Transport=HTTPS "@{Hostname=`"localhost`";CertificateThumbprint=`"$($env:winrm_cert)`"}"
|
36
|
+
- ps: winrm set winrm/config/client/auth '@{Basic="true"}'
|
37
|
+
- ps: winrm set winrm/config/service/auth '@{Basic="true"}'
|
38
|
+
- ps: winrm set winrm/config/service '@{AllowUnencrypted="true"}'
|
39
|
+
- ps: $env:PATH="C:\Ruby$env:ruby_version\bin;$env:PATH"
|
40
|
+
- ps: Write-Host $env:PATH
|
41
|
+
- ps: ruby --version
|
42
|
+
- ps: gem --version
|
43
|
+
- ps: gem install bundler --quiet --no-ri --no-rdoc
|
44
|
+
- ps: bundler --version
|
45
|
+
|
46
|
+
build_script:
|
47
|
+
- bundle install || bundle install || bundle install
|
48
|
+
|
49
|
+
test_script:
|
50
|
+
- SET SPEC_OPTS=--format progress
|
51
|
+
- bundle exec rake integration
|
data/bin/rwinrm
CHANGED
File without changes
|
data/changelog.md
CHANGED
@@ -1,5 +1,14 @@
|
|
1
1
|
# WinRM Gem Changelog
|
2
2
|
|
3
|
+
# 1.5.0
|
4
|
+
- Deprecating `WinRM::WinRMWebService` methods `cmd`, `run_cmd`, `powershell`, and `run_powershell_script` in favor of the `run_cmd` and `run_powershell_script` methods of the `WinRM::CommandExecutor` class. The `CommandExecutor` allows multiple commands to be run from the same WinRM shell providing a significant performance improvement when issuing multiple calls.
|
5
|
+
- Added an `:ssl_peer_fingerprint` option to be used instead of `:no_ssl_peer_verification` and allows a specific certificate to be verified.
|
6
|
+
- Opening a winrm shell is retriable with configurable delay and retry limit.
|
7
|
+
- Logging apends to `stdout` by default and can be replaced with a logger from a consuming application.
|
8
|
+
|
9
|
+
# 1.4.0
|
10
|
+
- Added WinRM::Version so the gem version is available at runtime for consumers.
|
11
|
+
|
3
12
|
# 1.3.6
|
4
13
|
- Remove BOM from response (Issue #159) added by Windows 2008R2
|
5
14
|
|
@@ -0,0 +1,242 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
#
|
3
|
+
# Copyright 2015 Shawn Neal <sneal@sneal.net>
|
4
|
+
# Copyright 2015 Matt Wrock <matt@mattwrock.com>
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
|
18
|
+
module WinRM
|
19
|
+
# Object which can execute multiple commands and Powershell scripts in
|
20
|
+
# one shared remote shell session. The maximum number of commands per
|
21
|
+
# shell is determined by interrogating the remote host when the session
|
22
|
+
# is opened and the remote shell is automatically recycled before the
|
23
|
+
# threshold is reached.
|
24
|
+
#
|
25
|
+
# @author Shawn Neal <sneal@sneal.net>
|
26
|
+
# @author Matt Wrock <matt@mattwrock.com>
|
27
|
+
# @author Fletcher Nichol <fnichol@nichol.ca>
|
28
|
+
class CommandExecutor
|
29
|
+
# Closes an open remote shell session left open
|
30
|
+
# after a command executor is garbage collecyted.
|
31
|
+
#
|
32
|
+
# @param shell_id [String] the remote shell identifier
|
33
|
+
# @param service [WinRM::WinRMWebService] a winrm web service object
|
34
|
+
def self.finalize(shell_id, service)
|
35
|
+
proc { service.close_shell(shell_id) }
|
36
|
+
end
|
37
|
+
|
38
|
+
# @return [Integer,nil] the safe maximum number of commands that can
|
39
|
+
# be executed in one remote shell session, or `nil` if the
|
40
|
+
# threshold has not yet been determined
|
41
|
+
attr_reader :max_commands
|
42
|
+
|
43
|
+
# @return [WinRM::WinRMWebService] a WinRM web service object
|
44
|
+
attr_reader :service
|
45
|
+
|
46
|
+
# @return [String,nil] the identifier for the current open remote
|
47
|
+
# shell session, or `nil` if the session is not open
|
48
|
+
attr_reader :shell
|
49
|
+
|
50
|
+
# Creates a CommandExecutor given a `WinRM::WinRMWebService` object.
|
51
|
+
#
|
52
|
+
# @param service [WinRM::WinRMWebService] a winrm web service object
|
53
|
+
# responds to `#debug` and `#info` (default: `nil`)
|
54
|
+
def initialize(service)
|
55
|
+
@service = service
|
56
|
+
@logger = service.logger
|
57
|
+
@command_count = 0
|
58
|
+
end
|
59
|
+
|
60
|
+
# Closes the open remote shell session. This method can be called
|
61
|
+
# multiple times, even if there is no open session.
|
62
|
+
def close
|
63
|
+
return if shell.nil?
|
64
|
+
|
65
|
+
service.close_shell(shell)
|
66
|
+
remove_finalizer
|
67
|
+
@shell = nil
|
68
|
+
end
|
69
|
+
|
70
|
+
# Opens a remote shell session for reuse. The maxiumum
|
71
|
+
# command-per-shell threshold is also determined the first time this
|
72
|
+
# method is invoked and cached for later invocations.
|
73
|
+
#
|
74
|
+
# @return [String] the remote shell session indentifier
|
75
|
+
def open
|
76
|
+
close
|
77
|
+
retryable(service.retry_limit, service.retry_delay) { @shell = service.open_shell }
|
78
|
+
add_finalizer(shell)
|
79
|
+
@command_count = 0
|
80
|
+
determine_max_commands unless max_commands
|
81
|
+
shell
|
82
|
+
end
|
83
|
+
|
84
|
+
# Runs a CMD command.
|
85
|
+
#
|
86
|
+
# @param command [String] the command to run on the remote system
|
87
|
+
# @param arguments [Array<String>] arguments to the command
|
88
|
+
# @yield [stdout, stderr] yields more live access the standard
|
89
|
+
# output and standard error streams as they are returns, if
|
90
|
+
# streaming behavior is desired
|
91
|
+
# @return [WinRM::Output] output object with stdout, stderr, and
|
92
|
+
# exit code
|
93
|
+
def run_cmd(command, arguments = [], &block)
|
94
|
+
reset if command_count_exceeded?
|
95
|
+
ensure_open_shell!
|
96
|
+
|
97
|
+
@command_count += 1
|
98
|
+
result = nil
|
99
|
+
service.run_command(shell, command, arguments) do |command_id|
|
100
|
+
result = service.get_command_output(shell, command_id, &block)
|
101
|
+
end
|
102
|
+
result
|
103
|
+
end
|
104
|
+
|
105
|
+
# Run a Powershell script that resides on the local box.
|
106
|
+
#
|
107
|
+
# @param script_file [IO,String] an IO reference for reading the
|
108
|
+
# Powershell script or the actual file contents
|
109
|
+
# @yield [stdout, stderr] yields more live access the standard
|
110
|
+
# output and standard error streams as they are returns, if
|
111
|
+
# streaming behavior is desired
|
112
|
+
# @return [WinRM::Output] output object with stdout, stderr, and
|
113
|
+
# exit code
|
114
|
+
def run_powershell_script(script_file, &block)
|
115
|
+
# this code looks overly compact in an attempt to limit local
|
116
|
+
# variable assignments that may contain large strings and
|
117
|
+
# consequently bloat the Ruby VM
|
118
|
+
run_cmd(
|
119
|
+
'powershell',
|
120
|
+
[
|
121
|
+
'-encodedCommand',
|
122
|
+
::WinRM::PowershellScript.new(
|
123
|
+
safe_script(script_file.is_a?(IO) ? script_file.read : script_file)
|
124
|
+
).encoded
|
125
|
+
],
|
126
|
+
&block
|
127
|
+
)
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
|
132
|
+
# @return [Integer] the default maximum number of commands which can be
|
133
|
+
# executed in one remote shell session on "older" versions of Windows
|
134
|
+
# @api private
|
135
|
+
LEGACY_LIMIT = 15
|
136
|
+
|
137
|
+
# @return [Integer] the default maximum number of commands which can be
|
138
|
+
# executed in one remote shell session on "modern" versions of Windows
|
139
|
+
# @api private
|
140
|
+
MODERN_LIMIT = 1500
|
141
|
+
|
142
|
+
# @return [String] the PowerShell command used to determine the version
|
143
|
+
# of Windows
|
144
|
+
# @api private
|
145
|
+
PS1_OS_VERSION = '[environment]::OSVersion.Version.tostring()'.freeze
|
146
|
+
|
147
|
+
# @return [Integer] the number of executed commands on the remote
|
148
|
+
# shell session
|
149
|
+
# @api private
|
150
|
+
attr_accessor :command_count
|
151
|
+
|
152
|
+
# @return [#debug,#info] the logger
|
153
|
+
# @api private
|
154
|
+
attr_reader :logger
|
155
|
+
|
156
|
+
# Creates a finalizer for this connection which will close the open
|
157
|
+
# remote shell session when the object is garabage collected or on
|
158
|
+
# Ruby VM shutdown.
|
159
|
+
#
|
160
|
+
# @param shell_id [String] the remote shell identifier
|
161
|
+
# @api private
|
162
|
+
def add_finalizer(shell_id)
|
163
|
+
ObjectSpace.define_finalizer(self, self.class.finalize(shell_id, service))
|
164
|
+
end
|
165
|
+
|
166
|
+
# @return [true,false] whether or not the number of exeecuted commands
|
167
|
+
# have exceeded the maxiumum threshold
|
168
|
+
# @api private
|
169
|
+
def command_count_exceeded?
|
170
|
+
command_count > max_commands.to_i
|
171
|
+
end
|
172
|
+
|
173
|
+
# Ensures that there is an open remote shell session.
|
174
|
+
#
|
175
|
+
# @raise [WinRM::WinRMError] if there is no open shell
|
176
|
+
# @api private
|
177
|
+
def ensure_open_shell!
|
178
|
+
fail ::WinRM::WinRMError, "#{self.class}#open must be called " \
|
179
|
+
'before any run methods are invoked' if shell.nil?
|
180
|
+
end
|
181
|
+
|
182
|
+
# Determines the safe maximum number of commands that can be executed
|
183
|
+
# on a remote shell session by interrogating the remote host.
|
184
|
+
#
|
185
|
+
# @api private
|
186
|
+
def determine_max_commands
|
187
|
+
os_version = run_powershell_script(PS1_OS_VERSION).stdout.chomp
|
188
|
+
@max_commands = os_version < '6.2' ? LEGACY_LIMIT : MODERN_LIMIT
|
189
|
+
@max_commands -= 2 # to be safe
|
190
|
+
end
|
191
|
+
|
192
|
+
# Removes any finalizers for this connection.
|
193
|
+
#
|
194
|
+
# @api private
|
195
|
+
def remove_finalizer
|
196
|
+
ObjectSpace.undefine_finalizer(self)
|
197
|
+
end
|
198
|
+
|
199
|
+
# Closes the remote shell session and opens a new one.
|
200
|
+
#
|
201
|
+
# @api private
|
202
|
+
def reset
|
203
|
+
logger.debug("Resetting WinRM shell (Max command limit is #{max_commands})")
|
204
|
+
open
|
205
|
+
end
|
206
|
+
|
207
|
+
# Yields to a block and reties the block if certain rescuable
|
208
|
+
# exceptions are raised.
|
209
|
+
#
|
210
|
+
# @param retries [Integer] the number of times to retry before failing
|
211
|
+
# @option delay [Float] the number of seconds to wait until
|
212
|
+
# attempting a retry
|
213
|
+
# @api private
|
214
|
+
def retryable(retries, delay)
|
215
|
+
yield
|
216
|
+
rescue *RESCUE_EXCEPTIONS_ON_ESTABLISH.call => e
|
217
|
+
if (retries -= 1) > 0
|
218
|
+
logger.info("[WinRM] connection failed. retrying in #{delay} seconds (#{e.inspect})")
|
219
|
+
sleep(delay)
|
220
|
+
retry
|
221
|
+
else
|
222
|
+
logger.warn("[WinRM] connection failed, terminating (#{e.inspect})")
|
223
|
+
raise
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
RESCUE_EXCEPTIONS_ON_ESTABLISH = lambda do
|
228
|
+
[
|
229
|
+
Errno::EACCES, Errno::EADDRINUSE, Errno::ECONNREFUSED,
|
230
|
+
Errno::ECONNRESET, Errno::ENETUNREACH, Errno::EHOSTUNREACH,
|
231
|
+
::WinRM::WinRMHTTPTransportError, ::WinRM::WinRMAuthorizationError,
|
232
|
+
HTTPClient::KeepAliveDisconnected,
|
233
|
+
HTTPClient::ConnectTimeoutError
|
234
|
+
].freeze
|
235
|
+
end
|
236
|
+
|
237
|
+
# suppress the progress stream from leaking to stderr
|
238
|
+
def safe_script(script)
|
239
|
+
"$ProgressPreference='SilentlyContinue';" + script
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
data/lib/winrm/http/transport.rb
CHANGED
@@ -40,12 +40,13 @@ module WinRM
|
|
40
40
|
# @param [String] The XML SOAP message
|
41
41
|
# @returns [REXML::Document] The parsed response body
|
42
42
|
def send_request(message)
|
43
|
+
ssl_peer_fingerprint_verification!
|
43
44
|
log_soap_message(message)
|
44
|
-
hdr = {
|
45
|
-
|
46
|
-
'Content-Length' => message.length }
|
45
|
+
hdr = { 'Content-Type' => 'application/soap+xml;charset=UTF-8',
|
46
|
+
'Content-Length' => message.length }
|
47
47
|
resp = @httpcli.post(@endpoint, message, hdr)
|
48
48
|
log_soap_message(resp.http_body.content)
|
49
|
+
verify_ssl_fingerprint(resp.peer_cert)
|
49
50
|
handler = WinRM::ResponseHandler.new(resp.http_body.content, resp.status)
|
50
51
|
handler.parse_to_xml
|
51
52
|
end
|
@@ -67,6 +68,41 @@ module WinRM
|
|
67
68
|
@httpcli.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
68
69
|
end
|
69
70
|
|
71
|
+
# SSL Peer Fingerprint Verification prior to connecting
|
72
|
+
def ssl_peer_fingerprint_verification!
|
73
|
+
return unless @ssl_peer_fingerprint && ! @ssl_peer_fingerprint_verified
|
74
|
+
|
75
|
+
with_untrusted_ssl_connection do |connection|
|
76
|
+
connection_cert = connection.peer_cert_chain.last
|
77
|
+
verify_ssl_fingerprint(connection_cert)
|
78
|
+
end
|
79
|
+
@logger.info("initial ssl fingerprint #{@ssl_peer_fingerprint} verified\n")
|
80
|
+
@ssl_peer_fingerprint_verified = true
|
81
|
+
no_ssl_peer_verification!
|
82
|
+
end
|
83
|
+
|
84
|
+
# Connect without verification to retrieve untrusted ssl context
|
85
|
+
def with_untrusted_ssl_connection
|
86
|
+
noverify_peer_context = OpenSSL::SSL::SSLContext.new
|
87
|
+
noverify_peer_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
88
|
+
tcp_connection = TCPSocket.new(@endpoint.host, @endpoint.port)
|
89
|
+
begin
|
90
|
+
ssl_connection = OpenSSL::SSL::SSLSocket.new(tcp_connection, noverify_peer_context)
|
91
|
+
ssl_connection.connect
|
92
|
+
yield ssl_connection
|
93
|
+
ensure
|
94
|
+
tcp_connection.close
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# compare @ssl_peer_fingerprint to current ssl context
|
99
|
+
def verify_ssl_fingerprint(cert)
|
100
|
+
return unless @ssl_peer_fingerprint
|
101
|
+
conn_fingerprint = OpenSSL::Digest::SHA1.new(cert.to_der).to_s
|
102
|
+
return unless @ssl_peer_fingerprint.casecmp(conn_fingerprint) != 0
|
103
|
+
fail "ssl fingerprint mismatch!!!!\n"
|
104
|
+
end
|
105
|
+
|
70
106
|
# HTTP Client receive timeout. How long should a remote call wait for a
|
71
107
|
# for a response from WinRM?
|
72
108
|
def receive_timeout=(sec)
|
@@ -112,6 +148,7 @@ module WinRM
|
|
112
148
|
no_sspi_auth! if opts[:disable_sspi]
|
113
149
|
basic_auth_only! if opts[:basic_auth_only]
|
114
150
|
no_ssl_peer_verification! if opts[:no_ssl_peer_verification]
|
151
|
+
@ssl_peer_fingerprint = opts[:ssl_peer_fingerprint]
|
115
152
|
end
|
116
153
|
end
|
117
154
|
|
data/lib/winrm/version.rb
CHANGED
data/lib/winrm/winrm_service.rb
CHANGED
@@ -16,6 +16,7 @@
|
|
16
16
|
|
17
17
|
require 'nori'
|
18
18
|
require 'rexml/document'
|
19
|
+
require 'winrm/command_executor'
|
19
20
|
require_relative 'helpers/powershell_script'
|
20
21
|
|
21
22
|
module WinRM
|
@@ -26,7 +27,9 @@ module WinRM
|
|
26
27
|
DEFAULT_MAX_ENV_SIZE = 153600
|
27
28
|
DEFAULT_LOCALE = 'en-US'
|
28
29
|
|
29
|
-
attr_reader :endpoint, :timeout
|
30
|
+
attr_reader :endpoint, :timeout, :retry_limit, :retry_delay
|
31
|
+
|
32
|
+
attr_accessor :logger
|
30
33
|
|
31
34
|
# @param [String,URI] endpoint the WinRM webservice endpoint
|
32
35
|
# @param [Symbol] transport either :kerberos(default)/:ssl/:plaintext
|
@@ -39,7 +42,8 @@ module WinRM
|
|
39
42
|
@timeout = DEFAULT_TIMEOUT
|
40
43
|
@max_env_sz = DEFAULT_MAX_ENV_SIZE
|
41
44
|
@locale = DEFAULT_LOCALE
|
42
|
-
|
45
|
+
setup_logger
|
46
|
+
configure_retries(opts)
|
43
47
|
case transport
|
44
48
|
when :kerberos
|
45
49
|
require 'gssapi'
|
@@ -94,6 +98,7 @@ module WinRM
|
|
94
98
|
# :env_vars => {:myvar1 => 'val1', :myvar2 => 'var2'}
|
95
99
|
# @return [String] The ShellId from the SOAP response. This is our open shell instance on the remote machine.
|
96
100
|
def open_shell(shell_opts = {}, &block)
|
101
|
+
logger.debug("[WinRM] opening remote shell on #{@endpoint}")
|
97
102
|
i_stream = shell_opts.has_key?(:i_stream) ? shell_opts[:i_stream] : 'stdin'
|
98
103
|
o_stream = shell_opts.has_key?(:o_stream) ? shell_opts[:o_stream] : 'stdout stderr'
|
99
104
|
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)
|
@@ -125,6 +130,7 @@ module WinRM
|
|
125
130
|
|
126
131
|
resp_doc = send_message(builder.target!)
|
127
132
|
shell_id = REXML::XPath.first(resp_doc, "//*[@Name='ShellId']").text
|
133
|
+
logger.debug("[WinRM] remote shell #{shell_id} is open on #{@endpoint}")
|
128
134
|
|
129
135
|
if block_given?
|
130
136
|
begin
|
@@ -279,6 +285,7 @@ module WinRM
|
|
279
285
|
# @param [String] shell_id The shell id on the remote machine. See #open_shell
|
280
286
|
# @return [true] This should have more error checking but it just returns true for now.
|
281
287
|
def close_shell(shell_id)
|
288
|
+
logger.debug("[WinRM] closing remote shell #{shell_id} on #{@endpoint}")
|
282
289
|
builder = Builder::XmlMarkup.new
|
283
290
|
builder.instruct!(:xml, :encoding => 'UTF-8')
|
284
291
|
|
@@ -288,38 +295,57 @@ module WinRM
|
|
288
295
|
end
|
289
296
|
|
290
297
|
resp = send_message(builder.target!)
|
298
|
+
logger.debug("[WinRM] remote shell #{shell_id} closed")
|
291
299
|
true
|
292
300
|
end
|
293
301
|
|
302
|
+
# DEPRECATED: Use WinRM::CommandExecutor#run_cmd instead
|
294
303
|
# Run a CMD command
|
295
304
|
# @param [String] command The command to run on the remote system
|
296
305
|
# @param [Array <String>] arguments arguments to the command
|
297
306
|
# @param [String] an existing and open shell id to reuse
|
298
307
|
# @return [Hash] :stdout and :stderr
|
299
308
|
def run_cmd(command, arguments = [], &block)
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
command_output = get_command_output(shell_id, command_id, &block)
|
304
|
-
end
|
309
|
+
logger.warn("WinRM::WinRMWebService#run_cmd is deprecated. Use WinRM::CommandExecutor#run_cmd instead")
|
310
|
+
create_executor do |executor|
|
311
|
+
executor.run_cmd(command, arguments, &block)
|
305
312
|
end
|
306
|
-
command_output
|
307
313
|
end
|
308
314
|
alias :cmd :run_cmd
|
309
315
|
|
310
|
-
|
316
|
+
# DEPRECATED: Use WinRM::CommandExecutor#run_powershell_script instead
|
311
317
|
# Run a Powershell script that resides on the local box.
|
312
318
|
# @param [IO,String] script_file an IO reference for reading the Powershell script or the actual file contents
|
313
319
|
# @param [String] an existing and open shell id to reuse
|
314
320
|
# @return [Hash] :stdout and :stderr
|
315
321
|
def run_powershell_script(script_file, &block)
|
316
|
-
#
|
317
|
-
|
318
|
-
|
319
|
-
|
322
|
+
logger.warn("WinRM::WinRMWebService#run_powershell_script is deprecated. Use WinRM::CommandExecutor#run_cmd instead")
|
323
|
+
create_executor do |executor|
|
324
|
+
executor.run_powershell_script(script_file, &block)
|
325
|
+
end
|
320
326
|
end
|
321
327
|
alias :powershell :run_powershell_script
|
322
328
|
|
329
|
+
# Creates a CommandExecutor initialized with this WinRMWebService
|
330
|
+
# If called with a block, create_executor yields an executor and
|
331
|
+
# ensures that the executor is closed after the block completes.
|
332
|
+
# The CommandExecutor is simply returned if no block is given.
|
333
|
+
# @yieldparam [CommandExecutor] a CommandExecutor instance
|
334
|
+
# @return [CommandExecutor] a CommandExecutor instance
|
335
|
+
def create_executor(&block)
|
336
|
+
executor = CommandExecutor.new(self)
|
337
|
+
executor.open
|
338
|
+
|
339
|
+
if block_given?
|
340
|
+
begin
|
341
|
+
yield executor
|
342
|
+
ensure
|
343
|
+
executor.close
|
344
|
+
end
|
345
|
+
else
|
346
|
+
executor
|
347
|
+
end
|
348
|
+
end
|
323
349
|
|
324
350
|
# Run a WQL Query
|
325
351
|
# @see http://msdn.microsoft.com/en-us/library/aa394606(VS.85).aspx
|
@@ -362,12 +388,23 @@ module WinRM
|
|
362
388
|
alias :wql :run_wql
|
363
389
|
|
364
390
|
def toggle_nori_type_casting(to)
|
365
|
-
|
391
|
+
logger.warn('toggle_nori_type_casting is deprecated and has no effect, ' +
|
366
392
|
'please remove calls to it')
|
367
393
|
end
|
368
394
|
|
369
395
|
private
|
370
396
|
|
397
|
+
def setup_logger
|
398
|
+
@logger = Logging.logger[self]
|
399
|
+
@logger.level = :warn
|
400
|
+
@logger.add_appenders(Logging.appenders.stdout)
|
401
|
+
end
|
402
|
+
|
403
|
+
def configure_retries(opts)
|
404
|
+
@retry_delay = opts[:retry_delay] || 10
|
405
|
+
@retry_limit = opts[:retry_limit] || 3
|
406
|
+
end
|
407
|
+
|
371
408
|
def namespaces
|
372
409
|
{
|
373
410
|
'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema',
|
@@ -0,0 +1,440 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
#
|
3
|
+
# Author:: Fletcher (<fnichol@nichol.ca>)
|
4
|
+
#
|
5
|
+
# Copyright (C) 2015, Fletcher Nichol
|
6
|
+
#
|
7
|
+
# Licensed under the Apache License, Version 2.0 (the 'License');
|
8
|
+
# you may not use this file except in compliance with the License.
|
9
|
+
# You may obtain a copy of the License at
|
10
|
+
#
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
12
|
+
#
|
13
|
+
# Unless required by applicable law or agreed to in writing, software
|
14
|
+
# distributed under the License is distributed on an 'AS IS' BASIS,
|
15
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
16
|
+
# See the License for the specific language governing permissions and
|
17
|
+
# limitations under the License.
|
18
|
+
|
19
|
+
require 'winrm/command_executor'
|
20
|
+
|
21
|
+
require 'base64'
|
22
|
+
require 'securerandom'
|
23
|
+
|
24
|
+
describe WinRM::CommandExecutor, unit: true do
|
25
|
+
let(:logged_output) { StringIO.new }
|
26
|
+
let(:shell_id) { 'shell-123' }
|
27
|
+
let(:executor_args) { [service, logger] }
|
28
|
+
let(:executor) { WinRM::CommandExecutor.new(service) }
|
29
|
+
let(:service) do
|
30
|
+
double(
|
31
|
+
'winrm_service',
|
32
|
+
logger: Logging.logger['test'],
|
33
|
+
retry_limit: 1,
|
34
|
+
retry_delay: 1
|
35
|
+
)
|
36
|
+
end
|
37
|
+
|
38
|
+
let(:version_output) do
|
39
|
+
o = ::WinRM::Output.new
|
40
|
+
o[:exitcode] = 0
|
41
|
+
o[:data].concat([{ stdout: '6.3.9600.0\r\n' }])
|
42
|
+
o
|
43
|
+
end
|
44
|
+
|
45
|
+
before do
|
46
|
+
allow(service).to receive(:open_shell).and_return(shell_id)
|
47
|
+
|
48
|
+
stub_powershell_script(
|
49
|
+
shell_id,
|
50
|
+
"$ProgressPreference='SilentlyContinue';[environment]::OSVersion.Version.tostring()",
|
51
|
+
version_output
|
52
|
+
)
|
53
|
+
end
|
54
|
+
|
55
|
+
describe '#close' do
|
56
|
+
it 'calls service#close_shell' do
|
57
|
+
executor.open
|
58
|
+
expect(service).to receive(:close_shell).with(shell_id)
|
59
|
+
|
60
|
+
executor.close
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'only calls service#close_shell once for multiple calls' do
|
64
|
+
executor.open
|
65
|
+
expect(service).to receive(:close_shell).with(shell_id).once
|
66
|
+
|
67
|
+
executor.close
|
68
|
+
executor.close
|
69
|
+
executor.close
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'undefines finalizer' do
|
73
|
+
allow(service).to receive(:close_shell)
|
74
|
+
allow(ObjectSpace).to receive(:define_finalizer) { |e, _| e == executor }
|
75
|
+
expect(ObjectSpace).to receive(:undefine_finalizer).with(executor)
|
76
|
+
executor.open
|
77
|
+
|
78
|
+
executor.close
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
describe '#open' do
|
83
|
+
it 'calls service#open_shell' do
|
84
|
+
expect(service).to receive(:open_shell).and_return(shell_id)
|
85
|
+
|
86
|
+
executor.open
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'defines a finalizer' do
|
90
|
+
expect(ObjectSpace).to receive(:define_finalizer) do |e, _|
|
91
|
+
expect(e).to eq(executor)
|
92
|
+
end
|
93
|
+
|
94
|
+
executor.open
|
95
|
+
end
|
96
|
+
|
97
|
+
it 'returns a shell id as a string' do
|
98
|
+
expect(executor.open).to eq shell_id
|
99
|
+
end
|
100
|
+
|
101
|
+
describe 'failed connection attempts' do
|
102
|
+
let(:error) { HTTPClient::ConnectTimeoutError }
|
103
|
+
let(:limit) { 3 }
|
104
|
+
let(:delay) { 0.1 }
|
105
|
+
|
106
|
+
before do
|
107
|
+
allow(service).to receive(:open_shell).and_raise(error)
|
108
|
+
allow(service).to receive(:retry_delay).and_return(delay)
|
109
|
+
allow(service).to receive(:retry_limit).and_return(limit)
|
110
|
+
end
|
111
|
+
|
112
|
+
it 'attempts to connect :retry_limit times' do
|
113
|
+
begin
|
114
|
+
allow(service).to receive(:open_shell).exactly.times(limit)
|
115
|
+
executor.open
|
116
|
+
rescue # rubocop:disable Lint/HandleExceptions
|
117
|
+
# the raise is not what is being tested here, rather its side-effect
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
it 'raises the inner error after retries' do
|
122
|
+
expect { executor.open }.to raise_error(error)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
describe 'for modern windows distributions' do
|
127
|
+
let(:version_output) do
|
128
|
+
o = ::WinRM::Output.new
|
129
|
+
o[:exitcode] = 0
|
130
|
+
o[:data].concat([{ stdout: '6.3.9600.0\r\n' }])
|
131
|
+
o
|
132
|
+
end
|
133
|
+
|
134
|
+
it 'sets #max_commands to 1500 - 2' do
|
135
|
+
expect(executor.max_commands).to eq nil
|
136
|
+
executor.open
|
137
|
+
|
138
|
+
expect(executor.max_commands).to eq(1500 - 2)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
describe 'for older/legacy windows distributions' do
|
143
|
+
let(:version_output) do
|
144
|
+
o = ::WinRM::Output.new
|
145
|
+
o[:exitcode] = 0
|
146
|
+
o[:data].concat([{ stdout: '6.1.8500.0\r\n' }])
|
147
|
+
o
|
148
|
+
end
|
149
|
+
|
150
|
+
it 'sets #max_commands to 15 - 2' do
|
151
|
+
expect(executor.max_commands).to eq nil
|
152
|
+
executor.open
|
153
|
+
|
154
|
+
expect(executor.max_commands).to eq(15 - 2)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
describe '#run_cmd' do
|
160
|
+
describe 'when #open has not been previously called' do
|
161
|
+
it 'raises a WinRMError error' do
|
162
|
+
expect { executor.run_cmd('nope') }.to raise_error(
|
163
|
+
::WinRM::WinRMError,
|
164
|
+
"#{executor.class}#open must be called before any run methods are invoked"
|
165
|
+
)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
describe 'when #open has been previously called' do
|
170
|
+
let(:command_id) { 'command-123' }
|
171
|
+
|
172
|
+
let(:echo_output) do
|
173
|
+
o = ::WinRM::Output.new
|
174
|
+
o[:exitcode] = 0
|
175
|
+
o[:data].concat([
|
176
|
+
{ stdout: 'Hello\r\n' },
|
177
|
+
{ stderr: 'Psst\r\n' }
|
178
|
+
])
|
179
|
+
o
|
180
|
+
end
|
181
|
+
|
182
|
+
before do
|
183
|
+
stub_cmd(shell_id, 'echo', ['Hello'], echo_output, command_id)
|
184
|
+
|
185
|
+
executor.open
|
186
|
+
end
|
187
|
+
|
188
|
+
it 'calls service#run_command' do
|
189
|
+
expect(service).to receive(:run_command).with(shell_id, 'echo', ['Hello'])
|
190
|
+
|
191
|
+
executor.run_cmd('echo', ['Hello'])
|
192
|
+
end
|
193
|
+
|
194
|
+
it 'calls service#get_command_output to get results' do
|
195
|
+
expect(service).to receive(:get_command_output).with(shell_id, command_id)
|
196
|
+
|
197
|
+
executor.run_cmd('echo', ['Hello'])
|
198
|
+
end
|
199
|
+
|
200
|
+
it 'calls service#get_command_output with a block to get results' do
|
201
|
+
blk = proc { |_, _| 'something' }
|
202
|
+
expect(service).to receive(:get_command_output).with(shell_id, command_id, &blk)
|
203
|
+
|
204
|
+
executor.run_cmd('echo', ['Hello'], &blk)
|
205
|
+
end
|
206
|
+
|
207
|
+
it 'returns an Output object hash' do
|
208
|
+
expect(executor.run_cmd('echo', ['Hello'])).to eq echo_output
|
209
|
+
end
|
210
|
+
|
211
|
+
it 'runs the block in #get_command_output when given' do
|
212
|
+
io_out = StringIO.new
|
213
|
+
io_err = StringIO.new
|
214
|
+
stub_cmd(
|
215
|
+
shell_id,
|
216
|
+
'echo',
|
217
|
+
['Hello'],
|
218
|
+
echo_output,
|
219
|
+
command_id
|
220
|
+
).and_yield(echo_output.stdout, echo_output.stderr)
|
221
|
+
output = executor.run_cmd('echo', ['Hello']) do |stdout, stderr|
|
222
|
+
io_out << stdout if stdout
|
223
|
+
io_err << stderr if stderr
|
224
|
+
end
|
225
|
+
|
226
|
+
expect(io_out.string).to eq 'Hello\r\n'
|
227
|
+
expect(io_err.string).to eq 'Psst\r\n'
|
228
|
+
expect(output).to eq echo_output
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
describe 'when called many times over time' do
|
233
|
+
# use a 'old' version of windows with lower max_commands threshold
|
234
|
+
# to trigger quicker shell recyles
|
235
|
+
let(:version_output) do
|
236
|
+
o = ::WinRM::Output.new
|
237
|
+
o[:exitcode] = 0
|
238
|
+
o[:data].concat([{ stdout: '6.1.8500.0\r\n' }])
|
239
|
+
o
|
240
|
+
end
|
241
|
+
|
242
|
+
let(:echo_output) do
|
243
|
+
o = ::WinRM::Output.new
|
244
|
+
o[:exitcode] = 0
|
245
|
+
o[:data].concat([{ stdout: 'Hello\r\n' }])
|
246
|
+
o
|
247
|
+
end
|
248
|
+
|
249
|
+
before do
|
250
|
+
allow(service).to receive(:open_shell).and_return('s1', 's2')
|
251
|
+
allow(service).to receive(:close_shell)
|
252
|
+
allow(service).to receive(:run_command).and_yield('command-xxx')
|
253
|
+
allow(service).to receive(:get_command_output).and_return(echo_output)
|
254
|
+
stub_powershell_script(
|
255
|
+
's1',
|
256
|
+
"$ProgressPreference='SilentlyContinue';[environment]::OSVersion.Version.tostring()",
|
257
|
+
version_output
|
258
|
+
)
|
259
|
+
end
|
260
|
+
|
261
|
+
it 'resets the shell when #max_commands threshold is tripped' do
|
262
|
+
iterations = 35
|
263
|
+
reset_times = iterations / (15 - 2)
|
264
|
+
|
265
|
+
expect(service).to receive(:close_shell).exactly(reset_times).times
|
266
|
+
executor.open
|
267
|
+
iterations.times { executor.run_cmd('echo', ['Hello']) }
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
describe '#run_powershell_script' do
|
273
|
+
describe 'when #open has not been previously called' do
|
274
|
+
it 'raises a WinRMError error' do
|
275
|
+
expect { executor.run_powershell_script('nope') }.to raise_error(
|
276
|
+
::WinRM::WinRMError,
|
277
|
+
"#{executor.class}#open must be called before any run methods are invoked"
|
278
|
+
)
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
describe 'when #open has been previously called' do
|
283
|
+
let(:command_id) { 'command-123' }
|
284
|
+
|
285
|
+
let(:echo_output) do
|
286
|
+
o = ::WinRM::Output.new
|
287
|
+
o[:exitcode] = 0
|
288
|
+
o[:data].concat([
|
289
|
+
{ stdout: 'Hello\r\n' },
|
290
|
+
{ stderr: 'Psst\r\n' }
|
291
|
+
])
|
292
|
+
o
|
293
|
+
end
|
294
|
+
|
295
|
+
before do
|
296
|
+
stub_powershell_script(
|
297
|
+
shell_id,
|
298
|
+
"$ProgressPreference='SilentlyContinue';echo Hello",
|
299
|
+
echo_output,
|
300
|
+
command_id
|
301
|
+
)
|
302
|
+
|
303
|
+
executor.open
|
304
|
+
end
|
305
|
+
|
306
|
+
it 'calls service#run_command' do
|
307
|
+
expect(service).to receive(:run_command).with(
|
308
|
+
shell_id,
|
309
|
+
'powershell',
|
310
|
+
[
|
311
|
+
'-encodedCommand',
|
312
|
+
::WinRM::PowershellScript.new("$ProgressPreference='SilentlyContinue';echo Hello")
|
313
|
+
.encoded
|
314
|
+
]
|
315
|
+
)
|
316
|
+
|
317
|
+
executor.run_powershell_script('echo Hello')
|
318
|
+
end
|
319
|
+
|
320
|
+
it 'calls service#get_command_output to get results' do
|
321
|
+
expect(service).to receive(:get_command_output).with(shell_id, command_id)
|
322
|
+
|
323
|
+
executor.run_powershell_script('echo Hello')
|
324
|
+
end
|
325
|
+
|
326
|
+
it 'calls service#get_command_output with a block to get results' do
|
327
|
+
blk = proc { |_, _| 'something' }
|
328
|
+
expect(service).to receive(:get_command_output).with(shell_id, command_id, &blk)
|
329
|
+
|
330
|
+
executor.run_powershell_script('echo Hello', &blk)
|
331
|
+
end
|
332
|
+
|
333
|
+
it 'returns an Output object hash' do
|
334
|
+
expect(executor.run_powershell_script('echo Hello')).to eq echo_output
|
335
|
+
end
|
336
|
+
|
337
|
+
it 'runs the block in #get_command_output when given' do
|
338
|
+
io_out = StringIO.new
|
339
|
+
io_err = StringIO.new
|
340
|
+
stub_cmd(shell_id, 'echo', ['Hello'], echo_output, command_id)
|
341
|
+
.and_yield(echo_output.stdout, echo_output.stderr)
|
342
|
+
output = executor.run_powershell_script('echo Hello') do |stdout, stderr|
|
343
|
+
io_out << stdout if stdout
|
344
|
+
io_err << stderr if stderr
|
345
|
+
end
|
346
|
+
|
347
|
+
expect(io_out.string).to eq 'Hello\r\n'
|
348
|
+
expect(io_err.string).to eq 'Psst\r\n'
|
349
|
+
expect(output).to eq echo_output
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
describe 'when called many times over time' do
|
354
|
+
# use a 'old' version of windows with lower max_commands threshold
|
355
|
+
# to trigger quicker shell recyles
|
356
|
+
let(:version_output) do
|
357
|
+
o = ::WinRM::Output.new
|
358
|
+
o[:exitcode] = 0
|
359
|
+
o[:data].concat([{ stdout: '6.1.8500.0\r\n' }])
|
360
|
+
o
|
361
|
+
end
|
362
|
+
|
363
|
+
let(:echo_output) do
|
364
|
+
o = ::WinRM::Output.new
|
365
|
+
o[:exitcode] = 0
|
366
|
+
o[:data].concat([{ stdout: 'Hello\r\n' }])
|
367
|
+
o
|
368
|
+
end
|
369
|
+
|
370
|
+
before do
|
371
|
+
allow(service).to receive(:open_shell).and_return('s1', 's2')
|
372
|
+
allow(service).to receive(:close_shell)
|
373
|
+
allow(service).to receive(:run_command).and_yield('command-xxx')
|
374
|
+
allow(service).to receive(:get_command_output).and_return(echo_output)
|
375
|
+
stub_powershell_script(
|
376
|
+
's1',
|
377
|
+
"$ProgressPreference='SilentlyContinue';[environment]::OSVersion.Version.tostring()",
|
378
|
+
version_output
|
379
|
+
)
|
380
|
+
end
|
381
|
+
|
382
|
+
it 'resets the shell when #max_commands threshold is tripped' do
|
383
|
+
iterations = 35
|
384
|
+
reset_times = iterations / (15 - 2)
|
385
|
+
|
386
|
+
expect(service).to receive(:close_shell).exactly(reset_times).times
|
387
|
+
executor.open
|
388
|
+
iterations.times { executor.run_powershell_script('echo Hello') }
|
389
|
+
end
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
describe '#shell' do
|
394
|
+
it 'is initially nil' do
|
395
|
+
expect(executor.shell).to eq nil
|
396
|
+
end
|
397
|
+
|
398
|
+
it 'is set after #open is called' do
|
399
|
+
executor.open
|
400
|
+
|
401
|
+
expect(executor.shell).to eq shell_id
|
402
|
+
end
|
403
|
+
end
|
404
|
+
|
405
|
+
def decode(powershell)
|
406
|
+
Base64.strict_decode64(powershell).encode('UTF-8', 'UTF-16LE')
|
407
|
+
end
|
408
|
+
|
409
|
+
def debug_line_with(msg)
|
410
|
+
/^D, .* : #{Regexp.escape(msg)}/
|
411
|
+
end
|
412
|
+
|
413
|
+
def regexify(string)
|
414
|
+
Regexp.new(Regexp.escape(string))
|
415
|
+
end
|
416
|
+
|
417
|
+
def regexify_line(string)
|
418
|
+
Regexp.new("^#{Regexp.escape(string)}$")
|
419
|
+
end
|
420
|
+
|
421
|
+
# rubocop:disable Metrics/ParameterLists
|
422
|
+
def stub_cmd(shell_id, cmd, args, output, command_id = nil, &block)
|
423
|
+
command_id ||= SecureRandom.uuid
|
424
|
+
|
425
|
+
allow(service).to receive(:run_command).with(shell_id, cmd, args).and_yield(command_id)
|
426
|
+
allow(service).to receive(:get_command_output).with(shell_id, command_id, &block)
|
427
|
+
.and_return(output)
|
428
|
+
end
|
429
|
+
|
430
|
+
def stub_powershell_script(shell_id, script, output, command_id = nil)
|
431
|
+
stub_cmd(
|
432
|
+
shell_id,
|
433
|
+
'powershell',
|
434
|
+
['-encodedCommand', ::WinRM::PowershellScript.new(script).encoded],
|
435
|
+
output,
|
436
|
+
command_id
|
437
|
+
)
|
438
|
+
end
|
439
|
+
# rubocop:enable Metrics/ParameterLists
|
440
|
+
end
|
data/spec/issue_59_spec.rb
CHANGED
@@ -6,10 +6,10 @@ describe 'issue 59', integration: true do
|
|
6
6
|
|
7
7
|
describe 'long running script without output' do
|
8
8
|
it 'should not error' do
|
9
|
-
|
10
|
-
expect(
|
11
|
-
expect(
|
12
|
-
expect(
|
9
|
+
out = @winrm.powershell('$ProgressPreference="SilentlyContinue";sleep 60; Write-Host "Hello"')
|
10
|
+
expect(out).to have_exit_code 0
|
11
|
+
expect(out).to have_stdout_match(/Hello/)
|
12
|
+
expect(out).to have_no_stderr
|
13
13
|
end
|
14
14
|
end
|
15
15
|
end
|
data/spec/powershell_spec.rb
CHANGED
@@ -4,12 +4,6 @@ describe 'winrm client powershell', integration: true do
|
|
4
4
|
@winrm = winrm_connection
|
5
5
|
end
|
6
6
|
|
7
|
-
describe 'empty string' do
|
8
|
-
subject(:output) { @winrm.powershell('') }
|
9
|
-
it { should have_exit_code 4_294_770_688 }
|
10
|
-
it { should have_stderr_match(/Cannot process the command because of a missing parameter/) }
|
11
|
-
end
|
12
|
-
|
13
7
|
describe 'ipconfig' do
|
14
8
|
subject(:output) { @winrm.powershell('ipconfig') }
|
15
9
|
it { should have_exit_code 0 }
|
data/spec/spec_helper.rb
CHANGED
@@ -7,13 +7,39 @@ require_relative 'matchers'
|
|
7
7
|
|
8
8
|
# Creates a WinRM connection for integration tests
|
9
9
|
module ConnectionHelper
|
10
|
+
# rubocop:disable AbcSize
|
10
11
|
def winrm_connection
|
11
|
-
config = symbolize_keys(YAML.load(File.read(winrm_config_path)))
|
12
|
-
config[:options].merge!(basic_auth_only: true) unless config[:auth_type].eql? :kerberos
|
13
12
|
winrm = WinRM::WinRMWebService.new(
|
14
13
|
config[:endpoint], config[:auth_type].to_sym, config[:options])
|
14
|
+
winrm.logger.level = :error
|
15
15
|
winrm
|
16
16
|
end
|
17
|
+
# rubocop:enable AbcSize
|
18
|
+
|
19
|
+
def config
|
20
|
+
@config ||= begin
|
21
|
+
cfg = symbolize_keys(YAML.load(File.read(winrm_config_path)))
|
22
|
+
cfg[:options].merge!(basic_auth_only: true) unless cfg[:auth_type].eql? :kerberos
|
23
|
+
merge_environment!(cfg)
|
24
|
+
cfg
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def merge_environment!(config)
|
29
|
+
merge_config_option_from_environment(config, 'user')
|
30
|
+
merge_config_option_from_environment(config, 'pass')
|
31
|
+
merge_config_option_from_environment(config, 'no_ssl_peer_verification')
|
32
|
+
if ENV['use_ssl_peer_fingerprint']
|
33
|
+
config[:options][:ssl_peer_fingerprint] = ENV['winrm_cert']
|
34
|
+
end
|
35
|
+
config[:endpoint] = ENV['winrm_endpoint'] if ENV['winrm_endpoint']
|
36
|
+
config[:auth_type] = ENV['winrm_auth_type'] if ENV['winrm_auth_type']
|
37
|
+
end
|
38
|
+
|
39
|
+
def merge_config_option_from_environment(config, key)
|
40
|
+
env_key = 'winrm_' + key
|
41
|
+
config[:options][key.to_sym] = ENV[env_key] if ENV[env_key]
|
42
|
+
end
|
17
43
|
|
18
44
|
def winrm_config_path
|
19
45
|
# Copy config-example.yml to config.yml and edit for your local configuration
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: winrm
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dan Wanek
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2016-01-11 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: gssapi
|
@@ -198,9 +198,11 @@ files:
|
|
198
198
|
- README.md
|
199
199
|
- Rakefile
|
200
200
|
- Vagrantfile
|
201
|
+
- appveyor.yml
|
201
202
|
- bin/rwinrm
|
202
203
|
- changelog.md
|
203
204
|
- lib/winrm.rb
|
205
|
+
- lib/winrm/command_executor.rb
|
204
206
|
- lib/winrm/exceptions/exceptions.rb
|
205
207
|
- lib/winrm/helpers/iso8601_duration.rb
|
206
208
|
- lib/winrm/helpers/powershell_script.rb
|
@@ -213,6 +215,7 @@ files:
|
|
213
215
|
- preamble
|
214
216
|
- spec/auth_timeout_spec.rb
|
215
217
|
- spec/cmd_spec.rb
|
218
|
+
- spec/command_executor_spec.rb
|
216
219
|
- spec/config-example.yml
|
217
220
|
- spec/exception_spec.rb
|
218
221
|
- spec/issue_59_spec.rb
|
@@ -254,7 +257,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
254
257
|
version: '0'
|
255
258
|
requirements: []
|
256
259
|
rubyforge_project:
|
257
|
-
rubygems_version: 2.4.
|
260
|
+
rubygems_version: 2.4.8
|
258
261
|
signing_key:
|
259
262
|
specification_version: 4
|
260
263
|
summary: Ruby library for Windows Remote Management
|