rubysspi 1.2.2 → 1.2.3
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.
- data/README.txt +5 -5
- data/Rakefile +1 -1
- data/lib/win32/sspi.rb +320 -319
- data/lib/win32/sspi/http_proxy_patch.rb +6 -0
- data/test/test_ruby_sspi.rb +95 -88
- metadata +2 -2
data/README.txt
CHANGED
@@ -99,11 +99,11 @@ The token returned (usually an NTLM Type 3) message can then be sent to the serv
|
|
99
99
|
|
100
100
|
If you have installed the RubySSPI library previously, and wish to upgrade to a new version, follow these steps:
|
101
101
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
102
|
+
* Install the new version of the gem
|
103
|
+
* If you applied the patch before, go to your Ruby library directory (under the One-click installer, its usually ruby\lib\ruby\1.8):
|
104
|
+
* If any http.orig.N.rb files exist (where N is a number), then delete http.rb and rename the lowest http.orig.N.rb file to http.rb
|
105
|
+
* Delete any other nttp.orig.N.rb files.
|
106
|
+
* Run "apply_sspi_patch" to apply the patch again
|
107
107
|
|
108
108
|
= Disabling proxy for certain servers
|
109
109
|
|
data/Rakefile
CHANGED
@@ -6,7 +6,7 @@ require 'rake/testtask'
|
|
6
6
|
spec = Gem::Specification.new do |s|
|
7
7
|
s.name = "rubysspi"
|
8
8
|
s.summary = "A library which implements Ruby bindings to the Win32 SSPI library. Also includes a module to add Negotiate authentication support to Net::HTTP."
|
9
|
-
s.version = "1.2.
|
9
|
+
s.version = "1.2.3"
|
10
10
|
s.author = "Justin Bailey"
|
11
11
|
s.email = "jgbailey @nospam@ gmail.com"
|
12
12
|
s.homepage = "http://rubyforge.org/projects/rubysspi/"
|
data/lib/win32/sspi.rb
CHANGED
@@ -9,323 +9,324 @@
|
|
9
9
|
# modify this program under the same terms of ruby itself ---
|
10
10
|
# Ruby Distribution License or GNU General Public License.
|
11
11
|
#
|
12
|
-
|
13
|
-
require 'Win32API'
|
14
|
-
require 'base64'
|
15
|
-
|
16
|
-
# Implements bindings to Win32 SSPI functions, focused on authentication to a proxy server over HTTP.
|
17
|
-
module Win32
|
18
|
-
module SSPI
|
19
|
-
# Specifies how credential structure requested will be used. Only SECPKG_CRED_OUTBOUND is used
|
20
|
-
# here.
|
21
|
-
SECPKG_CRED_INBOUND = 0x00000001
|
22
|
-
SECPKG_CRED_OUTBOUND = 0x00000002
|
23
|
-
SECPKG_CRED_BOTH = 0x00000003
|
24
|
-
|
25
|
-
# Format of token. NETWORK format is used here.
|
26
|
-
SECURITY_NATIVE_DREP = 0x00000010
|
27
|
-
SECURITY_NETWORK_DREP = 0x00000000
|
28
|
-
|
29
|
-
# InitializeSecurityContext Requirement flags
|
30
|
-
ISC_REQ_REPLAY_DETECT = 0x00000004
|
31
|
-
ISC_REQ_SEQUENCE_DETECT = 0x00000008
|
32
|
-
ISC_REQ_CONFIDENTIALITY = 0x00000010
|
33
|
-
ISC_REQ_USE_SESSION_KEY = 0x00000020
|
34
|
-
ISC_REQ_PROMPT_FOR_CREDS = 0x00000040
|
35
|
-
ISC_REQ_CONNECTION = 0x00000800
|
36
|
-
|
37
|
-
# Win32 API Functions. Uses Win32API to bind methods to constants contained in class.
|
38
|
-
module API
|
39
|
-
# Can be called with AcquireCredentialsHandle.call()
|
40
|
-
AcquireCredentialsHandle = Win32API.new("secur32", "AcquireCredentialsHandle", 'ppLpppppp', 'L')
|
41
|
-
# Can be called with InitializeSecurityContext.call()
|
42
|
-
InitializeSecurityContext = Win32API.new("secur32", "InitializeSecurityContext", 'pppLLLpLpppp', 'L')
|
43
|
-
# Can be called with DeleteSecurityContext.call()
|
44
|
-
DeleteSecurityContext = Win32API.new("secur32", "DeleteSecurityContext", 'P', 'L')
|
45
|
-
# Can be called with FreeCredentialsHandle.call()
|
46
|
-
FreeCredentialsHandle = Win32API.new("secur32", "FreeCredentialsHandle", 'P', 'L')
|
47
|
-
end
|
48
|
-
|
49
|
-
# SecHandle struct
|
50
|
-
class SecurityHandle
|
51
|
-
def upper
|
52
|
-
@struct.unpack("LL")[1]
|
53
|
-
end
|
54
|
-
|
55
|
-
def lower
|
56
|
-
@struct.unpack("LL")[0]
|
57
|
-
end
|
58
|
-
|
59
|
-
def to_p
|
60
|
-
@struct ||= "\0" * 8
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
# Some familiar aliases for the SecHandle structure
|
65
|
-
CredHandle = CtxtHandle = SecurityHandle
|
66
|
-
|
67
|
-
# TimeStamp struct
|
68
|
-
class TimeStamp
|
69
|
-
attr_reader :struct
|
70
|
-
|
71
|
-
def to_p
|
72
|
-
@struct ||= "\0" * 8
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
|
-
# Creates binary representaiton of a SecBufferDesc structure,
|
77
|
-
# including the SecBuffer contained inside.
|
78
|
-
class SecurityBuffer
|
79
|
-
|
80
|
-
SECBUFFER_TOKEN = 2 # Security token
|
81
|
-
|
82
|
-
TOKENBUFSIZE = 12288
|
83
|
-
SECBUFFER_VERSION = 0
|
84
|
-
|
85
|
-
def initialize(buffer = nil)
|
86
|
-
@buffer = buffer || "\0" * TOKENBUFSIZE
|
87
|
-
@bufferSize = @buffer.length
|
88
|
-
@type = SECBUFFER_TOKEN
|
89
|
-
end
|
90
|
-
|
91
|
-
def bufferSize
|
92
|
-
unpack
|
93
|
-
@bufferSize
|
94
|
-
end
|
95
|
-
|
96
|
-
def bufferType
|
97
|
-
unpack
|
98
|
-
@type
|
99
|
-
end
|
100
|
-
|
101
|
-
def token
|
102
|
-
unpack
|
103
|
-
@buffer
|
104
|
-
end
|
105
|
-
|
106
|
-
def to_p
|
107
|
-
# Assumption is that when to_p is called we are going to get a packed structure. Therefore,
|
108
|
-
# set @unpacked back to nil so we know to unpack when accessors are next accessed.
|
109
|
-
@unpacked = nil
|
110
|
-
# Assignment of inner structure to variable is very important here. Without it,
|
111
|
-
# will not be able to unpack changes to the structure. Alternative, nested unpacks,
|
112
|
-
# does not work (i.e. @struct.unpack("LLP12")[2].unpack("LLP12") results in "no associated pointer")
|
113
|
-
@sec_buffer ||= [@bufferSize, @type, @buffer].pack("LLP")
|
114
|
-
@struct ||= [SECBUFFER_VERSION, 1, @sec_buffer].pack("LLP")
|
115
|
-
end
|
116
|
-
|
117
|
-
private
|
118
|
-
|
119
|
-
# Unpacks the SecurityBufferDesc structure into member variables. We
|
120
|
-
# only want to do this once per struct, so the struct is deleted
|
121
|
-
# after unpacking.
|
122
|
-
def unpack
|
123
|
-
if ! @unpacked && @sec_buffer && @struct
|
124
|
-
@bufferSize, @type = @sec_buffer.unpack("LL")
|
125
|
-
@buffer = @sec_buffer.unpack("LLP#{@bufferSize}")[2]
|
126
|
-
@struct = nil
|
127
|
-
@sec_buffer = nil
|
128
|
-
@unpacked = true
|
129
|
-
end
|
130
|
-
end
|
131
|
-
end
|
132
|
-
|
133
|
-
# SEC_WINNT_AUTH_IDENTITY structure
|
134
|
-
class Identity
|
135
|
-
SEC_WINNT_AUTH_IDENTITY_ANSI = 0x1
|
136
|
-
|
137
|
-
attr_accessor :user, :domain, :password
|
138
|
-
|
139
|
-
def initialize(user = nil, domain = nil, password = nil)
|
140
|
-
@user = user
|
141
|
-
@domain = domain
|
142
|
-
@password = password
|
143
|
-
@flags = SEC_WINNT_AUTH_IDENTITY_ANSI
|
144
|
-
end
|
145
|
-
|
146
|
-
def to_p
|
147
|
-
[@user, @user ? @user.length : 0,
|
148
|
-
@domain, @domain ? @domain.length : 0,
|
149
|
-
@password, @password ? @password.length : 0,
|
150
|
-
@flags].pack("PLPLPLL")
|
151
|
-
end
|
152
|
-
end
|
153
|
-
|
154
|
-
# Takes a return result from an SSPI function and interprets the value.
|
155
|
-
class SSPIResult
|
156
|
-
# Good results
|
157
|
-
SEC_E_OK = 0x00000000
|
158
|
-
SEC_I_CONTINUE_NEEDED = 0x00090312
|
159
|
-
|
160
|
-
# These are generally returned by InitializeSecurityContext
|
161
|
-
SEC_E_INSUFFICIENT_MEMORY = 0x80090300
|
162
|
-
SEC_E_INTERNAL_ERROR = 0x80090304
|
163
|
-
SEC_E_INVALID_HANDLE = 0x80090301
|
164
|
-
SEC_E_INVALID_TOKEN = 0x80090308
|
165
|
-
SEC_E_LOGON_DENIED = 0x8009030C
|
166
|
-
SEC_E_NO_AUTHENTICATING_AUTHORITY = 0x80090311
|
167
|
-
SEC_E_NO_CREDENTIALS = 0x8009030E
|
168
|
-
SEC_E_TARGET_UNKNOWN = 0x80090303
|
169
|
-
SEC_E_UNSUPPORTED_FUNCTION = 0x80090302
|
170
|
-
SEC_E_WRONG_PRINCIPAL = 0x80090322
|
171
|
-
|
172
|
-
# These are generally returned by AcquireCredentialsHandle
|
173
|
-
SEC_E_NOT_OWNER = 0x80090306
|
174
|
-
SEC_E_SECPKG_NOT_FOUND = 0x80090305
|
175
|
-
SEC_E_UNKNOWN_CREDENTIALS = 0x8009030D
|
176
|
-
|
177
|
-
@@map = {}
|
178
|
-
constants.each { |v| @@map[self.const_get(v.to_s)] = v }
|
179
|
-
|
180
|
-
attr_reader :value
|
181
|
-
|
182
|
-
def initialize(value)
|
183
|
-
# convert to unsigned long
|
184
|
-
value = [value].pack("L").unpack("L").first
|
185
|
-
raise "#{value.to_s(16)} is not a recognized result" unless @@map.has_key? value
|
186
|
-
@value = value
|
187
|
-
end
|
188
|
-
|
189
|
-
def to_s
|
190
|
-
@@map[@value].to_s
|
191
|
-
end
|
192
|
-
|
193
|
-
def ok?
|
194
|
-
@value == SEC_I_CONTINUE_NEEDED || @value == SEC_E_OK
|
195
|
-
end
|
196
|
-
|
197
|
-
def ==(other)
|
198
|
-
if other.is_a?(SSPIResult)
|
199
|
-
@value == other.value
|
200
|
-
elsif other.is_a?(Fixnum)
|
201
|
-
@value == @@map[other]
|
202
|
-
else
|
203
|
-
false
|
204
|
-
end
|
205
|
-
end
|
206
|
-
end
|
207
|
-
|
208
|
-
# Handles "Negotiate" type authentication. Geared towards authenticating with a proxy server over HTTP
|
209
|
-
class NegotiateAuth
|
210
|
-
attr_accessor :credentials, :context, :contextAttributes, :user, :domain
|
211
|
-
|
212
|
-
# Default request flags for SSPI functions
|
213
|
-
REQUEST_FLAGS = ISC_REQ_CONFIDENTIALITY | ISC_REQ_REPLAY_DETECT | ISC_REQ_CONNECTION
|
214
|
-
|
215
|
-
# NTLM tokens start with this header always. Encoding alone adds "==" and newline, so remove those
|
216
|
-
B64_TOKEN_PREFIX = Base64.encode64("NTLMSSP").delete("=\n")
|
217
|
-
|
218
|
-
# Given a connection and a request path, performs authentication as the current user and returns
|
219
|
-
# the response from a GET request. The connnection should be a Net::HTTP object, and it should
|
220
|
-
# have been constructed using the Net::HTTP.Proxy method, but anything that responds to "get" will work.
|
221
|
-
# If a user and domain are given, will authenticate as the given user.
|
222
|
-
# Returns the response received from the get method (usually Net::HTTPResponse)
|
223
|
-
def NegotiateAuth.proxy_auth_get(http, path, user = nil, domain = nil)
|
224
|
-
raise "http must respond to :get" unless http.respond_to?(:get)
|
225
|
-
nego_auth = self.new user, domain
|
226
|
-
|
227
|
-
resp = http.get path, { "Proxy-Authorization" => "Negotiate " + nego_auth.get_initial_token }
|
228
|
-
if resp["Proxy-Authenticate"]
|
229
|
-
resp = http.get path, { "Proxy-Authorization" => "Negotiate " + nego_auth.complete_authentication(resp["Proxy-Authenticate"].split(" ").last.strip) }
|
230
|
-
end
|
231
|
-
|
232
|
-
resp
|
233
|
-
end
|
234
|
-
|
235
|
-
# Creates a new instance ready for authentication as the given user in the given domain.
|
236
|
-
# Defaults to current user and domain as defined by ENV["USERDOMAIN"] and ENV["USERNAME"] if
|
237
|
-
# no arguments are supplied.
|
238
|
-
def initialize(user = nil, domain = nil)
|
239
|
-
if user.nil? && domain.nil? && ENV["USERNAME"].nil? && ENV["USERDOMAIN"].nil?
|
240
|
-
raise "A username or domain must be supplied since they cannot be retrieved from the environment"
|
241
|
-
end
|
242
|
-
|
243
|
-
@user = user || ENV["USERNAME"]
|
244
|
-
@domain = domain || ENV["USERDOMAIN"]
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
#
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
@
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
#
|
269
|
-
#
|
270
|
-
#
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
token
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
API::
|
310
|
-
@context
|
311
|
-
@
|
312
|
-
@
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
12
|
+
|
13
|
+
require 'Win32API'
|
14
|
+
require 'base64'
|
15
|
+
|
16
|
+
# Implements bindings to Win32 SSPI functions, focused on authentication to a proxy server over HTTP.
|
17
|
+
module Win32
|
18
|
+
module SSPI
|
19
|
+
# Specifies how credential structure requested will be used. Only SECPKG_CRED_OUTBOUND is used
|
20
|
+
# here.
|
21
|
+
SECPKG_CRED_INBOUND = 0x00000001
|
22
|
+
SECPKG_CRED_OUTBOUND = 0x00000002
|
23
|
+
SECPKG_CRED_BOTH = 0x00000003
|
24
|
+
|
25
|
+
# Format of token. NETWORK format is used here.
|
26
|
+
SECURITY_NATIVE_DREP = 0x00000010
|
27
|
+
SECURITY_NETWORK_DREP = 0x00000000
|
28
|
+
|
29
|
+
# InitializeSecurityContext Requirement flags
|
30
|
+
ISC_REQ_REPLAY_DETECT = 0x00000004
|
31
|
+
ISC_REQ_SEQUENCE_DETECT = 0x00000008
|
32
|
+
ISC_REQ_CONFIDENTIALITY = 0x00000010
|
33
|
+
ISC_REQ_USE_SESSION_KEY = 0x00000020
|
34
|
+
ISC_REQ_PROMPT_FOR_CREDS = 0x00000040
|
35
|
+
ISC_REQ_CONNECTION = 0x00000800
|
36
|
+
|
37
|
+
# Win32 API Functions. Uses Win32API to bind methods to constants contained in class.
|
38
|
+
module API
|
39
|
+
# Can be called with AcquireCredentialsHandle.call()
|
40
|
+
AcquireCredentialsHandle = Win32API.new("secur32", "AcquireCredentialsHandle", 'ppLpppppp', 'L')
|
41
|
+
# Can be called with InitializeSecurityContext.call()
|
42
|
+
InitializeSecurityContext = Win32API.new("secur32", "InitializeSecurityContext", 'pppLLLpLpppp', 'L')
|
43
|
+
# Can be called with DeleteSecurityContext.call()
|
44
|
+
DeleteSecurityContext = Win32API.new("secur32", "DeleteSecurityContext", 'P', 'L')
|
45
|
+
# Can be called with FreeCredentialsHandle.call()
|
46
|
+
FreeCredentialsHandle = Win32API.new("secur32", "FreeCredentialsHandle", 'P', 'L')
|
47
|
+
end
|
48
|
+
|
49
|
+
# SecHandle struct
|
50
|
+
class SecurityHandle
|
51
|
+
def upper
|
52
|
+
@struct.unpack("LL")[1]
|
53
|
+
end
|
54
|
+
|
55
|
+
def lower
|
56
|
+
@struct.unpack("LL")[0]
|
57
|
+
end
|
58
|
+
|
59
|
+
def to_p
|
60
|
+
@struct ||= "\0" * 8
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Some familiar aliases for the SecHandle structure
|
65
|
+
CredHandle = CtxtHandle = SecurityHandle
|
66
|
+
|
67
|
+
# TimeStamp struct
|
68
|
+
class TimeStamp
|
69
|
+
attr_reader :struct
|
70
|
+
|
71
|
+
def to_p
|
72
|
+
@struct ||= "\0" * 8
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Creates binary representaiton of a SecBufferDesc structure,
|
77
|
+
# including the SecBuffer contained inside.
|
78
|
+
class SecurityBuffer
|
79
|
+
|
80
|
+
SECBUFFER_TOKEN = 2 # Security token
|
81
|
+
|
82
|
+
TOKENBUFSIZE = 12288
|
83
|
+
SECBUFFER_VERSION = 0
|
84
|
+
|
85
|
+
def initialize(buffer = nil)
|
86
|
+
@buffer = buffer || "\0" * TOKENBUFSIZE
|
87
|
+
@bufferSize = @buffer.length
|
88
|
+
@type = SECBUFFER_TOKEN
|
89
|
+
end
|
90
|
+
|
91
|
+
def bufferSize
|
92
|
+
unpack
|
93
|
+
@bufferSize
|
94
|
+
end
|
95
|
+
|
96
|
+
def bufferType
|
97
|
+
unpack
|
98
|
+
@type
|
99
|
+
end
|
100
|
+
|
101
|
+
def token
|
102
|
+
unpack
|
103
|
+
@buffer
|
104
|
+
end
|
105
|
+
|
106
|
+
def to_p
|
107
|
+
# Assumption is that when to_p is called we are going to get a packed structure. Therefore,
|
108
|
+
# set @unpacked back to nil so we know to unpack when accessors are next accessed.
|
109
|
+
@unpacked = nil
|
110
|
+
# Assignment of inner structure to variable is very important here. Without it,
|
111
|
+
# will not be able to unpack changes to the structure. Alternative, nested unpacks,
|
112
|
+
# does not work (i.e. @struct.unpack("LLP12")[2].unpack("LLP12") results in "no associated pointer")
|
113
|
+
@sec_buffer ||= [@bufferSize, @type, @buffer].pack("LLP")
|
114
|
+
@struct ||= [SECBUFFER_VERSION, 1, @sec_buffer].pack("LLP")
|
115
|
+
end
|
116
|
+
|
117
|
+
private
|
118
|
+
|
119
|
+
# Unpacks the SecurityBufferDesc structure into member variables. We
|
120
|
+
# only want to do this once per struct, so the struct is deleted
|
121
|
+
# after unpacking.
|
122
|
+
def unpack
|
123
|
+
if ! @unpacked && @sec_buffer && @struct
|
124
|
+
@bufferSize, @type = @sec_buffer.unpack("LL")
|
125
|
+
@buffer = @sec_buffer.unpack("LLP#{@bufferSize}")[2]
|
126
|
+
@struct = nil
|
127
|
+
@sec_buffer = nil
|
128
|
+
@unpacked = true
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# SEC_WINNT_AUTH_IDENTITY structure
|
134
|
+
class Identity
|
135
|
+
SEC_WINNT_AUTH_IDENTITY_ANSI = 0x1
|
136
|
+
|
137
|
+
attr_accessor :user, :domain, :password
|
138
|
+
|
139
|
+
def initialize(user = nil, domain = nil, password = nil)
|
140
|
+
@user = user
|
141
|
+
@domain = domain
|
142
|
+
@password = password
|
143
|
+
@flags = SEC_WINNT_AUTH_IDENTITY_ANSI
|
144
|
+
end
|
145
|
+
|
146
|
+
def to_p
|
147
|
+
[@user, @user ? @user.length : 0,
|
148
|
+
@domain, @domain ? @domain.length : 0,
|
149
|
+
@password, @password ? @password.length : 0,
|
150
|
+
@flags].pack("PLPLPLL")
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# Takes a return result from an SSPI function and interprets the value.
|
155
|
+
class SSPIResult
|
156
|
+
# Good results
|
157
|
+
SEC_E_OK = 0x00000000
|
158
|
+
SEC_I_CONTINUE_NEEDED = 0x00090312
|
159
|
+
|
160
|
+
# These are generally returned by InitializeSecurityContext
|
161
|
+
SEC_E_INSUFFICIENT_MEMORY = 0x80090300
|
162
|
+
SEC_E_INTERNAL_ERROR = 0x80090304
|
163
|
+
SEC_E_INVALID_HANDLE = 0x80090301
|
164
|
+
SEC_E_INVALID_TOKEN = 0x80090308
|
165
|
+
SEC_E_LOGON_DENIED = 0x8009030C
|
166
|
+
SEC_E_NO_AUTHENTICATING_AUTHORITY = 0x80090311
|
167
|
+
SEC_E_NO_CREDENTIALS = 0x8009030E
|
168
|
+
SEC_E_TARGET_UNKNOWN = 0x80090303
|
169
|
+
SEC_E_UNSUPPORTED_FUNCTION = 0x80090302
|
170
|
+
SEC_E_WRONG_PRINCIPAL = 0x80090322
|
171
|
+
|
172
|
+
# These are generally returned by AcquireCredentialsHandle
|
173
|
+
SEC_E_NOT_OWNER = 0x80090306
|
174
|
+
SEC_E_SECPKG_NOT_FOUND = 0x80090305
|
175
|
+
SEC_E_UNKNOWN_CREDENTIALS = 0x8009030D
|
176
|
+
|
177
|
+
@@map = {}
|
178
|
+
constants.each { |v| @@map[self.const_get(v.to_s)] = v }
|
179
|
+
|
180
|
+
attr_reader :value
|
181
|
+
|
182
|
+
def initialize(value)
|
183
|
+
# convert to unsigned long
|
184
|
+
value = [value].pack("L").unpack("L").first
|
185
|
+
raise "#{value.to_s(16)} is not a recognized result" unless @@map.has_key? value
|
186
|
+
@value = value
|
187
|
+
end
|
188
|
+
|
189
|
+
def to_s
|
190
|
+
@@map[@value].to_s
|
191
|
+
end
|
192
|
+
|
193
|
+
def ok?
|
194
|
+
@value == SEC_I_CONTINUE_NEEDED || @value == SEC_E_OK
|
195
|
+
end
|
196
|
+
|
197
|
+
def ==(other)
|
198
|
+
if other.is_a?(SSPIResult)
|
199
|
+
@value == other.value
|
200
|
+
elsif other.is_a?(Fixnum)
|
201
|
+
@value == @@map[other]
|
202
|
+
else
|
203
|
+
false
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
# Handles "Negotiate" type authentication. Geared towards authenticating with a proxy server over HTTP
|
209
|
+
class NegotiateAuth
|
210
|
+
attr_accessor :credentials, :context, :contextAttributes, :user, :domain
|
211
|
+
|
212
|
+
# Default request flags for SSPI functions
|
213
|
+
REQUEST_FLAGS = ISC_REQ_CONFIDENTIALITY | ISC_REQ_REPLAY_DETECT | ISC_REQ_CONNECTION
|
214
|
+
|
215
|
+
# NTLM tokens start with this header always. Encoding alone adds "==" and newline, so remove those
|
216
|
+
B64_TOKEN_PREFIX = Base64.encode64("NTLMSSP").delete("=\n")
|
217
|
+
|
218
|
+
# Given a connection and a request path, performs authentication as the current user and returns
|
219
|
+
# the response from a GET request. The connnection should be a Net::HTTP object, and it should
|
220
|
+
# have been constructed using the Net::HTTP.Proxy method, but anything that responds to "get" will work.
|
221
|
+
# If a user and domain are given, will authenticate as the given user.
|
222
|
+
# Returns the response received from the get method (usually Net::HTTPResponse)
|
223
|
+
def NegotiateAuth.proxy_auth_get(http, path, user = nil, domain = nil)
|
224
|
+
raise "http must respond to :get" unless http.respond_to?(:get)
|
225
|
+
nego_auth = self.new user, domain
|
226
|
+
|
227
|
+
resp = http.get path, { "Proxy-Authorization" => "Negotiate " + nego_auth.get_initial_token }
|
228
|
+
if resp["Proxy-Authenticate"]
|
229
|
+
resp = http.get path, { "Proxy-Authorization" => "Negotiate " + nego_auth.complete_authentication(resp["Proxy-Authenticate"].split(" ").last.strip) }
|
230
|
+
end
|
231
|
+
|
232
|
+
resp
|
233
|
+
end
|
234
|
+
|
235
|
+
# Creates a new instance ready for authentication as the given user in the given domain.
|
236
|
+
# Defaults to current user and domain as defined by ENV["USERDOMAIN"] and ENV["USERNAME"] if
|
237
|
+
# no arguments are supplied.
|
238
|
+
def initialize(user = nil, domain = nil)
|
239
|
+
if user.nil? && domain.nil? && ENV["USERNAME"].nil? && ENV["USERDOMAIN"].nil?
|
240
|
+
raise "A username or domain must be supplied since they cannot be retrieved from the environment"
|
241
|
+
end
|
242
|
+
|
243
|
+
@user = user || ENV["USERNAME"]
|
244
|
+
@domain = domain || ENV["USERDOMAIN"]
|
245
|
+
@cleaned_up = nil
|
246
|
+
end
|
247
|
+
|
248
|
+
# Gets the initial Negotiate token. Returns it as a base64 encoded string suitable for use in HTTP. Can
|
249
|
+
# be easily decoded, however.
|
250
|
+
def get_initial_token
|
251
|
+
raise "This object is no longer usable because its resources have been freed." if @cleaned_up
|
252
|
+
get_credentials
|
253
|
+
|
254
|
+
outputBuffer = SecurityBuffer.new
|
255
|
+
@context = CtxtHandle.new
|
256
|
+
@contextAttributes = "\0" * 4
|
257
|
+
|
258
|
+
result = SSPIResult.new(API::InitializeSecurityContext.call(@credentials.to_p, nil, nil,
|
259
|
+
REQUEST_FLAGS,0, SECURITY_NETWORK_DREP, nil, 0, @context.to_p, outputBuffer.to_p, @contextAttributes, TimeStamp.new.to_p))
|
260
|
+
|
261
|
+
if result.ok? then
|
262
|
+
return encode_token(outputBuffer.token)
|
263
|
+
else
|
264
|
+
raise "Error: #{result.to_s}"
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
# Takes a token and gets the next token in the Negotiate authentication chain. Token can be Base64 encoded or not.
|
269
|
+
# The token can include the "Negotiate" header and it will be stripped.
|
270
|
+
# Does not indicate if SEC_I_CONTINUE or SEC_E_OK was returned.
|
271
|
+
# Token returned is Base64 encoded w/ all new lines removed.
|
272
|
+
def complete_authentication(token)
|
273
|
+
raise "This object is no longer usable because its resources have been freed." if @cleaned_up
|
274
|
+
|
275
|
+
# Nil token OK, just set it to empty string
|
276
|
+
token = "" if token.nil?
|
277
|
+
|
278
|
+
if token.include? "Negotiate"
|
279
|
+
# If the Negotiate prefix is passed in, assume we are seeing "Negotiate <token>" and get the token.
|
280
|
+
token = token.split(" ").last
|
281
|
+
end
|
282
|
+
|
283
|
+
if token.include? B64_TOKEN_PREFIX
|
284
|
+
# indicates base64 encoded token
|
285
|
+
token = Base64.decode64(token.strip)
|
286
|
+
end
|
287
|
+
|
288
|
+
outputBuffer = SecurityBuffer.new
|
289
|
+
result = SSPIResult.new(API::InitializeSecurityContext.call(@credentials.to_p, @context.to_p, nil,
|
290
|
+
REQUEST_FLAGS, 0, SECURITY_NETWORK_DREP, SecurityBuffer.new(token).to_p, 0,
|
291
|
+
@context.to_p,
|
292
|
+
outputBuffer.to_p, @contextAttributes, TimeStamp.new.to_p))
|
293
|
+
|
294
|
+
if result.ok? then
|
295
|
+
return encode_token(outputBuffer.token)
|
296
|
+
else
|
297
|
+
raise "Error: #{result.to_s}"
|
298
|
+
end
|
299
|
+
ensure
|
300
|
+
# need to make sure we don't clean up if we've already cleaned up.
|
301
|
+
clean_up unless @cleaned_up
|
302
|
+
end
|
303
|
+
|
304
|
+
private
|
305
|
+
|
306
|
+
def clean_up
|
307
|
+
# free structures allocated
|
308
|
+
@cleaned_up = true
|
309
|
+
API::FreeCredentialsHandle.call(@credentials.to_p)
|
310
|
+
API::DeleteSecurityContext.call(@context.to_p)
|
311
|
+
@context = nil
|
312
|
+
@credentials = nil
|
313
|
+
@contextAttributes = nil
|
314
|
+
end
|
315
|
+
|
316
|
+
# Gets credentials based on user, domain or both. If both are nil, an error occurs
|
317
|
+
def get_credentials
|
318
|
+
@credentials = CredHandle.new
|
319
|
+
ts = TimeStamp.new
|
320
|
+
@identity = Identity.new @user, @domain
|
321
|
+
result = SSPIResult.new(API::AcquireCredentialsHandle.call(nil, "Negotiate", SECPKG_CRED_OUTBOUND, nil, @identity.to_p,
|
322
|
+
nil, nil, @credentials.to_p, ts.to_p))
|
323
|
+
raise "Error acquire credentials: #{result}" unless result.ok?
|
324
|
+
end
|
325
|
+
|
326
|
+
def encode_token(t)
|
327
|
+
# encode64 will add newlines every 60 characters so we need to remove those.
|
328
|
+
Base64.encode64(t).delete("\n")
|
329
|
+
end
|
330
|
+
end
|
331
|
+
end
|
331
332
|
end
|
@@ -26,6 +26,10 @@ module Net
|
|
26
26
|
def self.sspi?
|
27
27
|
true
|
28
28
|
end
|
29
|
+
|
30
|
+
# Suppress warnings about redefining request
|
31
|
+
@old_verbose = $VERBOSE
|
32
|
+
$VERBOSE = nil
|
29
33
|
|
30
34
|
def request(req, body = nil, &block) # :yield: +response+
|
31
35
|
unless started?
|
@@ -84,5 +88,7 @@ module Net
|
|
84
88
|
|
85
89
|
res
|
86
90
|
end
|
91
|
+
|
92
|
+
$VERBOSE = @old_verbose
|
87
93
|
end
|
88
94
|
end
|
data/test/test_ruby_sspi.rb
CHANGED
@@ -9,91 +9,98 @@
|
|
9
9
|
# modify this program under the same terms of ruby itself ---
|
10
10
|
# Ruby Distribution License or GNU General Public License.
|
11
11
|
#
|
12
|
-
|
13
|
-
#
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
proxy
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
resp
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
end
|
12
|
+
|
13
|
+
# Ruby gems > 0.9 loads patch automatically so DISABLE_RUBY_SSPI_PATCH doesn't do any good.
|
14
|
+
# Therefore, need to ensure ruby gems isn't loaded automatically.
|
15
|
+
if ENV["RUBYOPT"]
|
16
|
+
puts "Unset RUBYOPT environment variable before running these tests."
|
17
|
+
exit!
|
18
|
+
end
|
19
|
+
|
20
|
+
# Magic constant will ensure that, if the SSPI patch has been applied, it won't break these tests
|
21
|
+
DISABLE_RUBY_SSPI_PATCH = true
|
22
|
+
|
23
|
+
require 'test/unit'
|
24
|
+
require 'net/http'
|
25
|
+
require 'pathname'
|
26
|
+
$: << (File.dirname(__FILE__) << "/../lib")
|
27
|
+
require 'win32/sspi'
|
28
|
+
|
29
|
+
class NTLMTest < Test::Unit::TestCase
|
30
|
+
def test_auth
|
31
|
+
proxy = get_proxy
|
32
|
+
|
33
|
+
Net::HTTP.Proxy(proxy.host, proxy.port).start("www.google.com") do |http|
|
34
|
+
nego_auth = Win32::SSPI::NegotiateAuth.new
|
35
|
+
sr = http.request_get "/", { "Proxy-Authorization" => "Negotiate " + nego_auth.get_initial_token }
|
36
|
+
resp = http.get "/", { "Proxy-Authorization" => "Negotiate " + nego_auth.complete_authentication(sr["Proxy-Authenticate"].split(" ").last.strip) }
|
37
|
+
# Google redirects to country of origins domain if not US.
|
38
|
+
assert success_or_redirect(resp.code), "Response code not as expected: #{resp.inspect}"
|
39
|
+
resp = http.get "/foobar.html"
|
40
|
+
# Some proxy servers don't return 404 but 407.
|
41
|
+
assert(resp.code.to_i == 404 || resp.code.to_i == 407, "Response code not as expected: #{resp.inspect}")
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_proxy_auth_get
|
46
|
+
proxy = get_proxy
|
47
|
+
|
48
|
+
Net::HTTP.Proxy(proxy.host, proxy.port).start("www.google.com") do |http|
|
49
|
+
resp = Win32::SSPI::NegotiateAuth.proxy_auth_get http, "/"
|
50
|
+
assert success_or_redirect(resp.code), "Response code not as expected: #{resp.inspect}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def test_one_time_use_only
|
55
|
+
proxy = get_proxy
|
56
|
+
|
57
|
+
Net::HTTP.Proxy(proxy.host, proxy.port).start("www.google.com") do |http|
|
58
|
+
nego_auth = Win32::SSPI::NegotiateAuth.new
|
59
|
+
sr = http.request_get "/", { "Proxy-Authorization" => "Negotiate " + nego_auth.get_initial_token }
|
60
|
+
resp = http.get "/", { "Proxy-Authorization" => "Negotiate " + nego_auth.complete_authentication(sr["Proxy-Authenticate"].split(" ").last.strip) }
|
61
|
+
assert success_or_redirect(resp.code), "Response code not as expected: #{resp.inspect}"
|
62
|
+
assert_raises(RuntimeError, "Should not be able to call complete_authentication again") do
|
63
|
+
nego_auth.complete_authentication "foo"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def test_token_variations
|
69
|
+
proxy = get_proxy
|
70
|
+
|
71
|
+
# Test that raw token works
|
72
|
+
Net::HTTP.Proxy(proxy.host, proxy.port).start("www.google.com") do |http|
|
73
|
+
nego_auth = Win32::SSPI::NegotiateAuth.new
|
74
|
+
sr = http.request_get "/", { "Proxy-Authorization" => "Negotiate " + nego_auth.get_initial_token }
|
75
|
+
token = Base64.decode64(sr["Proxy-Authenticate"].split(" ").last.strip)
|
76
|
+
completed_token = nego_auth.complete_authentication(token)
|
77
|
+
resp = http.get "/", { "Proxy-Authorization" => "Negotiate " + completed_token }
|
78
|
+
assert success_or_redirect(resp.code), "Response code not as expected: #{resp.inspect}"
|
79
|
+
end
|
80
|
+
|
81
|
+
# Test that token w/ "Negotiate" header included works
|
82
|
+
Net::HTTP.Proxy(proxy.host, proxy.port).start("www.google.com") do |http|
|
83
|
+
nego_auth = Win32::SSPI::NegotiateAuth.new
|
84
|
+
sr = http.request_get "/", { "Proxy-Authorization" => "Negotiate " + nego_auth.get_initial_token }
|
85
|
+
resp = http.get "/", { "Proxy-Authorization" => "Negotiate " + nego_auth.complete_authentication(sr["Proxy-Authenticate"]) }
|
86
|
+
assert success_or_redirect(resp.code), "Response code not as expected: #{resp.inspect}"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
# Gets the proxy from the environment and makes some assertions
|
93
|
+
def get_proxy
|
94
|
+
assert ENV["http_proxy"], "http_proxy environment variable must be set."
|
95
|
+
proxy = URI.parse(ENV["http_proxy"])
|
96
|
+
assert proxy.host && proxy.port, "Could not parse http_proxy (#{ENV["http_proxy"]}). http_proxy should be a URL with a port (e.g. http://proxy.corp.com:8080)."
|
97
|
+
|
98
|
+
return proxy
|
99
|
+
end
|
100
|
+
|
101
|
+
# Returns true if code given is 200 or 302. I.e. if HTTP request was successful or resulted in redirect.
|
102
|
+
def success_or_redirect(code)
|
103
|
+
code.to_i == 200 || code.to_i == 302
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
metadata
CHANGED
@@ -3,8 +3,8 @@ rubygems_version: 0.9.2
|
|
3
3
|
specification_version: 1
|
4
4
|
name: rubysspi
|
5
5
|
version: !ruby/object:Gem::Version
|
6
|
-
version: 1.2.
|
7
|
-
date: 2007-
|
6
|
+
version: 1.2.3
|
7
|
+
date: 2007-07-10 00:00:00 -07:00
|
8
8
|
summary: A library which implements Ruby bindings to the Win32 SSPI library. Also includes a module to add Negotiate authentication support to Net::HTTP.
|
9
9
|
require_paths:
|
10
10
|
- lib
|