winrm-s 0.2.4 → 0.3.0.dev.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +27 -27
- data/.rspec +2 -2
- data/CHANGELOG.md +29 -29
- data/CONTRIBUTING.md +5 -5
- data/Gemfile +7 -7
- data/LICENSE +201 -201
- data/README.md +69 -69
- data/Rakefile +1 -1
- data/lib/winrm-s.rb +30 -30
- data/lib/winrm-s/version.rb +22 -22
- data/lib/winrm/helpers/assert_patch.rb +17 -17
- data/lib/winrm/http/auth.rb +191 -191
- data/lib/winrm/http/transport_patch.rb +35 -35
- data/lib/winrm/win32/sspi.rb +256 -256
- data/winrm-s.gemspec +25 -25
- metadata +23 -25
data/README.md
CHANGED
@@ -1,69 +1,69 @@
|
|
1
|
-
winrm-s
|
2
|
-
=======
|
3
|
-
|
4
|
-
`winrm-s` extends the functionality of the
|
5
|
-
[WinRM gem](http://rubygems.org/gems/winrm) to support the Microsoft
|
6
|
-
[Negotiate protocol](http://msdn.microsoft.com/en-us/library/windows/desktop/aa378748(v=vs.85).aspx)
|
7
|
-
when authenticating to a remote [WinRM](http://msdn.microsoft.com/en-us/library/aa384426(v=vs.85).aspx) endpoint from a Windows system.
|
8
|
-
|
9
|
-
This extended functionality is **only** supported when running on Microsoft
|
10
|
-
Windows. This gem can still be used on other operating systems just like the
|
11
|
-
`WinRM` gem, but the extended capabilities will not be available.
|
12
|
-
|
13
|
-
Installation
|
14
|
-
------------
|
15
|
-
|
16
|
-
To install it, run:
|
17
|
-
|
18
|
-
gem install winrm-s
|
19
|
-
|
20
|
-
Usage
|
21
|
-
-----
|
22
|
-
|
23
|
-
`winrm-s` provides the same interface as the `winrm` gem -- see `winrm`
|
24
|
-
[documentation](https://github.com/WinRb/WinRM/blob/master/README.md) for `winrm-s` usage.
|
25
|
-
|
26
|
-
* To use it, simply require `winrm` or `winrm-s`, depending on whether your code
|
27
|
-
is running on Windows. The extended negotiate protocol is only available if
|
28
|
-
you include `winrm-s`, which will only work on Windows.
|
29
|
-
* When you use WinRM::WinRMWebService.new, be sure to specify the
|
30
|
-
`:sspinegotiate` parameter, along with a user name in the form `domain_name\user_name`
|
31
|
-
for the user name in order to make use of negotiate protocol. If the user
|
32
|
-
account is local to the remote system, you can use `.` for the domain. The
|
33
|
-
example further on demonstrates the negotiate use case.
|
34
|
-
* All other use cases enabled by the `winrm` gem are also supported.
|
35
|
-
|
36
|
-
Example
|
37
|
-
-------
|
38
|
-
Note the argument value of `:sspinegotiate` for transport option, and the
|
39
|
-
explicit specification of a domain name, in this case `.`, in the user name:
|
40
|
-
```ruby
|
41
|
-
if RUBY_PLATFORM =~ /mswin|mingw32|windows/
|
42
|
-
require 'winrm-s' # only works on Windows, otherwise use require 'winrm'
|
43
|
-
endpoint = http://mywinrmhost:5985/wsman
|
44
|
-
winrm = WinRM::WinRMWebService.new(endpoint, :sspinegotiate, :user => ".\administrator", :pass => "adminpasswd")
|
45
|
-
winrm.cmd('ipconfig /all') do |stdout, stderr|
|
46
|
-
STDOUT.print stdout
|
47
|
-
STDERR.print stderr
|
48
|
-
end
|
49
|
-
end
|
50
|
-
```
|
51
|
-
|
52
|
-
License
|
53
|
-
-------
|
54
|
-
|
55
|
-
Copyright:: Copyright (c) 2014 Chef Software, Inc.
|
56
|
-
License:: Apache License, Version 2.0
|
57
|
-
|
58
|
-
Licensed under the Apache License, Version 2.0 (the "License");
|
59
|
-
you may not use this file except in compliance with the License.
|
60
|
-
You may obtain a copy of the License at
|
61
|
-
|
62
|
-
http://www.apache.org/licenses/LICENSE-2.0
|
63
|
-
|
64
|
-
Unless required by applicable law or agreed to in writing, software
|
65
|
-
distributed under the License is distributed on an "AS IS" BASIS,
|
66
|
-
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
67
|
-
See the License for the specific language governing permissions and
|
68
|
-
limitations under the License.
|
69
|
-
|
1
|
+
winrm-s
|
2
|
+
=======
|
3
|
+
|
4
|
+
`winrm-s` extends the functionality of the
|
5
|
+
[WinRM gem](http://rubygems.org/gems/winrm) to support the Microsoft
|
6
|
+
[Negotiate protocol](http://msdn.microsoft.com/en-us/library/windows/desktop/aa378748(v=vs.85).aspx)
|
7
|
+
when authenticating to a remote [WinRM](http://msdn.microsoft.com/en-us/library/aa384426(v=vs.85).aspx) endpoint from a Windows system.
|
8
|
+
|
9
|
+
This extended functionality is **only** supported when running on Microsoft
|
10
|
+
Windows. This gem can still be used on other operating systems just like the
|
11
|
+
`WinRM` gem, but the extended capabilities will not be available.
|
12
|
+
|
13
|
+
Installation
|
14
|
+
------------
|
15
|
+
|
16
|
+
To install it, run:
|
17
|
+
|
18
|
+
gem install winrm-s
|
19
|
+
|
20
|
+
Usage
|
21
|
+
-----
|
22
|
+
|
23
|
+
`winrm-s` provides the same interface as the `winrm` gem -- see `winrm`
|
24
|
+
[documentation](https://github.com/WinRb/WinRM/blob/master/README.md) for `winrm-s` usage.
|
25
|
+
|
26
|
+
* To use it, simply require `winrm` or `winrm-s`, depending on whether your code
|
27
|
+
is running on Windows. The extended negotiate protocol is only available if
|
28
|
+
you include `winrm-s`, which will only work on Windows.
|
29
|
+
* When you use WinRM::WinRMWebService.new, be sure to specify the
|
30
|
+
`:sspinegotiate` parameter, along with a user name in the form `domain_name\user_name`
|
31
|
+
for the user name in order to make use of negotiate protocol. If the user
|
32
|
+
account is local to the remote system, you can use `.` for the domain. The
|
33
|
+
example further on demonstrates the negotiate use case.
|
34
|
+
* All other use cases enabled by the `winrm` gem are also supported.
|
35
|
+
|
36
|
+
Example
|
37
|
+
-------
|
38
|
+
Note the argument value of `:sspinegotiate` for transport option, and the
|
39
|
+
explicit specification of a domain name, in this case `.`, in the user name:
|
40
|
+
```ruby
|
41
|
+
if RUBY_PLATFORM =~ /mswin|mingw32|windows/
|
42
|
+
require 'winrm-s' # only works on Windows, otherwise use require 'winrm'
|
43
|
+
endpoint = http://mywinrmhost:5985/wsman
|
44
|
+
winrm = WinRM::WinRMWebService.new(endpoint, :sspinegotiate, :user => ".\administrator", :pass => "adminpasswd")
|
45
|
+
winrm.cmd('ipconfig /all') do |stdout, stderr|
|
46
|
+
STDOUT.print stdout
|
47
|
+
STDERR.print stderr
|
48
|
+
end
|
49
|
+
end
|
50
|
+
```
|
51
|
+
|
52
|
+
License
|
53
|
+
-------
|
54
|
+
|
55
|
+
Copyright:: Copyright (c) 2014 Chef Software, Inc.
|
56
|
+
License:: Apache License, Version 2.0
|
57
|
+
|
58
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
59
|
+
you may not use this file except in compliance with the License.
|
60
|
+
You may obtain a copy of the License at
|
61
|
+
|
62
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
63
|
+
|
64
|
+
Unless required by applicable law or agreed to in writing, software
|
65
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
66
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
67
|
+
See the License for the specific language governing permissions and
|
68
|
+
limitations under the License.
|
69
|
+
|
data/Rakefile
CHANGED
@@ -1 +1 @@
|
|
1
|
-
require 'bundler/gem_tasks'
|
1
|
+
require 'bundler/gem_tasks'
|
data/lib/winrm-s.rb
CHANGED
@@ -1,30 +1,30 @@
|
|
1
|
-
#
|
2
|
-
# Author:: Kaustubh Deorukhkar (<kaustubh@clogeny.com>)>
|
3
|
-
# Copyright:: Copyright (c) 2014 Chef Software, Inc.
|
4
|
-
# License:: Apache License, Version 2.0
|
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
|
-
|
19
|
-
require 'winrm'
|
20
|
-
|
21
|
-
def windows?
|
22
|
-
!!(RUBY_PLATFORM =~ /mswin|mingw|windows/)
|
23
|
-
end
|
24
|
-
|
25
|
-
# Patch only on windows
|
26
|
-
if windows?
|
27
|
-
require 'winrm/winrm_service_patch'
|
28
|
-
else
|
29
|
-
raise "ERROR: winrm-s extensions to the winrm gem for the negotiate protocol are only supported on Windows. Require 'winrm' and not 'winrm-s' on non-Windows platforms."
|
30
|
-
end
|
1
|
+
#
|
2
|
+
# Author:: Kaustubh Deorukhkar (<kaustubh@clogeny.com>)>
|
3
|
+
# Copyright:: Copyright (c) 2014 Chef Software, Inc.
|
4
|
+
# License:: Apache License, Version 2.0
|
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
|
+
|
19
|
+
require 'winrm'
|
20
|
+
|
21
|
+
def windows?
|
22
|
+
!!(RUBY_PLATFORM =~ /mswin|mingw|windows/)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Patch only on windows
|
26
|
+
if windows?
|
27
|
+
require 'winrm/winrm_service_patch'
|
28
|
+
else
|
29
|
+
raise "ERROR: winrm-s extensions to the winrm gem for the negotiate protocol are only supported on Windows. Require 'winrm' and not 'winrm-s' on non-Windows platforms."
|
30
|
+
end
|
data/lib/winrm-s/version.rb
CHANGED
@@ -1,22 +1,22 @@
|
|
1
|
-
#
|
2
|
-
# Author:: Kaustubh Deorukhkar (<kaustubh@clogeny.com>)
|
3
|
-
# Copyright:: Copyright (c) 2014 Chef Software, Inc.
|
4
|
-
# License:: Apache License, Version 2.0
|
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
|
-
|
19
|
-
module WinrmS
|
20
|
-
VERSION = "0.
|
21
|
-
MAJOR, MINOR, TINY = VERSION.split('.')
|
22
|
-
end
|
1
|
+
#
|
2
|
+
# Author:: Kaustubh Deorukhkar (<kaustubh@clogeny.com>)
|
3
|
+
# Copyright:: Copyright (c) 2014 Chef Software, Inc.
|
4
|
+
# License:: Apache License, Version 2.0
|
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
|
+
|
19
|
+
module WinrmS
|
20
|
+
VERSION = "0.3.0.dev.0"
|
21
|
+
MAJOR, MINOR, TINY = VERSION.split('.')
|
22
|
+
end
|
@@ -1,18 +1,18 @@
|
|
1
|
-
|
2
|
-
module PatchAssertions
|
3
|
-
|
4
|
-
def self.assert_major_version(library_name, expected_major, override_var_name)
|
5
|
-
supported_major_ver = ENV[override_var_name] || expected_major
|
6
|
-
loaded_ver = Gem.loaded_specs[library_name].version
|
7
|
-
loaded_major_ver = loaded_ver.to_s.match(/(^\d+\.\d+)\.(.*)$/)[1]
|
8
|
-
if loaded_major_ver.to_s != supported_major_ver.to_s
|
9
|
-
puts "WARNING: Unsupported version of #{library_name}. The supported major version of library is #{library_name} version #{expected_major}. This code path monkey patches few methods in #{library_name} to support additional features. This could possibly work, but it is advised to extensively test your version. If you are aware of the impact of using #{loaded_ver}, this warning can be disabled by setting #{override_var_name} to the major version #{loaded_major_ver}. "
|
10
|
-
end
|
11
|
-
end
|
12
|
-
|
13
|
-
def self.assert_arity_of_patched_method(klass, method_name, expected_arity)
|
14
|
-
if klass.instance_method(method_name).arity != expected_arity
|
15
|
-
puts "WARNING: Cannot patch method #{klass}::#{method_name} since your latest gem seems to have different method definition that cannot be safely patched. This could possibly work, but it is advised to extensively test your version. Please use the supported version of patched gem."
|
16
|
-
end
|
17
|
-
end
|
1
|
+
|
2
|
+
module PatchAssertions
|
3
|
+
|
4
|
+
def self.assert_major_version(library_name, expected_major, override_var_name)
|
5
|
+
supported_major_ver = ENV[override_var_name] || expected_major
|
6
|
+
loaded_ver = Gem.loaded_specs[library_name].version
|
7
|
+
loaded_major_ver = loaded_ver.to_s.match(/(^\d+\.\d+)\.(.*)$/)[1]
|
8
|
+
if loaded_major_ver.to_s != supported_major_ver.to_s
|
9
|
+
puts "WARNING: Unsupported version of #{library_name}. The supported major version of library is #{library_name} version #{expected_major}. This code path monkey patches few methods in #{library_name} to support additional features. This could possibly work, but it is advised to extensively test your version. If you are aware of the impact of using #{loaded_ver}, this warning can be disabled by setting #{override_var_name} to the major version #{loaded_major_ver}. "
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.assert_arity_of_patched_method(klass, method_name, expected_arity)
|
14
|
+
if klass.instance_method(method_name).arity != expected_arity
|
15
|
+
puts "WARNING: Cannot patch method #{klass}::#{method_name} since your latest gem seems to have different method definition that cannot be safely patched. This could possibly work, but it is advised to extensively test your version. Please use the supported version of patched gem."
|
16
|
+
end
|
17
|
+
end
|
18
18
|
end
|
data/lib/winrm/http/auth.rb
CHANGED
@@ -1,191 +1,191 @@
|
|
1
|
-
#
|
2
|
-
# Author:: Kaustubh Deorukhkar (<kaustubh@clogeny.com>)
|
3
|
-
# Copyright:: Copyright (c) 2014 Chef Software, Inc.
|
4
|
-
# License:: Apache License, Version 2.0
|
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
|
-
|
19
|
-
require 'httpclient/auth'
|
20
|
-
|
21
|
-
require 'winrm/helpers/assert_patch'
|
22
|
-
|
23
|
-
def validate_patch
|
24
|
-
# The code below patches the httpclient library to add support
|
25
|
-
# for encrypt/decrypt as described below.
|
26
|
-
# Add few restriction to make sure the patched methods are still
|
27
|
-
# available, but still give a way to consciously use later versions
|
28
|
-
PatchAssertions.assert_major_version("httpclient", 2.6, "USE_HTTPCLIENT_MAJOR")
|
29
|
-
PatchAssertions.assert_arity_of_patched_method(HTTPClient::WWWAuth, "filter_request", 1)
|
30
|
-
PatchAssertions.assert_arity_of_patched_method(HTTPClient::WWWAuth, "filter_response", 2)
|
31
|
-
PatchAssertions.assert_arity_of_patched_method(HTTPClient::SSPINegotiateAuth, "set", -1)
|
32
|
-
PatchAssertions.assert_arity_of_patched_method(HTTPClient::SSPINegotiateAuth, "get", 1)
|
33
|
-
end
|
34
|
-
|
35
|
-
# Perform the patch validations
|
36
|
-
validate_patch
|
37
|
-
|
38
|
-
# Overrides the HTTPClient::WWWAuth code to add support for encryption/decryption
|
39
|
-
# of data sent during the NTLM auth over negotiate.
|
40
|
-
|
41
|
-
# Also Overrides HTTPClient::SSPINegotiateAuth to remember user credentials, original
|
42
|
-
# library code relies on the current login user credentials on the client machine.
|
43
|
-
|
44
|
-
# Below code helps ruby client to perform auth using the credentials provided to
|
45
|
-
# ruby client, and also enhances to use encrypted channel.
|
46
|
-
|
47
|
-
class HTTPClient
|
48
|
-
|
49
|
-
class WWWAuth
|
50
|
-
|
51
|
-
# Filter API implementation. Traps HTTP request and insert
|
52
|
-
# 'Authorization' header if needed.
|
53
|
-
def filter_request(req)
|
54
|
-
@authenticator.each do |auth|
|
55
|
-
next unless auth.set? # hasn't be set, don't use it
|
56
|
-
if cred = auth.get(req)
|
57
|
-
auth.encrypt_payload(req) if auth.respond_to?(:encrypt_payload)
|
58
|
-
req.header.set('Authorization', auth.scheme + " " + cred)
|
59
|
-
return
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
# Filter API implementation. Traps HTTP response and parses
|
65
|
-
# 'WWW-Authenticate' header.
|
66
|
-
#
|
67
|
-
# This remembers the challenges for all authentication methods
|
68
|
-
# available to the client. On the subsequent retry of the request,
|
69
|
-
# filter_request will select the strongest method.
|
70
|
-
def filter_response(req, res)
|
71
|
-
command = nil
|
72
|
-
if res.status == HTTP::Status::UNAUTHORIZED
|
73
|
-
if challenge = parse_authentication_header(res, 'www-authenticate')
|
74
|
-
uri = req.header.request_uri
|
75
|
-
challenge.each do |scheme, param_str|
|
76
|
-
@authenticator.each do |auth|
|
77
|
-
next unless auth.set? # hasn't be set, don't use it
|
78
|
-
if scheme.downcase == auth.scheme.downcase
|
79
|
-
challengeable = auth.challenge(uri, param_str)
|
80
|
-
command = :retry if challengeable
|
81
|
-
end
|
82
|
-
end
|
83
|
-
end
|
84
|
-
# ignore unknown authentication scheme
|
85
|
-
end
|
86
|
-
elsif res.status == HTTP::Status::OK
|
87
|
-
decrypted_content = res.content
|
88
|
-
@authenticator.each do |auth|
|
89
|
-
next unless auth.set? # hasn't be set, don't use it
|
90
|
-
decrypted_content = auth.decrypt_payload(res.content) if auth.respond_to?(:encrypted_channel?) and auth.encrypted_channel?
|
91
|
-
end
|
92
|
-
# update with decrypted content
|
93
|
-
res.content.replace(decrypted_content) if res.content and !res.content.empty?
|
94
|
-
end
|
95
|
-
command
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
class SSPINegotiateAuth
|
100
|
-
|
101
|
-
begin
|
102
|
-
require 'win32/sspi'
|
103
|
-
SSPIEnabled = true
|
104
|
-
rescue LoadError
|
105
|
-
SSPIEnabled = false
|
106
|
-
end
|
107
|
-
|
108
|
-
begin
|
109
|
-
require 'gssapi'
|
110
|
-
GSSAPIEnabled = true
|
111
|
-
rescue LoadError
|
112
|
-
GSSAPIEnabled = false
|
113
|
-
end
|
114
|
-
|
115
|
-
# Override to remember creds
|
116
|
-
# Set authentication credential.
|
117
|
-
def set(uri, user, passwd)
|
118
|
-
# Check if user has domain specified in it.
|
119
|
-
if user
|
120
|
-
creds = user.split("\\")
|
121
|
-
creds.length.eql?(2) ? (@domain,@user = creds) : @user = creds[0]
|
122
|
-
end
|
123
|
-
@passwd = passwd
|
124
|
-
end
|
125
|
-
|
126
|
-
def set?
|
127
|
-
SSPIEnabled || GSSAPIEnabled
|
128
|
-
end
|
129
|
-
|
130
|
-
# Response handler: returns credential.
|
131
|
-
# See win32/sspi for negotiation state transition.
|
132
|
-
def get(req)
|
133
|
-
return nil unless SSPIEnabled || GSSAPIEnabled
|
134
|
-
target_uri = req.header.request_uri
|
135
|
-
domain_uri, param = @challenge.find { |uri, v|
|
136
|
-
Util.uri_part_of(target_uri, uri)
|
137
|
-
}
|
138
|
-
|
139
|
-
return nil unless param
|
140
|
-
|
141
|
-
state = param[:state]
|
142
|
-
authenticator = param[:authenticator]
|
143
|
-
authphrase = param[:authphrase]
|
144
|
-
case state
|
145
|
-
when :init
|
146
|
-
if SSPIEnabled
|
147
|
-
# Over-ride ruby win32 sspi to support encrypt/decrypt
|
148
|
-
require 'winrm/win32/sspi'
|
149
|
-
authenticator = param[:authenticator] = Win32::SSPI::NegotiateAuth.new(@user, @domain, @passwd)
|
150
|
-
@authenticator = authenticator # **** Hacky remember as we need this for encrypt/decrypt
|
151
|
-
return authenticator.get_initial_token
|
152
|
-
else # use GSSAPI
|
153
|
-
authenticator = param[:authenticator] = GSSAPI::Simple.new(domain_uri.host, 'HTTP')
|
154
|
-
# Base64 encode the context token
|
155
|
-
return [authenticator.init_context].pack('m').gsub(/\n/,'')
|
156
|
-
end
|
157
|
-
when :response
|
158
|
-
@challenge.delete(domain_uri)
|
159
|
-
if SSPIEnabled
|
160
|
-
return authenticator.complete_authentication(authphrase)
|
161
|
-
else # use GSSAPI
|
162
|
-
return authenticator.init_context(authphrase.unpack('m').pop)
|
163
|
-
end
|
164
|
-
end
|
165
|
-
nil
|
166
|
-
end
|
167
|
-
|
168
|
-
def encrypted_channel?
|
169
|
-
@encrypted_channel
|
170
|
-
end
|
171
|
-
|
172
|
-
def encrypt_payload(req)
|
173
|
-
if SSPIEnabled
|
174
|
-
body = @authenticator.encrypt_payload(req.body)
|
175
|
-
req.http_body = HTTP::Message::Body.new
|
176
|
-
req.http_body.init_request(body)
|
177
|
-
req.http_header.body_size = body.length if body
|
178
|
-
# if body is encrypted update the header
|
179
|
-
if body.include? "HTTP-SPNEGO-session-encrypted"
|
180
|
-
@encrypted_channel = true
|
181
|
-
req.header.set('Content-Type', "multipart/encrypted;protocol=\"application/HTTP-SPNEGO-session-encrypted\";boundary=\"Encrypted Boundary\"")
|
182
|
-
end
|
183
|
-
end
|
184
|
-
end
|
185
|
-
|
186
|
-
def decrypt_payload(body)
|
187
|
-
body = @authenticator.decrypt_payload(body) if SSPIEnabled
|
188
|
-
body
|
189
|
-
end
|
190
|
-
end
|
191
|
-
end
|
1
|
+
#
|
2
|
+
# Author:: Kaustubh Deorukhkar (<kaustubh@clogeny.com>)
|
3
|
+
# Copyright:: Copyright (c) 2014 Chef Software, Inc.
|
4
|
+
# License:: Apache License, Version 2.0
|
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
|
+
|
19
|
+
require 'httpclient/auth'
|
20
|
+
|
21
|
+
require 'winrm/helpers/assert_patch'
|
22
|
+
|
23
|
+
def validate_patch
|
24
|
+
# The code below patches the httpclient library to add support
|
25
|
+
# for encrypt/decrypt as described below.
|
26
|
+
# Add few restriction to make sure the patched methods are still
|
27
|
+
# available, but still give a way to consciously use later versions
|
28
|
+
PatchAssertions.assert_major_version("httpclient", 2.6, "USE_HTTPCLIENT_MAJOR")
|
29
|
+
PatchAssertions.assert_arity_of_patched_method(HTTPClient::WWWAuth, "filter_request", 1)
|
30
|
+
PatchAssertions.assert_arity_of_patched_method(HTTPClient::WWWAuth, "filter_response", 2)
|
31
|
+
PatchAssertions.assert_arity_of_patched_method(HTTPClient::SSPINegotiateAuth, "set", -1)
|
32
|
+
PatchAssertions.assert_arity_of_patched_method(HTTPClient::SSPINegotiateAuth, "get", 1)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Perform the patch validations
|
36
|
+
validate_patch
|
37
|
+
|
38
|
+
# Overrides the HTTPClient::WWWAuth code to add support for encryption/decryption
|
39
|
+
# of data sent during the NTLM auth over negotiate.
|
40
|
+
|
41
|
+
# Also Overrides HTTPClient::SSPINegotiateAuth to remember user credentials, original
|
42
|
+
# library code relies on the current login user credentials on the client machine.
|
43
|
+
|
44
|
+
# Below code helps ruby client to perform auth using the credentials provided to
|
45
|
+
# ruby client, and also enhances to use encrypted channel.
|
46
|
+
|
47
|
+
class HTTPClient
|
48
|
+
|
49
|
+
class WWWAuth
|
50
|
+
|
51
|
+
# Filter API implementation. Traps HTTP request and insert
|
52
|
+
# 'Authorization' header if needed.
|
53
|
+
def filter_request(req)
|
54
|
+
@authenticator.each do |auth|
|
55
|
+
next unless auth.set? # hasn't be set, don't use it
|
56
|
+
if cred = auth.get(req)
|
57
|
+
auth.encrypt_payload(req) if auth.respond_to?(:encrypt_payload)
|
58
|
+
req.header.set('Authorization', auth.scheme + " " + cred)
|
59
|
+
return
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Filter API implementation. Traps HTTP response and parses
|
65
|
+
# 'WWW-Authenticate' header.
|
66
|
+
#
|
67
|
+
# This remembers the challenges for all authentication methods
|
68
|
+
# available to the client. On the subsequent retry of the request,
|
69
|
+
# filter_request will select the strongest method.
|
70
|
+
def filter_response(req, res)
|
71
|
+
command = nil
|
72
|
+
if res.status == HTTP::Status::UNAUTHORIZED
|
73
|
+
if challenge = parse_authentication_header(res, 'www-authenticate')
|
74
|
+
uri = req.header.request_uri
|
75
|
+
challenge.each do |scheme, param_str|
|
76
|
+
@authenticator.each do |auth|
|
77
|
+
next unless auth.set? # hasn't be set, don't use it
|
78
|
+
if scheme.downcase == auth.scheme.downcase
|
79
|
+
challengeable = auth.challenge(uri, param_str)
|
80
|
+
command = :retry if challengeable
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
# ignore unknown authentication scheme
|
85
|
+
end
|
86
|
+
elsif res.status == HTTP::Status::OK
|
87
|
+
decrypted_content = res.content
|
88
|
+
@authenticator.each do |auth|
|
89
|
+
next unless auth.set? # hasn't be set, don't use it
|
90
|
+
decrypted_content = auth.decrypt_payload(res.content) if auth.respond_to?(:encrypted_channel?) and auth.encrypted_channel?
|
91
|
+
end
|
92
|
+
# update with decrypted content
|
93
|
+
res.content.replace(decrypted_content) if res.content and !res.content.empty?
|
94
|
+
end
|
95
|
+
command
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
class SSPINegotiateAuth
|
100
|
+
|
101
|
+
begin
|
102
|
+
require 'win32/sspi'
|
103
|
+
SSPIEnabled = true
|
104
|
+
rescue LoadError
|
105
|
+
SSPIEnabled = false
|
106
|
+
end
|
107
|
+
|
108
|
+
begin
|
109
|
+
require 'gssapi'
|
110
|
+
GSSAPIEnabled = true
|
111
|
+
rescue LoadError
|
112
|
+
GSSAPIEnabled = false
|
113
|
+
end
|
114
|
+
|
115
|
+
# Override to remember creds
|
116
|
+
# Set authentication credential.
|
117
|
+
def set(uri, user, passwd)
|
118
|
+
# Check if user has domain specified in it.
|
119
|
+
if user
|
120
|
+
creds = user.split("\\")
|
121
|
+
creds.length.eql?(2) ? (@domain,@user = creds) : @user = creds[0]
|
122
|
+
end
|
123
|
+
@passwd = passwd
|
124
|
+
end
|
125
|
+
|
126
|
+
def set?
|
127
|
+
SSPIEnabled || GSSAPIEnabled
|
128
|
+
end
|
129
|
+
|
130
|
+
# Response handler: returns credential.
|
131
|
+
# See win32/sspi for negotiation state transition.
|
132
|
+
def get(req)
|
133
|
+
return nil unless SSPIEnabled || GSSAPIEnabled
|
134
|
+
target_uri = req.header.request_uri
|
135
|
+
domain_uri, param = @challenge.find { |uri, v|
|
136
|
+
Util.uri_part_of(target_uri, uri)
|
137
|
+
}
|
138
|
+
|
139
|
+
return nil unless param
|
140
|
+
|
141
|
+
state = param[:state]
|
142
|
+
authenticator = param[:authenticator]
|
143
|
+
authphrase = param[:authphrase]
|
144
|
+
case state
|
145
|
+
when :init
|
146
|
+
if SSPIEnabled
|
147
|
+
# Over-ride ruby win32 sspi to support encrypt/decrypt
|
148
|
+
require 'winrm/win32/sspi'
|
149
|
+
authenticator = param[:authenticator] = Win32::SSPI::NegotiateAuth.new(@user, @domain, @passwd)
|
150
|
+
@authenticator = authenticator # **** Hacky remember as we need this for encrypt/decrypt
|
151
|
+
return authenticator.get_initial_token
|
152
|
+
else # use GSSAPI
|
153
|
+
authenticator = param[:authenticator] = GSSAPI::Simple.new(domain_uri.host, 'HTTP')
|
154
|
+
# Base64 encode the context token
|
155
|
+
return [authenticator.init_context].pack('m').gsub(/\n/,'')
|
156
|
+
end
|
157
|
+
when :response
|
158
|
+
@challenge.delete(domain_uri)
|
159
|
+
if SSPIEnabled
|
160
|
+
return authenticator.complete_authentication(authphrase)
|
161
|
+
else # use GSSAPI
|
162
|
+
return authenticator.init_context(authphrase.unpack('m').pop)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
nil
|
166
|
+
end
|
167
|
+
|
168
|
+
def encrypted_channel?
|
169
|
+
@encrypted_channel
|
170
|
+
end
|
171
|
+
|
172
|
+
def encrypt_payload(req)
|
173
|
+
if SSPIEnabled
|
174
|
+
body = @authenticator.encrypt_payload(req.body)
|
175
|
+
req.http_body = HTTP::Message::Body.new
|
176
|
+
req.http_body.init_request(body)
|
177
|
+
req.http_header.body_size = body.length if body
|
178
|
+
# if body is encrypted update the header
|
179
|
+
if body.include? "HTTP-SPNEGO-session-encrypted"
|
180
|
+
@encrypted_channel = true
|
181
|
+
req.header.set('Content-Type', "multipart/encrypted;protocol=\"application/HTTP-SPNEGO-session-encrypted\";boundary=\"Encrypted Boundary\"")
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def decrypt_payload(body)
|
187
|
+
body = @authenticator.decrypt_payload(body) if SSPIEnabled
|
188
|
+
body
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|