rubysspi 0.0.1-i386-mswin32
Sign up to get free protection for your applications and to get access to all the features.
- data/README.txt +93 -0
- data/Rakefile +39 -0
- data/lib/rubysspi.rb +303 -0
- data/lib/rubysspi/proxy_auth.rb +56 -0
- data/test/ruby_sspi_test.rb +54 -0
- data/test/test_net_http.rb +23 -0
- metadata +56 -0
data/README.txt
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
= Introduction
|
2
|
+
|
3
|
+
This library provides bindings to the Win32 SSPI libraries, which implement various security protocols for Windows. The library was primarily developed to give Negotiate/NTLM proxy authentication abilities to Net::HTTP, similar to support found in Internet Explorer or Firefox.
|
4
|
+
|
5
|
+
The libary is NOT an implementation of the NTLM protocol, and does not give the ability to authenticate as any given user. It is able to authenticate with a proxy server as the current user.
|
6
|
+
|
7
|
+
This project can be found on rubyforge at:
|
8
|
+
|
9
|
+
http://rubyforge.org/projects/rubysspi
|
10
|
+
|
11
|
+
= Using with open-uri
|
12
|
+
|
13
|
+
To use the library with open-uri, make sure to set the environment variable +http_proxy+ to your proxy server. This must be a hostname and port in URL form. E.g.:
|
14
|
+
|
15
|
+
http://proxy.corp.com:8080
|
16
|
+
|
17
|
+
The library will grab your current username and domain from the environment variables +USERNAME+ and +USERDOMAIN+. This should be set for you by Windows already.
|
18
|
+
|
19
|
+
The library implements a patch on top of Net::HTTP, which means open-uri gets it too. At the top of your script, make sure to require the patch after +open-uri+:
|
20
|
+
|
21
|
+
require +open-uri+
|
22
|
+
require +rubysspi/proxy_auth+
|
23
|
+
|
24
|
+
open("http://www.google.com") { |f| puts(f.gets(nil)) }
|
25
|
+
|
26
|
+
Note that this patch does NOT work with the +http_proxy_user+ and +http_proxy_password+ environment variables. The library will ONLY authenticate as the current user.
|
27
|
+
|
28
|
+
= Using with Net::HTTP
|
29
|
+
|
30
|
+
Net::HTTP will not use the proxy server supplied in the environment variable automatically, so you have to supply the proxy address yourself. Otherwise, it's exactly the same:
|
31
|
+
|
32
|
+
require 'net/http'
|
33
|
+
require 'rubysspi/proxy_auth+
|
34
|
+
|
35
|
+
Net::HTTP::Proxy("proxy.corp.com", 8080).start("www.google.com") do |http|
|
36
|
+
resp = http.request_get "/"
|
37
|
+
puts resp.body
|
38
|
+
end
|
39
|
+
|
40
|
+
= Using rubysspi directly
|
41
|
+
|
42
|
+
As stated, the library is geared primarily towards supporting Negotiate/NTLM authentication with proxy servers. In this vein, you can manually authenticate a given HTTP connection with a single call:
|
43
|
+
|
44
|
+
require 'rubysspi'
|
45
|
+
|
46
|
+
Net::HTTP.Proxy(proxy.host, proxy.port).start("www.google.com") do |http|
|
47
|
+
resp = SSPI::NegotiateAuth.proxy_auth_get http, "/"
|
48
|
+
end
|
49
|
+
|
50
|
+
The +resp+ variable will contain the response from Google, with any proxy authorization necessary taken care of automatically. Note that if the +http+ connection is not closed, any subsequent requests will NOT require authentication.
|
51
|
+
|
52
|
+
If the above method is used, it is recommended that you do NOT require the 'rubysspi/proxy_auth' library, as the interaction between the two will fail.
|
53
|
+
|
54
|
+
The library can be used directly to generate tokens appropriate for the current user, too.
|
55
|
+
|
56
|
+
To get started, first create an instance of the SSPI::NegotiateAuth class:
|
57
|
+
|
58
|
+
require 'rubysspi'
|
59
|
+
|
60
|
+
n = SSPI::NegotiateAuth.new
|
61
|
+
|
62
|
+
Next, get the first token by calling get_initial_token:
|
63
|
+
|
64
|
+
token = n.get_initial_token
|
65
|
+
|
66
|
+
This token returned will be Base64 encoded and can be directly placed in an HTTP header. This token can be easily decoded, however, and is usually an NTLM Type 1 message.
|
67
|
+
|
68
|
+
After getting a response from the server (usually an NTLM Type 2 message), pass it into the complete_authentication:
|
69
|
+
|
70
|
+
token = n.complete_authentication(server_token)
|
71
|
+
|
72
|
+
Note that server_token can be Base64 encoded or not, and if it starts with "Negotiate", that phrase will be stripped off. This allows the response from a Proxy-Authentication header to be passed into the method directly. The token can be decoded externally and passed in, too.
|
73
|
+
|
74
|
+
The token returned (usually an NTLM Type 3) message can then be sent to the server and the connection should be authenticated.
|
75
|
+
|
76
|
+
= Thanks & References
|
77
|
+
|
78
|
+
Many references were used in decoding both NTLM messages and integrating with the SSPI library. Among them are:
|
79
|
+
|
80
|
+
* Managed SSPI Sample - A .NET implementation of a simple client/server using SSPI. A complex undertaking but provides a great resource for playing with the API.
|
81
|
+
* http://msdn.microsoft.com/library/?url=/library/en-us/dndotnet/html/remsspi.asp?frame=true
|
82
|
+
* John Lam's RubyCLR - http://www.rubyclr.com
|
83
|
+
* Originally, I used RubyCLR to call into the Managed SSPI sample which really helped me decode what the SSPI interface did and how it worked. I did not end up using that implementation but it was great for research.
|
84
|
+
* The NTLM Authentication Protocol - The definitive explanation for the NTLM protocol (outside MS internal documents, I presume).
|
85
|
+
* http://davenport.sourceforge.net/ntlm.html
|
86
|
+
* Ruby/NTLM - A pure Ruby implementation of the NTLM protocol. Again, not used in this project but invaluable for decoding NTLM messages and figuring out what SSPI was returning.
|
87
|
+
* http://rubyforge.org/projects/rubyntlm/
|
88
|
+
* Seamonkey/Mozilla NTLM implementation - The only source for an implementation in an actual browser. How they figured out how to use SSPI themselves is beyond me.
|
89
|
+
* http://lxr.mozilla.org/seamonkey/source/mailnews/base/util/nsMsgProtocol.cpp#899
|
90
|
+
|
91
|
+
And of course, thanks to my Lord and Savior, Jesus Christ. In the name of the Father, the Son, and the Holy Spirit.
|
92
|
+
|
93
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
Gem::manage_gems
|
3
|
+
require 'rake/gempackagetask'
|
4
|
+
|
5
|
+
spec = Gem::Specification.new do |s|
|
6
|
+
s.name = "rubysspi"
|
7
|
+
s.summary = "A library which adds implements Ruby bindings to the Win32 SSPI library. Also includes a module to add Negotate authentication support to Net::HTTP."
|
8
|
+
s.version = "0.0.1"
|
9
|
+
s.author = "Justin Bailey"
|
10
|
+
s.email = "jgbailey @nospan@ gmail.com"
|
11
|
+
s.homepage = "http://rubyforge.org/projects/rubysspi/"
|
12
|
+
s.rubyforge_project = "http://rubyforge.org/projects/rubysspi/"
|
13
|
+
s.description = <<EOS
|
14
|
+
This gem provides bindings to the Win32 SSPI libraries, primarily to support Negotiate (i.e. SPNEGO, NTLM)
|
15
|
+
authentication with a proxy server. Enough support is implemented to provide the necessary support for
|
16
|
+
the authentication.
|
17
|
+
|
18
|
+
A module is also provided which overrides Net::HTTP and adds support for Negotiate authentication to it.
|
19
|
+
|
20
|
+
This implies that open-uri automatically gets support for it, as long as the http_proxy environment variable
|
21
|
+
is set.
|
22
|
+
EOS
|
23
|
+
|
24
|
+
s.platform = Gem::Platform::CURRENT
|
25
|
+
s.files = FileList["lib/**/*", "test/*", "*.txt", "Rakefile"].to_a
|
26
|
+
|
27
|
+
s.require_path = "lib"
|
28
|
+
s.autorequire = "rubysspi"
|
29
|
+
|
30
|
+
s.has_rdoc = true
|
31
|
+
s.extra_rdoc_files = ["README.txt"]
|
32
|
+
s.rdoc_options << '--title' << 'Ruby SSPI -- Win32 SSPI Bindings for Ruby' <<
|
33
|
+
'--main' << 'README.txt' <<
|
34
|
+
'--line-numbers'
|
35
|
+
end
|
36
|
+
|
37
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
38
|
+
pkg.need_tar = true
|
39
|
+
end
|
data/lib/rubysspi.rb
ADDED
@@ -0,0 +1,303 @@
|
|
1
|
+
require 'dl/win32'
|
2
|
+
require 'base64'
|
3
|
+
|
4
|
+
# Implements bindings to Win32 SSPI functions, focused on authentication to a proxy server over HTTP.
|
5
|
+
module SSPI
|
6
|
+
# Specifies how credential structure requested will be used. Only SECPKG_CRED_OUTBOUND is used
|
7
|
+
# here.
|
8
|
+
SECPKG_CRED_INBOUND = 0x00000001
|
9
|
+
SECPKG_CRED_OUTBOUND = 0x00000002
|
10
|
+
SECPKG_CRED_BOTH = 0x00000003
|
11
|
+
|
12
|
+
# Format of token. NETWORK format is used here.
|
13
|
+
SECURITY_NATIVE_DREP = 0x00000010
|
14
|
+
SECURITY_NETWORK_DREP = 0x00000000
|
15
|
+
|
16
|
+
# InitializeSecurityContext Requirement flags
|
17
|
+
ISC_REQ_REPLAY_DETECT = 0x00000004
|
18
|
+
ISC_REQ_SEQUENCE_DETECT = 0x00000008
|
19
|
+
ISC_REQ_CONFIDENTIALITY = 0x00000010
|
20
|
+
ISC_REQ_USE_SESSION_KEY = 0x00000020
|
21
|
+
ISC_REQ_PROMPT_FOR_CREDS = 0x00000040
|
22
|
+
ISC_REQ_CONNECTION = 0x00000800
|
23
|
+
|
24
|
+
# Win32 API Functions. Uses dl/win32 to bind methods to constants contained in class.
|
25
|
+
module API
|
26
|
+
# Can be called with AcquireCredentialsHandle.call()
|
27
|
+
AcquireCredentialsHandle = Win32API.new("secur32", "AcquireCredentialsHandle", ['ppLpppppp'], 'L')
|
28
|
+
# Can be called with AcquireCredentialsHandle.call()
|
29
|
+
InitializeSecurityContext = Win32API.new("secur32", "InitializeSecurityContext", [ 'pppLLLpLpppp'], 'L')
|
30
|
+
# Can be called with AcquireCredentialsHandle.call()
|
31
|
+
DeleteSecurityContext = Win32API.new("secur32", "DeleteSecurityContext", ['P'], 'L')
|
32
|
+
# Can be called with AcquireCredentialsHandle.call()
|
33
|
+
FreeCredentialsHandle = Win32API.new("secur32", "FreeCredentialsHandle", ['P'], 'L')
|
34
|
+
end
|
35
|
+
|
36
|
+
# SecHandle struct
|
37
|
+
class SecurityHandle
|
38
|
+
def upper
|
39
|
+
@struct.unpack("LL")[1]
|
40
|
+
end
|
41
|
+
|
42
|
+
def lower
|
43
|
+
@struct.unpack("LL")[0]
|
44
|
+
end
|
45
|
+
|
46
|
+
def to_p
|
47
|
+
@struct ||= "\0" * 8
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Some familiar aliases for the SecHandle structure
|
52
|
+
CredHandle = CtxtHandle = SecurityHandle
|
53
|
+
|
54
|
+
# TimeStamp struct
|
55
|
+
class TimeStamp
|
56
|
+
attr_reader :struct
|
57
|
+
|
58
|
+
def to_p
|
59
|
+
@struct ||= "\0" * 8
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Creates binary representaiton of a SecBufferDesc structure,
|
64
|
+
# including the SecBuffer contained inside.
|
65
|
+
class SecurityBuffer
|
66
|
+
|
67
|
+
SECBUFFER_TOKEN = 2 # Security token
|
68
|
+
|
69
|
+
TOKENBUFSIZE = 12288
|
70
|
+
SECBUFFER_VERSION = 0
|
71
|
+
|
72
|
+
def initialize(buffer = nil)
|
73
|
+
@buffer = buffer || "\0" * TOKENBUFSIZE
|
74
|
+
@bufferSize = @buffer.length
|
75
|
+
@type = SECBUFFER_TOKEN
|
76
|
+
end
|
77
|
+
|
78
|
+
def bufferSize
|
79
|
+
unpack
|
80
|
+
@bufferSize
|
81
|
+
end
|
82
|
+
|
83
|
+
def bufferType
|
84
|
+
unpack
|
85
|
+
@type
|
86
|
+
end
|
87
|
+
|
88
|
+
def token
|
89
|
+
unpack
|
90
|
+
@buffer
|
91
|
+
end
|
92
|
+
|
93
|
+
def to_p
|
94
|
+
# Assumption is that when to_p is called we are going to get a packed structure. Therefore,
|
95
|
+
# set @unpacked back to nil so we know to unpack when accessors are next accessed.
|
96
|
+
@unpacked = nil
|
97
|
+
# Assignment of inner structure to variable is very important here. Without it,
|
98
|
+
# will not be able to unpack changes to the structure. Alternative, nested unpacks,
|
99
|
+
# does not work (i.e. @struct.unpack("LLP12")[2].unpack("LLP12") results in "no associated pointer")
|
100
|
+
@sec_buffer ||= [@bufferSize, @type, @buffer].pack("LLP")
|
101
|
+
@struct ||= [SECBUFFER_VERSION, 1, @sec_buffer].pack("LLP")
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
# Unpacks the SecurityBufferDesc structure into member variables. We
|
107
|
+
# only want to do this once per struct, so the struct is deleted
|
108
|
+
# after unpacking.
|
109
|
+
def unpack
|
110
|
+
if ! @unpacked && @sec_buffer && @struct
|
111
|
+
@bufferSize, @type = @sec_buffer.unpack("LL")
|
112
|
+
@buffer = @sec_buffer.unpack("LLP#{@bufferSize}")[2]
|
113
|
+
@struct = nil
|
114
|
+
@sec_buffer = nil
|
115
|
+
@unpacked = true
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# SEC_WINNT_AUTH_IDENTITY structure
|
121
|
+
class Identity
|
122
|
+
SEC_WINNT_AUTH_IDENTITY_ANSI = 0x1
|
123
|
+
|
124
|
+
attr_accessor :user, :domain, :password
|
125
|
+
|
126
|
+
def initialize(user = nil, domain = nil, password = nil)
|
127
|
+
@user = user
|
128
|
+
@domain = domain
|
129
|
+
@password = password
|
130
|
+
@flags = SEC_WINNT_AUTH_IDENTITY_ANSI
|
131
|
+
end
|
132
|
+
|
133
|
+
def to_p
|
134
|
+
[@user, @user ? @user.length : 0,
|
135
|
+
@domain, @domain ? @domain.length : 0,
|
136
|
+
@password, @password ? @password.length : 0,
|
137
|
+
@flags].pack("PLPLPLL")
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# Takes a return result from an SSPI function and interprets the value.
|
142
|
+
class SSPIResult
|
143
|
+
# Good results
|
144
|
+
SEC_E_OK = 0x00000000
|
145
|
+
SEC_I_CONTINUE_NEEDED = 0x00090312
|
146
|
+
|
147
|
+
# These are generally returned by InitializeSecurityContext
|
148
|
+
SEC_E_INSUFFICIENT_MEMORY = 0x80090300
|
149
|
+
SEC_E_INTERNAL_ERROR = 0x80090304
|
150
|
+
SEC_E_INVALID_HANDLE = 0x80090301
|
151
|
+
SEC_E_INVALID_TOKEN = 0x80090308
|
152
|
+
SEC_E_LOGON_DENIED = 0x8009030C
|
153
|
+
SEC_E_NO_AUTHENTICATING_AUTHORITY = 0x80090311
|
154
|
+
SEC_E_NO_CREDENTIALS = 0x8009030E
|
155
|
+
SEC_E_TARGET_UNKNOWN = 0x80090303
|
156
|
+
SEC_E_UNSUPPORTED_FUNCTION = 0x80090302
|
157
|
+
SEC_E_WRONG_PRINCIPAL = 0x80090322
|
158
|
+
|
159
|
+
# These are generally returned by AcquireCredentialsHandle
|
160
|
+
SEC_E_NOT_OWNER = 0x80090306
|
161
|
+
SEC_E_SECPKG_NOT_FOUND = 0x80090305
|
162
|
+
SEC_E_UNKNOWN_CREDENTIALS = 0x8009030D
|
163
|
+
|
164
|
+
@@map = {}
|
165
|
+
constants.each { |v| @@map[self.const_get(v.to_s)] = v }
|
166
|
+
|
167
|
+
attr_reader :value
|
168
|
+
|
169
|
+
def initialize(value)
|
170
|
+
# convert to unsigned long
|
171
|
+
value = [value].pack("L").unpack("L").first
|
172
|
+
raise "#{value.to_s(16)} is not a recognized result" unless @@map.has_key? value
|
173
|
+
@value = value
|
174
|
+
end
|
175
|
+
|
176
|
+
def to_s
|
177
|
+
@@map[@value].to_s
|
178
|
+
end
|
179
|
+
|
180
|
+
def ok?
|
181
|
+
@value == SEC_I_CONTINUE_NEEDED || @value == SEC_E_OK
|
182
|
+
end
|
183
|
+
|
184
|
+
def ==(other)
|
185
|
+
if other.is_a?(SSPIResult)
|
186
|
+
@value == other.value
|
187
|
+
elsif other.is_a?(Fixnum)
|
188
|
+
@value == @@map[other]
|
189
|
+
else
|
190
|
+
false
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
# Handles "Negotiate" type authentication. Geared towards authenticating with a proxy server over HTTP
|
196
|
+
class NegotiateAuth
|
197
|
+
attr_accessor :credentials, :context, :contextAttributes, :user, :domain
|
198
|
+
REQUEST_FLAGS = ISC_REQ_CONFIDENTIALITY | ISC_REQ_REPLAY_DETECT | ISC_REQ_CONNECTION
|
199
|
+
|
200
|
+
# Given a connection and a request path, performs authentication as the current user and returns
|
201
|
+
# the response from a GET request. The connnection should be a Net::HTTP object, and it should
|
202
|
+
# have been constructed using the Net::HTTP.Proxy method, but anything that responds to "get" will work.
|
203
|
+
# If a user and domain are given, will authenticate as the given user.
|
204
|
+
# Returns the response received from the get method (usually Net::HTTPResponse)
|
205
|
+
def NegotiateAuth.proxy_auth_get(http, path, user = nil, domain = nil)
|
206
|
+
raise "http must respond to :get" unless http.respond_to?(:get)
|
207
|
+
nego_auth = self.new user, domain
|
208
|
+
|
209
|
+
resp = http.get path, { "Proxy-Authorization" => "Negotiate " + nego_auth.get_initial_token }
|
210
|
+
if resp["Proxy-Authenticate"]
|
211
|
+
resp = http.get path, { "Proxy-Authorization" => "Negotiate " + nego_auth.complete_authentication(resp["Proxy-Authenticate"].split(" ").last.strip) }
|
212
|
+
end
|
213
|
+
|
214
|
+
resp
|
215
|
+
end
|
216
|
+
|
217
|
+
# Creates a new instance ready for authentication as the given user in the given domain.
|
218
|
+
# Defaults to current user and domain as defined by ENV["USERDOMAIN"] and ENV["USERNAME"] if
|
219
|
+
# no arguments are supplied.
|
220
|
+
def initialize(user = nil, domain = nil)
|
221
|
+
if user.nil? && domain.nil? && ENV["USERNAME"].nil? && ENV["USERDOMAIN"].nil?
|
222
|
+
raise "A username or domain must be supplied since they cannot be retrieved from the environment"
|
223
|
+
end
|
224
|
+
|
225
|
+
@user = user || ENV["USERNAME"]
|
226
|
+
@domain = domain || ENV["USERDOMAIN"]
|
227
|
+
end
|
228
|
+
|
229
|
+
# Gets the initial Negotiate token. Returns it as a base64 encoded string suitable for use in HTTP. Can
|
230
|
+
# be easily decoded, however.
|
231
|
+
def get_initial_token
|
232
|
+
raise "This object is no longer usable because its resources have been freed." if @cleaned_up
|
233
|
+
get_credentials
|
234
|
+
|
235
|
+
outputBuffer = SecurityBuffer.new
|
236
|
+
@context = CtxtHandle.new
|
237
|
+
@contextAttributes = "\0" * 4
|
238
|
+
|
239
|
+
result = SSPIResult.new(API::InitializeSecurityContext.call(@credentials.to_p, nil, nil,
|
240
|
+
REQUEST_FLAGS,0, SECURITY_NETWORK_DREP, nil, 0, @context.to_p, outputBuffer.to_p, @contextAttributes, TimeStamp.new.to_p))
|
241
|
+
|
242
|
+
if result.ok? then
|
243
|
+
return encode_token(outputBuffer.token)
|
244
|
+
else
|
245
|
+
raise "Error: #{result.to_s}"
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
# Takes a Base64 encoded token and gets the next token in the
|
250
|
+
# Negotiate authentication chain. The token can include the "Negotiate" header and it will be stripped.
|
251
|
+
# Does not indicate if SEC_I_CONTINUE or SEC_E_OK was returned.
|
252
|
+
# Token returned is Base64 encoded.
|
253
|
+
def complete_authentication(token)
|
254
|
+
raise "This object is no longer usable because its resources have been freed." if @cleaned_up
|
255
|
+
|
256
|
+
# If the Negotiate prefix is passed in, assume we are seeing "Negotiate <token>" and get the token.
|
257
|
+
if token.include? "Negotiate"
|
258
|
+
token = token.split(" ").last.strip
|
259
|
+
end
|
260
|
+
outputBuffer = SecurityBuffer.new
|
261
|
+
|
262
|
+
result = SSPIResult.new(API::InitializeSecurityContext.call(@credentials.to_p, @context.to_p, nil,
|
263
|
+
REQUEST_FLAGS, 0, SECURITY_NETWORK_DREP, SecurityBuffer.new(Base64.decode64(token)).to_p, 0,
|
264
|
+
@context.to_p,
|
265
|
+
outputBuffer.to_p, @contextAttributes, TimeStamp.new.to_p))
|
266
|
+
|
267
|
+
if result.ok? then
|
268
|
+
return encode_token(outputBuffer.token)
|
269
|
+
else
|
270
|
+
raise "Error: #{result.to_s}"
|
271
|
+
end
|
272
|
+
ensure
|
273
|
+
# need to make sure we don't clean up if we've already cleaned up.
|
274
|
+
clean_up unless @cleaned_up
|
275
|
+
end
|
276
|
+
|
277
|
+
private
|
278
|
+
|
279
|
+
def clean_up
|
280
|
+
# free structures allocated
|
281
|
+
@cleaned_up = true
|
282
|
+
API::FreeCredentialsHandle.call(@credentials.to_p)
|
283
|
+
API::DeleteSecurityContext.call(@context.to_p)
|
284
|
+
@context = nil
|
285
|
+
@credentials = nil
|
286
|
+
@contextAttributes = nil
|
287
|
+
end
|
288
|
+
|
289
|
+
# Gets credentials based on user, domain or both. If both are nil, an error occurs
|
290
|
+
def get_credentials
|
291
|
+
@credentials = CredHandle.new
|
292
|
+
ts = TimeStamp.new
|
293
|
+
@identity = Identity.new @user, @domain
|
294
|
+
result = SSPIResult.new(API::AcquireCredentialsHandle.call(nil, "Negotiate", SECPKG_CRED_OUTBOUND, nil, @identity.to_p, nil, nil, @credentials.to_p, ts.to_p))
|
295
|
+
raise "Error acquire credentials: #{result}" unless result.ok?
|
296
|
+
end
|
297
|
+
|
298
|
+
def encode_token(t)
|
299
|
+
# encode64 will add newlines every 60 characters so we need to remove those.
|
300
|
+
Base64.encode64(t).gsub(/\n/, "")
|
301
|
+
end
|
302
|
+
end
|
303
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'rubysspi'
|
3
|
+
|
4
|
+
|
5
|
+
module Net
|
6
|
+
# Replaces Net::HTTP.request to understand Negotate HTTP proxy authorization. Uses native Win32
|
7
|
+
# libraries to authentic as the current user (as defined by the ENV["USERNAME"] and ENV["USERDOMAIN"]
|
8
|
+
# environment variables.
|
9
|
+
class HTTP
|
10
|
+
def request(req, body = nil, &block) # :yield: +response+
|
11
|
+
unless started?
|
12
|
+
start {
|
13
|
+
req['connection'] ||= 'close'
|
14
|
+
return request(req, body, &block)
|
15
|
+
}
|
16
|
+
end
|
17
|
+
if proxy_user()
|
18
|
+
unless use_ssl?
|
19
|
+
req.proxy_basic_auth proxy_user(), proxy_pass()
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
req.set_body_internal body
|
24
|
+
begin_transport req
|
25
|
+
req.exec @socket, @curr_http_version, edit_path(req.path)
|
26
|
+
begin
|
27
|
+
res = HTTPResponse.read_new(@socket)
|
28
|
+
end while res.kind_of?(HTTPContinue)
|
29
|
+
# If proxy was specified, and the server is demanding authentication, negotatiate with it
|
30
|
+
if proxy? && res.code.to_i == 407 && res["Proxy-Authenticate"].include?("Negotiate")
|
31
|
+
n = SSPI::NegotiateAuth.new
|
32
|
+
res.reading_body(@socket, req.response_body_permitted?) { }
|
33
|
+
req["Proxy-Authorization"] = "Negotiate #{n.get_initial_token}"
|
34
|
+
req.exec @socket, @curr_http_version, edit_path(req.path)
|
35
|
+
begin
|
36
|
+
res = HTTPResponse.read_new(@socket)
|
37
|
+
end while res.kind_of?(HTTPContinue)
|
38
|
+
|
39
|
+
if res["Proxy-Authenticate"]
|
40
|
+
res.reading_body(@socket, req.response_body_permitted?) { }
|
41
|
+
req["Proxy-Authorization"] = "Negotiate #{n.complete_authentication res["Proxy-Authenticate"]}"
|
42
|
+
req.exec @socket, @curr_http_version, edit_path(req.path)
|
43
|
+
begin
|
44
|
+
res = HTTPResponse.read_new(@socket)
|
45
|
+
end while res.kind_of?(HTTPContinue)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
res.reading_body(@socket, req.response_body_permitted?) {
|
49
|
+
yield res if block_given?
|
50
|
+
}
|
51
|
+
end_transport req, res
|
52
|
+
|
53
|
+
res
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'net/http'
|
3
|
+
require 'pathname'
|
4
|
+
$: << (File.dirname(__FILE__) << "/../lib")
|
5
|
+
require 'rubysspi'
|
6
|
+
|
7
|
+
class NTLMTest < Test::Unit::TestCase
|
8
|
+
|
9
|
+
def setup
|
10
|
+
assert ENV["http_proxy"], "http_proxy must be set before running tests."
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_auth
|
14
|
+
assert ENV["http_proxy"], "http_proxy environment variable must be set."
|
15
|
+
proxy = URI.parse(ENV["http_proxy"])
|
16
|
+
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)."
|
17
|
+
|
18
|
+
Net::HTTP.Proxy(proxy.host, proxy.port).start("www.google.com") do |http|
|
19
|
+
nego_auth = SSPI::NegotiateAuth.new
|
20
|
+
sr = http.request_get "/", { "Proxy-Authorization" => "Negotiate " + nego_auth.get_initial_token }
|
21
|
+
resp = http.get "/", { "Proxy-Authorization" => "Negotiate " + nego_auth.complete_authentication(sr["Proxy-Authenticate"].split(" ").last.strip) }
|
22
|
+
assert resp.code.to_i == 200, "Resposne code not as expected: #{resp.inspect}"
|
23
|
+
resp = http.get "/foobar.html"
|
24
|
+
assert resp.code.to_i == 404, "Response code not as expected: #{resp.inspect}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_proxy_auth_get
|
29
|
+
assert ENV["http_proxy"], "http_proxy environment variable must be set."
|
30
|
+
proxy = URI.parse(ENV["http_proxy"])
|
31
|
+
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)."
|
32
|
+
|
33
|
+
Net::HTTP.Proxy(proxy.host, proxy.port).start("www.google.com") do |http|
|
34
|
+
resp = SSPI::NegotiateAuth.proxy_auth_get http, "/"
|
35
|
+
assert resp.code.to_i == 200, "Response code not as expected: #{resp.inspect}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_one_time_use_only
|
40
|
+
assert ENV["http_proxy"], "http_proxy environment variable must be set."
|
41
|
+
proxy = URI.parse(ENV["http_proxy"])
|
42
|
+
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)."
|
43
|
+
|
44
|
+
Net::HTTP.Proxy(proxy.host, proxy.port).start("www.google.com") do |http|
|
45
|
+
nego_auth = SSPI::NegotiateAuth.new
|
46
|
+
sr = http.request_get "/", { "Proxy-Authorization" => "Negotiate " + nego_auth.get_initial_token }
|
47
|
+
resp = http.get "/", { "Proxy-Authorization" => "Negotiate " + nego_auth.complete_authentication(sr["Proxy-Authenticate"].split(" ").last.strip) }
|
48
|
+
assert resp.code.to_i == 200, "Response code not as expected: #{resp.inspect}"
|
49
|
+
assert_raises(RuntimeError, "Should not be able to call complete_authentication again") do
|
50
|
+
nego_auth.complete_authentication "foo"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'net/http'
|
3
|
+
require 'pathname'
|
4
|
+
$: << (File.dirname(__FILE__) << "/../lib")
|
5
|
+
require 'rubysspi/proxy_auth'
|
6
|
+
|
7
|
+
class NTLMTest < Test::Unit::TestCase
|
8
|
+
def setup
|
9
|
+
assert ENV["http_proxy"], "http_proxy must be set before running tests."
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_net_http
|
13
|
+
|
14
|
+
assert ENV["http_proxy"], "http_proxy environment variable must be set."
|
15
|
+
proxy = URI.parse(ENV["http_proxy"])
|
16
|
+
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)."
|
17
|
+
|
18
|
+
Net::HTTP.Proxy(proxy.host, proxy.port).start("www.google.com") do |http|
|
19
|
+
resp = http.get("/")
|
20
|
+
assert resp.code.to_i == 200, "Did not get response from Google as expected."
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
metadata
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.9.0
|
3
|
+
specification_version: 1
|
4
|
+
name: rubysspi
|
5
|
+
version: !ruby/object:Gem::Version
|
6
|
+
version: 0.0.1
|
7
|
+
date: 2006-07-14 00:00:00 -07:00
|
8
|
+
summary: A library which adds implements Ruby bindings to the Win32 SSPI library. Also includes a module to add Negotate authentication support to Net::HTTP.
|
9
|
+
require_paths:
|
10
|
+
- lib
|
11
|
+
email: jgbailey @nospan@ gmail.com
|
12
|
+
homepage: http://rubyforge.org/projects/rubysspi/
|
13
|
+
rubyforge_project: http://rubyforge.org/projects/rubysspi/
|
14
|
+
description: This gem provides bindings to the Win32 SSPI libraries, primarily to support Negotiate (i.e. SPNEGO, NTLM) authentication with a proxy server. Enough support is implemented to provide the necessary support for the authentication. A module is also provided which overrides Net::HTTP and adds support for Negotiate authentication to it. This implies that open-uri automatically gets support for it, as long as the http_proxy environment variable is set.
|
15
|
+
autorequire: rubysspi
|
16
|
+
default_executable:
|
17
|
+
bindir: bin
|
18
|
+
has_rdoc: true
|
19
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">"
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.0.0
|
24
|
+
version:
|
25
|
+
platform: i386-mswin32
|
26
|
+
signing_key:
|
27
|
+
cert_chain:
|
28
|
+
post_install_message:
|
29
|
+
authors:
|
30
|
+
- Justin Bailey
|
31
|
+
files:
|
32
|
+
- lib/rubysspi
|
33
|
+
- lib/rubysspi.rb
|
34
|
+
- lib/rubysspi/proxy_auth.rb
|
35
|
+
- test/ruby_sspi_test.rb
|
36
|
+
- test/test_net_http.rb
|
37
|
+
- README.txt
|
38
|
+
- Rakefile
|
39
|
+
test_files: []
|
40
|
+
|
41
|
+
rdoc_options:
|
42
|
+
- --title
|
43
|
+
- Ruby SSPI -- Win32 SSPI Bindings for Ruby
|
44
|
+
- --main
|
45
|
+
- README.txt
|
46
|
+
- --line-numbers
|
47
|
+
extra_rdoc_files:
|
48
|
+
- README.txt
|
49
|
+
executables: []
|
50
|
+
|
51
|
+
extensions: []
|
52
|
+
|
53
|
+
requirements: []
|
54
|
+
|
55
|
+
dependencies: []
|
56
|
+
|