rubysspi 1.2.2 → 1.2.3

Sign up to get free protection for your applications and to get access to all the features.
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
- - 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
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.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/"
@@ -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
- end
246
-
247
- # Gets the initial Negotiate token. Returns it as a base64 encoded string suitable for use in HTTP. Can
248
- # be easily decoded, however.
249
- def get_initial_token
250
- raise "This object is no longer usable because its resources have been freed." if @cleaned_up
251
- get_credentials
252
-
253
- outputBuffer = SecurityBuffer.new
254
- @context = CtxtHandle.new
255
- @contextAttributes = "\0" * 4
256
-
257
- result = SSPIResult.new(API::InitializeSecurityContext.call(@credentials.to_p, nil, nil,
258
- REQUEST_FLAGS,0, SECURITY_NETWORK_DREP, nil, 0, @context.to_p, outputBuffer.to_p, @contextAttributes, TimeStamp.new.to_p))
259
-
260
- if result.ok? then
261
- return encode_token(outputBuffer.token)
262
- else
263
- raise "Error: #{result.to_s}"
264
- end
265
- end
266
-
267
- # Takes a token and gets the next token in the Negotiate authentication chain. Token can be Base64 encoded or not.
268
- # The token can include the "Negotiate" header and it will be stripped.
269
- # Does not indicate if SEC_I_CONTINUE or SEC_E_OK was returned.
270
- # Token returned is Base64 encoded w/ all new lines removed.
271
- def complete_authentication(token)
272
- raise "This object is no longer usable because its resources have been freed." if @cleaned_up
273
-
274
- # Nil token OK, just set it to empty string
275
- token = "" if token.nil?
276
-
277
- if token.include? "Negotiate"
278
- # If the Negotiate prefix is passed in, assume we are seeing "Negotiate <token>" and get the token.
279
- token = token.split(" ").last
280
- end
281
-
282
- if token.include? B64_TOKEN_PREFIX
283
- # indicates base64 encoded token
284
- token = Base64.decode64(token.strip)
285
- end
286
-
287
- outputBuffer = SecurityBuffer.new
288
- result = SSPIResult.new(API::InitializeSecurityContext.call(@credentials.to_p, @context.to_p, nil,
289
- REQUEST_FLAGS, 0, SECURITY_NETWORK_DREP, SecurityBuffer.new(token).to_p, 0,
290
- @context.to_p,
291
- outputBuffer.to_p, @contextAttributes, TimeStamp.new.to_p))
292
-
293
- if result.ok? then
294
- return encode_token(outputBuffer.token)
295
- else
296
- raise "Error: #{result.to_s}"
297
- end
298
- ensure
299
- # need to make sure we don't clean up if we've already cleaned up.
300
- clean_up unless @cleaned_up
301
- end
302
-
303
- private
304
-
305
- def clean_up
306
- # free structures allocated
307
- @cleaned_up = true
308
- API::FreeCredentialsHandle.call(@credentials.to_p)
309
- API::DeleteSecurityContext.call(@context.to_p)
310
- @context = nil
311
- @credentials = nil
312
- @contextAttributes = nil
313
- end
314
-
315
- # Gets credentials based on user, domain or both. If both are nil, an error occurs
316
- def get_credentials
317
- @credentials = CredHandle.new
318
- ts = TimeStamp.new
319
- @identity = Identity.new @user, @domain
320
- result = SSPIResult.new(API::AcquireCredentialsHandle.call(nil, "Negotiate", SECPKG_CRED_OUTBOUND, nil, @identity.to_p,
321
- nil, nil, @credentials.to_p, ts.to_p))
322
- raise "Error acquire credentials: #{result}" unless result.ok?
323
- end
324
-
325
- def encode_token(t)
326
- # encode64 will add newlines every 60 characters so we need to remove those.
327
- Base64.encode64(t).delete("\n")
328
- end
329
- end
330
- end
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
@@ -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
- # Magic constant will ensure that, if the SSPI patch has been applied, it won't break these tests
14
- DISABLE_RUBY_SSPI_PATCH = true
15
-
16
- require 'test/unit'
17
- require 'net/http'
18
- require 'pathname'
19
- $: << (File.dirname(__FILE__) << "/../lib")
20
- require 'win32/sspi'
21
-
22
- class NTLMTest < Test::Unit::TestCase
23
- def test_auth
24
- proxy = get_proxy
25
-
26
- Net::HTTP.Proxy(proxy.host, proxy.port).start("www.google.com") do |http|
27
- nego_auth = Win32::SSPI::NegotiateAuth.new
28
- sr = http.request_get "/", { "Proxy-Authorization" => "Negotiate " + nego_auth.get_initial_token }
29
- resp = http.get "/", { "Proxy-Authorization" => "Negotiate " + nego_auth.complete_authentication(sr["Proxy-Authenticate"].split(" ").last.strip) }
30
- # Google redirects to country of origins domain if not US.
31
- assert success_or_redirect(resp.code), "Response code not as expected: #{resp.inspect}"
32
- resp = http.get "/foobar.html"
33
- # Some proxy servers don't return 404 but 407.
34
- assert(resp.code.to_i == 404 || resp.code.to_i == 407, "Response code not as expected: #{resp.inspect}")
35
- end
36
- end
37
-
38
- def test_proxy_auth_get
39
- proxy = get_proxy
40
-
41
- Net::HTTP.Proxy(proxy.host, proxy.port).start("www.google.com") do |http|
42
- resp = Win32::SSPI::NegotiateAuth.proxy_auth_get http, "/"
43
- assert success_or_redirect(resp.code), "Response code not as expected: #{resp.inspect}"
44
- end
45
- end
46
-
47
- def test_one_time_use_only
48
- proxy = get_proxy
49
-
50
- Net::HTTP.Proxy(proxy.host, proxy.port).start("www.google.com") do |http|
51
- nego_auth = Win32::SSPI::NegotiateAuth.new
52
- sr = http.request_get "/", { "Proxy-Authorization" => "Negotiate " + nego_auth.get_initial_token }
53
- resp = http.get "/", { "Proxy-Authorization" => "Negotiate " + nego_auth.complete_authentication(sr["Proxy-Authenticate"].split(" ").last.strip) }
54
- assert success_or_redirect(resp.code), "Response code not as expected: #{resp.inspect}"
55
- assert_raises(RuntimeError, "Should not be able to call complete_authentication again") do
56
- nego_auth.complete_authentication "foo"
57
- end
58
- end
59
- end
60
-
61
- def test_token_variations
62
- proxy = get_proxy
63
-
64
- # Test that raw token works
65
- Net::HTTP.Proxy(proxy.host, proxy.port).start("www.google.com") do |http|
66
- nego_auth = Win32::SSPI::NegotiateAuth.new
67
- sr = http.request_get "/", { "Proxy-Authorization" => "Negotiate " + nego_auth.get_initial_token }
68
- token = Base64.decode64(sr["Proxy-Authenticate"].split(" ").last.strip)
69
- completed_token = nego_auth.complete_authentication(token)
70
- resp = http.get "/", { "Proxy-Authorization" => "Negotiate " + completed_token }
71
- assert success_or_redirect(resp.code), "Response code not as expected: #{resp.inspect}"
72
- end
73
-
74
- # Test that token w/ "Negotiate" header included works
75
- Net::HTTP.Proxy(proxy.host, proxy.port).start("www.google.com") do |http|
76
- nego_auth = Win32::SSPI::NegotiateAuth.new
77
- sr = http.request_get "/", { "Proxy-Authorization" => "Negotiate " + nego_auth.get_initial_token }
78
- resp = http.get "/", { "Proxy-Authorization" => "Negotiate " + nego_auth.complete_authentication(sr["Proxy-Authenticate"]) }
79
- assert success_or_redirect(resp.code), "Response code not as expected: #{resp.inspect}"
80
- end
81
- end
82
-
83
- private
84
-
85
- # Gets the proxy from the environment and makes some assertions
86
- def get_proxy
87
- assert ENV["http_proxy"], "http_proxy environment variable must be set."
88
- proxy = URI.parse(ENV["http_proxy"])
89
- 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)."
90
-
91
- return proxy
92
- end
93
-
94
- # Returns true if code given is 200 or 302. I.e. if HTTP request was successful or resulted in redirect.
95
- def success_or_redirect(code)
96
- code.to_i == 200 || code.to_i == 302
97
- end
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.2
7
- date: 2007-03-12 00:00:00 -07:00
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