ruby-ntlm-namespace 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +77 -0
- data/Rakefile +34 -0
- data/VERSION +1 -0
- data/lib/ntlm.rb +34 -0
- data/lib/ntlm/http.rb +51 -0
- data/lib/ntlm/imap.rb +37 -0
- data/lib/ntlm/mechanize.rb +42 -0
- data/lib/ntlm/message.rb +361 -0
- data/lib/ntlm/smtp.rb +30 -0
- data/lib/ntlm/util.rb +105 -0
- data/ruby-ntlm-namespace.gemspec +24 -0
- data/test/auth_test.rb +27 -0
- data/test/function_test.rb +43 -0
- data/test/test_helper.rb +20 -0
- metadata +61 -0
data/README.markdown
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
ruby-ntlm
|
2
|
+
=========
|
3
|
+
|
4
|
+
ruby-ntlm is NTLM authentication client for Ruby.
|
5
|
+
This library supports NTLM v1 only.
|
6
|
+
|
7
|
+
NTLM authentication is used in Microsoft's server products,
|
8
|
+
such as MS Exchange Server and IIS.
|
9
|
+
|
10
|
+
ruby-ntlm-namespace
|
11
|
+
-------------------
|
12
|
+
|
13
|
+
This is a fork of [mademaxus' namespace fix](https://github.com/mademaxus/ruby-ntlm) for the [macks' ruby-ntlm](https://github.com/macks/ruby-ntlm).
|
14
|
+
|
15
|
+
This gem release is only meant to help those experiencing the namespace issues with macks' ruby-ntlm and a simple dropin for Ruby on Rails gem files.
|
16
|
+
|
17
|
+
|
18
|
+
Install
|
19
|
+
-------
|
20
|
+
|
21
|
+
$ sudo gem install ruby-ntlm
|
22
|
+
|
23
|
+
|
24
|
+
Usage
|
25
|
+
-----
|
26
|
+
|
27
|
+
### HTTP ###
|
28
|
+
|
29
|
+
require 'ntlm/http'
|
30
|
+
http = Net::HTTP.new('www.example.com')
|
31
|
+
request = Net::HTTP::Get.new('/')
|
32
|
+
request.ntlm_auth('User', 'Domain', 'Password')
|
33
|
+
response = http.request(request)
|
34
|
+
|
35
|
+
### HTTP (using Mechanize) ###
|
36
|
+
|
37
|
+
require 'ntlm/mechanize'
|
38
|
+
mech = Mechanize.new
|
39
|
+
mech.auth('Domain\\User', 'Password')
|
40
|
+
mech.get('http://www.example.com/index.html')
|
41
|
+
|
42
|
+
### IMAP ###
|
43
|
+
|
44
|
+
require 'ntlm/imap'
|
45
|
+
imap = Net::IMAP.new('imap.example.com')
|
46
|
+
imap.authenticate('NTLM', 'User', 'Domain', 'Password')
|
47
|
+
|
48
|
+
### SMTP ###
|
49
|
+
|
50
|
+
require 'ntlm/smtp'
|
51
|
+
smtp = Net::SMTP.new('smtp.example.com')
|
52
|
+
smtp.start('localhost.localdomain', 'Domain\\User', 'Password', :ntlm) do |smtp|
|
53
|
+
smtp.send_mail(mail_body, from_addr, to_addr)
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
Author
|
58
|
+
------
|
59
|
+
|
60
|
+
MATSUYAMA Kengo (<macksx@gmail.com>)
|
61
|
+
|
62
|
+
|
63
|
+
License
|
64
|
+
-------
|
65
|
+
|
66
|
+
MIT License.
|
67
|
+
|
68
|
+
Copyright (c) 2010 MATSUYAMA Kengo
|
69
|
+
|
70
|
+
|
71
|
+
References
|
72
|
+
----------
|
73
|
+
|
74
|
+
* [MS-NLMP][]: NT LAN Manager (NTLM) Authentication Protocol Specification
|
75
|
+
[MS-NLMP]: http://msdn.microsoft.com/en-us/library/cc236621%28PROT.13%29.aspx
|
76
|
+
* [Ruby/NTLM][]: Another NTLM implementation for Ruby
|
77
|
+
[Ruby/NTLM]: http://rubyforge.org/projects/rubyntlm/
|
data/Rakefile
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'jeweler'
|
4
|
+
require 'rake/testtask'
|
5
|
+
|
6
|
+
task :default => :test
|
7
|
+
|
8
|
+
Jeweler::Tasks.new do |gem|
|
9
|
+
gem.name = 'ruby-ntlm'
|
10
|
+
gem.summary = %Q{NTLM implementation for Ruby}
|
11
|
+
gem.description = %Q{NTLM implementation for Ruby.}
|
12
|
+
gem.email = 'macksx@gmail.com'
|
13
|
+
gem.homepage = 'http://github.com/macks/ruby-ntlm'
|
14
|
+
gem.authors = ['MATSUYAMA Kengo']
|
15
|
+
end
|
16
|
+
|
17
|
+
Rake::TestTask.new(:test) do |task|
|
18
|
+
task.libs << 'lib:test'
|
19
|
+
task.pattern = 'test/**/*_test.rb'
|
20
|
+
task.verbose = true
|
21
|
+
end
|
22
|
+
|
23
|
+
begin
|
24
|
+
require 'rcov/rcovtask'
|
25
|
+
Rcov::RcovTask.new do |task|
|
26
|
+
task.libs << 'lib:test'
|
27
|
+
task.pattern = 'test/**/*_test.rb'
|
28
|
+
task.verbose = true
|
29
|
+
end
|
30
|
+
rescue LoadError
|
31
|
+
task :rcov do
|
32
|
+
abort 'rcov is not available.'
|
33
|
+
end
|
34
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.1
|
data/lib/ntlm.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# vim: set et sw=2 sts=2:
|
2
|
+
|
3
|
+
require 'ntlm/util'
|
4
|
+
require 'ntlm/message'
|
5
|
+
|
6
|
+
module NTLM
|
7
|
+
|
8
|
+
begin
|
9
|
+
Version = File.read(File.dirname(__FILE__) + '/../VERSION').strip
|
10
|
+
rescue
|
11
|
+
Version = 'unknown'
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.negotiate(args = {})
|
15
|
+
Message::Negotiate.new(args)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.authenticate(challenge_message, user, domain, password, options = {})
|
19
|
+
challenge = Message::Challenge.parse(challenge_message)
|
20
|
+
|
21
|
+
opt = options.merge({
|
22
|
+
:ntlm_v2_session => challenge.has_flag?(:NEGOTIATE_EXTENDED_SECURITY),
|
23
|
+
})
|
24
|
+
nt_response, lm_response = Util.ntlm_v1_response(challenge.challenge, password, opt)
|
25
|
+
|
26
|
+
Message::Authenticate.new(
|
27
|
+
:user => user,
|
28
|
+
:domain => domain,
|
29
|
+
:lm_response => lm_response,
|
30
|
+
:nt_response => nt_response
|
31
|
+
)
|
32
|
+
end
|
33
|
+
|
34
|
+
end # NTLM
|
data/lib/ntlm/http.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'ntlm'
|
2
|
+
require 'net/http'
|
3
|
+
|
4
|
+
module Net
|
5
|
+
|
6
|
+
module HTTPHeader
|
7
|
+
attr_reader :ntlm_auth_params
|
8
|
+
|
9
|
+
def ntlm_auth(user, domain, password)
|
10
|
+
@ntlm_auth_params = [user, domain, password]
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class HTTP
|
15
|
+
|
16
|
+
unless method_defined?(:request_without_ntlm_auth)
|
17
|
+
alias request_without_ntlm_auth request
|
18
|
+
end
|
19
|
+
|
20
|
+
def request(req, body = nil, &block)
|
21
|
+
unless req.ntlm_auth_params
|
22
|
+
return request_without_ntlm_auth(req, body, &block)
|
23
|
+
end
|
24
|
+
|
25
|
+
unless started?
|
26
|
+
start do
|
27
|
+
req.delete('connection')
|
28
|
+
return request(req, body, &block)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Negotiation
|
33
|
+
req['authorization'] = 'NTLM ' + ::NTLM.negotiate.to_base64
|
34
|
+
res = request_without_ntlm_auth(req, body)
|
35
|
+
challenge = res['www-authenticate'][/NTLM (.*)/, 1].unpack('m').first rescue nil
|
36
|
+
|
37
|
+
if challenge && res.code == '401'
|
38
|
+
# Authentication
|
39
|
+
user, domain, password = req.ntlm_auth_params
|
40
|
+
req['authorization'] = 'NTLM ' + ::NTLM.authenticate(challenge, user, domain, password).to_base64
|
41
|
+
req.body_stream.rewind if req.body_stream
|
42
|
+
request_without_ntlm_auth(req, body, &block) # We must re-use the connection.
|
43
|
+
else
|
44
|
+
yield res if block_given?
|
45
|
+
res
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
end # HTTP
|
50
|
+
|
51
|
+
end # Net
|
data/lib/ntlm/imap.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'ntlm'
|
2
|
+
require 'net/imap'
|
3
|
+
|
4
|
+
module Net
|
5
|
+
class IMAP
|
6
|
+
class ResponseParser
|
7
|
+
def continue_req
|
8
|
+
match(T_PLUS)
|
9
|
+
if lookahead.symbol == T_CRLF
|
10
|
+
return ContinuationRequest.new(ResponseText.new(nil, ''), @str)
|
11
|
+
else
|
12
|
+
match(T_SPACE)
|
13
|
+
return ContinuationRequest.new(resp_text, @str)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end # ResponseParser
|
17
|
+
|
18
|
+
class NTLMAuthenticator
|
19
|
+
def initialize(user, domain, password)
|
20
|
+
@user, @domain, @password = user, domain, password
|
21
|
+
@state = 0
|
22
|
+
end
|
23
|
+
|
24
|
+
def process(data)
|
25
|
+
case (@state += 1)
|
26
|
+
when 1
|
27
|
+
::NTLM.negotiate.to_s
|
28
|
+
when 2
|
29
|
+
::NTLM.authenticate(data, @user, @domain, @password).to_s
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end # NTLMAuthenticator
|
33
|
+
|
34
|
+
add_authenticator 'NTLM', NTLMAuthenticator
|
35
|
+
|
36
|
+
end # IMAP
|
37
|
+
end # Net
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'mechanize'
|
2
|
+
require 'ntlm/http'
|
3
|
+
|
4
|
+
class Mechanize
|
5
|
+
class Chain
|
6
|
+
class AuthHeaders
|
7
|
+
|
8
|
+
unless method_defined?(:handle_without_ntlm)
|
9
|
+
alias handle_without_ntlm handle
|
10
|
+
end
|
11
|
+
|
12
|
+
def handle(ctx, params)
|
13
|
+
if @auth_hash[params[:uri].host] == :ntlm && @user && @password
|
14
|
+
if @user.index('\\')
|
15
|
+
domain, user = @user.split('\\', 2)
|
16
|
+
end
|
17
|
+
params[:request].ntlm_auth(user, domain, @password)
|
18
|
+
end
|
19
|
+
handle_without_ntlm(ctx, params)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
unless private_method_defined?(:fetch_page_without_ntlm)
|
25
|
+
alias fetch_page_without_ntlm fetch_page
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def fetch_page(params)
|
31
|
+
begin
|
32
|
+
fetch_page_without_ntlm(params)
|
33
|
+
rescue Mechanize::ResponseCodeError => e
|
34
|
+
if e.response_code == '401' && e.page.header['www-authenticate'] =~ /NTLM/ && @auth_hash[e.page.uri.host] != :ntlm
|
35
|
+
@auth_hash[e.page.uri.host] = :ntlm
|
36
|
+
fetch_page_without_ntlm(params)
|
37
|
+
else
|
38
|
+
raise
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/lib/ntlm/message.rb
ADDED
@@ -0,0 +1,361 @@
|
|
1
|
+
# vim: set et sw=2 sts=2:
|
2
|
+
|
3
|
+
require 'ntlm/util'
|
4
|
+
|
5
|
+
module NTLM
|
6
|
+
class Message
|
7
|
+
|
8
|
+
include Util
|
9
|
+
|
10
|
+
SSP_SIGNATURE = "NTLMSSP\0"
|
11
|
+
|
12
|
+
# [MS-NLMP] 2.2.2.5
|
13
|
+
FLAGS = {
|
14
|
+
:NEGOTIATE_UNICODE => 0x00000001, # Unicode character set encoding
|
15
|
+
:NEGOTIATE_OEM => 0x00000002, # OEM character set encoding
|
16
|
+
:REQUEST_TARGET => 0x00000004, # TargetName is supplied in challenge message
|
17
|
+
:UNUSED10 => 0x00000008,
|
18
|
+
:NEGOTIATE_SIGN => 0x00000010, # Session key negotiation for message signatures
|
19
|
+
:NEGOTIATE_SEAL => 0x00000020, # Session key negotiation for message confidentiality
|
20
|
+
:NEGOTIATE_DATAGRAM => 0x00000040, # Connectionless authentication
|
21
|
+
:NEGOTIATE_LM_KEY => 0x00000080, # LAN Manager session key computation
|
22
|
+
:UNUSED9 => 0x00000100,
|
23
|
+
:NEGOTIATE_NTLM => 0x00000200, # NTLM v1 protocol
|
24
|
+
:UNUSED8 => 0x00000400,
|
25
|
+
:ANONYMOUS => 0x00000800, # Anonymous connection
|
26
|
+
:OEM_DOMAIN_SUPPLIED => 0x00001000, # Domain field is present
|
27
|
+
:OEM_WORKSTATION_SUPPLIED => 0x00002000, # Workstations field is present
|
28
|
+
:UNUSED7 => 0x00004000,
|
29
|
+
:NEGOTIATE_ALWAYS_SIGN => 0x00008000,
|
30
|
+
:TARGET_TYPE_DOMAIN => 0x00010000, # TargetName is domain name
|
31
|
+
:TARGET_TYPE_SERVER => 0x00020000, # TargetName is server name
|
32
|
+
:UNUSED6 => 0x00040000,
|
33
|
+
:NEGOTIATE_EXTENDED_SECURITY => 0x00080000, # NTLM v2 session security
|
34
|
+
:NEGOTIATE_IDENTIFY => 0x00100000, # Requests identify level token
|
35
|
+
:UNUSED5 => 0x00200000,
|
36
|
+
:REQUEST_NON_NT_SESSION_KEY => 0x00400000, # LM session key is used
|
37
|
+
:NEGOTIATE_TARGET_INFO => 0x00800000, # Requests TargetInfo
|
38
|
+
:UNUSED4 => 0x01000000,
|
39
|
+
:NEGOTIATE_VERSION => 0x02000000, # Version field is present
|
40
|
+
:UNUSED3 => 0x04000000,
|
41
|
+
:UNUSED2 => 0x08000000,
|
42
|
+
:UNUSED1 => 0x10000000,
|
43
|
+
:NEGOTIATE_128 => 0x20000000, # 128bit encryption
|
44
|
+
:NEGOTIATE_KEY_EXCH => 0x40000000, # Explicit key exchange
|
45
|
+
:NEGOTIATE_56 => 0x80000000, # 56bit encryption
|
46
|
+
}
|
47
|
+
|
48
|
+
# [MS-NLMP] 2.2.2.1
|
49
|
+
AV_PAIRS = {
|
50
|
+
:AV_EOL => 0,
|
51
|
+
:AV_NB_COMPUTER_NAME => 1,
|
52
|
+
:AV_NB_DOMAIN_NAME => 2,
|
53
|
+
:AV_DNS_COMPUTER_NAME => 3,
|
54
|
+
:AV_DNS_DOMAIN_NAME => 4,
|
55
|
+
:AV_DNS_TREE_NAME => 5,
|
56
|
+
:AV_FLAGS => 6,
|
57
|
+
:AV_TIMESTAMP => 7,
|
58
|
+
:AV_RESTRICTIONS => 8,
|
59
|
+
:AV_TARGET_NAME => 9,
|
60
|
+
:AV_CHANNEL_BINDINGS => 10,
|
61
|
+
}
|
62
|
+
AV_PAIR_NAMES = AV_PAIRS.invert
|
63
|
+
|
64
|
+
FLAGS.each do |name, val|
|
65
|
+
const_set(name, val)
|
66
|
+
end
|
67
|
+
|
68
|
+
AV_PAIRS.each do |name, val|
|
69
|
+
const_set(name, val)
|
70
|
+
end
|
71
|
+
|
72
|
+
class ParseError < StandardError; end
|
73
|
+
|
74
|
+
attr_accessor :flag
|
75
|
+
|
76
|
+
|
77
|
+
def self.parse(*args)
|
78
|
+
new.parse(*args)
|
79
|
+
end
|
80
|
+
|
81
|
+
def initialize(args = {})
|
82
|
+
@buffer = ''
|
83
|
+
@offset = 0
|
84
|
+
@flag = args[:flag] || self.class::DEFAULT_FLAGS
|
85
|
+
|
86
|
+
self.class::ATTRIBUTES.each do |key|
|
87
|
+
instance_variable_set("@#{key}", args[key]) if args[key]
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def to_s
|
92
|
+
serialize
|
93
|
+
end
|
94
|
+
|
95
|
+
def serialize_to_base64
|
96
|
+
[serialize].pack('m').delete("\r\n")
|
97
|
+
end
|
98
|
+
|
99
|
+
alias to_base64 serialize_to_base64
|
100
|
+
|
101
|
+
def has_flag?(symbol)
|
102
|
+
(@flag & FLAGS[symbol]) != 0
|
103
|
+
end
|
104
|
+
|
105
|
+
def set(symbol)
|
106
|
+
@flag |= FLAGS[symbol]
|
107
|
+
end
|
108
|
+
|
109
|
+
def clear(symbol)
|
110
|
+
@flag &= ~FLAGS[symbol]
|
111
|
+
end
|
112
|
+
|
113
|
+
def unicode?
|
114
|
+
has_flag?(:NEGOTIATE_UNICODE)
|
115
|
+
end
|
116
|
+
|
117
|
+
def inspect_flags
|
118
|
+
flags = []
|
119
|
+
FLAGS.sort_by(&:last).each do |name, val|
|
120
|
+
flags << name if (@flag & val).nonzero?
|
121
|
+
end
|
122
|
+
"[#{flags.join(', ')}]"
|
123
|
+
end
|
124
|
+
|
125
|
+
def inspect
|
126
|
+
variables = (instance_variables.map(&:to_sym) - [:@offset, :@buffer, :@flag]).sort.map {|name| "#{name}=#{instance_variable_get(name).inspect}, " }.join
|
127
|
+
"\#<#{self.class.name} #{variables}@flag=#{inspect_flags}>"
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
|
132
|
+
def parse(string)
|
133
|
+
@buffer = string
|
134
|
+
signature, type = string.unpack('a8V')
|
135
|
+
raise ParseError, 'Unknown signature' if signature != SSP_SIGNATURE
|
136
|
+
raise ParseError, "Wrong type (expected #{self.class::TYPE}, but got #{type})" if type != self.class::TYPE
|
137
|
+
end
|
138
|
+
|
139
|
+
def append_payload(string, allocation_size = nil)
|
140
|
+
size = string.size
|
141
|
+
allocation_size ||= (size + 1) & ~1
|
142
|
+
string = string.ljust(allocation_size, "\0")
|
143
|
+
@buffer << string[0, allocation_size]
|
144
|
+
result = [size, allocation_size, @offset].pack('vvV')
|
145
|
+
@offset += allocation_size
|
146
|
+
result
|
147
|
+
end
|
148
|
+
|
149
|
+
def fetch_payload(fields)
|
150
|
+
size, allocated_size, offset = fields.unpack('vvV')
|
151
|
+
return nil if size.zero?
|
152
|
+
@buffer[offset, size]
|
153
|
+
end
|
154
|
+
|
155
|
+
def encode_version(array)
|
156
|
+
array.pack('CCvx3C') # major, minor, build, ntlm revision
|
157
|
+
end
|
158
|
+
|
159
|
+
def decode_version(string)
|
160
|
+
string.unpack('CCvx3C') # major, minor, build, ntlm revision
|
161
|
+
end
|
162
|
+
|
163
|
+
def decode_av_pair(string)
|
164
|
+
result = []
|
165
|
+
string = string.dup
|
166
|
+
while true
|
167
|
+
id, length = string.slice!(0, 4).unpack('vv')
|
168
|
+
value = string.slice!(0, length)
|
169
|
+
|
170
|
+
case sym = AV_PAIR_NAMES[id]
|
171
|
+
when :AV_EOL
|
172
|
+
break
|
173
|
+
when :AV_NB_COMPUTER_NAME, :AV_NB_DOMAIN_NAME, :AV_DNS_COMPUTER_NAME, :AV_DNS_DOMAIN_NAME, :AV_DNS_TREE_NAME, :AV_TARGET_NAME
|
174
|
+
value = decode_utf16(value)
|
175
|
+
when :AV_FLAGS
|
176
|
+
value = data.unpack('V').first
|
177
|
+
end
|
178
|
+
|
179
|
+
result << [sym, value]
|
180
|
+
end
|
181
|
+
result
|
182
|
+
end
|
183
|
+
|
184
|
+
def encode_av_pair(av_pair)
|
185
|
+
result = ''
|
186
|
+
av_pair.each do |(id, value)|
|
187
|
+
case id
|
188
|
+
when :AV_NB_COMPUTER_NAME, :AV_NB_DOMAIN_NAME, :AV_DNS_COMPUTER_NAME, :AV_DNS_DOMAIN_NAME, :AV_DNS_TREE_NAME, :AV_TARGET_NAME
|
189
|
+
value = encode_utf16(value)
|
190
|
+
when :AV_FLAGS
|
191
|
+
value = [data].pack('V')
|
192
|
+
end
|
193
|
+
result << [AV_PAIRS[id], value.size, value].pack('vva*')
|
194
|
+
end
|
195
|
+
|
196
|
+
result << [AV_EOL, 0].pack('vv')
|
197
|
+
end
|
198
|
+
|
199
|
+
|
200
|
+
# [MS-NLMP] 2.2.1.1
|
201
|
+
class Negotiate < Message
|
202
|
+
|
203
|
+
TYPE = 1
|
204
|
+
ATTRIBUTES = [:domain, :workstation, :version]
|
205
|
+
DEFAULT_FLAGS = [NEGOTIATE_UNICODE, NEGOTIATE_OEM, REQUEST_TARGET, NEGOTIATE_NTLM, NEGOTIATE_ALWAYS_SIGN, NEGOTIATE_EXTENDED_SECURITY].inject(:|)
|
206
|
+
|
207
|
+
attr_accessor *ATTRIBUTES
|
208
|
+
|
209
|
+
def parse(string)
|
210
|
+
super
|
211
|
+
@flag, domain, workstation, version = string.unpack('x12Va8a8a8')
|
212
|
+
@domain = fetch_payload(domain) if has_flag?(:OEM_DOMAIN_SUPPLIED)
|
213
|
+
@workstation = fetch_payload(workstation) if has_flag?(:OEM_WORKSTATION_SUPPLIED)
|
214
|
+
@version = decode_version(version) if has_flag?(:NEGOTIATE_VERSION)
|
215
|
+
self
|
216
|
+
end
|
217
|
+
|
218
|
+
def serialize
|
219
|
+
@buffer = ''
|
220
|
+
@offset = 40 # (8 + 4) + 4 + (8 * 3)
|
221
|
+
|
222
|
+
if @domain
|
223
|
+
set(:OEM_DOMAIN_SUPPLIED)
|
224
|
+
domain = append_payload(@domain)
|
225
|
+
end
|
226
|
+
|
227
|
+
if @workstation
|
228
|
+
set(:OEM_WORKSTATION_SUPPLIED)
|
229
|
+
workstation = append_payload(@workstation)
|
230
|
+
end
|
231
|
+
|
232
|
+
if @version
|
233
|
+
set(:NEGOTIATE_VERSION)
|
234
|
+
version = encode_version(@version)
|
235
|
+
end
|
236
|
+
|
237
|
+
[SSP_SIGNATURE, TYPE, @flag, domain, workstation, version].pack('a8VVa8a8a8') + @buffer
|
238
|
+
end
|
239
|
+
|
240
|
+
end # Negotiate
|
241
|
+
|
242
|
+
|
243
|
+
# [MS-NLMP] 2.2.1.2
|
244
|
+
class Challenge < Message
|
245
|
+
|
246
|
+
TYPE = 2
|
247
|
+
ATTRIBUTES = [:target_name, :challenge, :target_info, :version]
|
248
|
+
DEFAULT_FLAGS = 0
|
249
|
+
|
250
|
+
attr_accessor *ATTRIBUTES
|
251
|
+
|
252
|
+
def parse(string)
|
253
|
+
super
|
254
|
+
target_name, @flag, @challenge, target_info, version = string.unpack('x12a8Va8x8a8a8')
|
255
|
+
@target_name = fetch_payload(target_name) if has_flag?(:REQUEST_TARGET)
|
256
|
+
@target_info = fetch_payload(target_info) if has_flag?(:NEGOTIATE_TARGET_INFO)
|
257
|
+
@version = decode_version(version) if has_flag?(:NEGOTIATE_VERSION)
|
258
|
+
|
259
|
+
@target_name &&= decode_utf16(@target_name) if unicode?
|
260
|
+
@target_info &&= decode_av_pair(@target_info)
|
261
|
+
|
262
|
+
self
|
263
|
+
end
|
264
|
+
|
265
|
+
def serialize
|
266
|
+
@buffer = ''
|
267
|
+
@offset = 56 # (8 + 4) + 8 + 4 + (8 * 4)
|
268
|
+
|
269
|
+
@challenge ||= OpenSSL::Random.random_bytes(8)
|
270
|
+
|
271
|
+
if @target_name
|
272
|
+
set(:REQUEST_TARGET)
|
273
|
+
if unicode?
|
274
|
+
target_name = append_payload(encode_utf16(@target_name))
|
275
|
+
else
|
276
|
+
target_name = append_payload(@target_name)
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
if @target_info
|
281
|
+
set(:NEGOTIATE_TARGET_INFO)
|
282
|
+
target_info = append_payload(encode_av_pair(@target_info))
|
283
|
+
end
|
284
|
+
|
285
|
+
if @version
|
286
|
+
set(:NEGOTIATE_VERSION)
|
287
|
+
version = encode_version(@version)
|
288
|
+
end
|
289
|
+
|
290
|
+
[SSP_SIGNATURE, TYPE, target_name, @flag, @challenge, target_info, version].pack('a8Va8Va8x8a8a8') + @buffer
|
291
|
+
end
|
292
|
+
|
293
|
+
end # Challenge
|
294
|
+
|
295
|
+
|
296
|
+
# [MS-NLMP] 2.2.1.3
|
297
|
+
class Authenticate < Message
|
298
|
+
|
299
|
+
TYPE = 3
|
300
|
+
ATTRIBUTES = [:lm_response, :nt_response, :domain, :user, :workstation, :session_key, :version, :mic]
|
301
|
+
DEFAULT_FLAGS = [NEGOTIATE_UNICODE, REQUEST_TARGET, NEGOTIATE_NTLM, NEGOTIATE_ALWAYS_SIGN, NEGOTIATE_EXTENDED_SECURITY].inject(:|)
|
302
|
+
|
303
|
+
attr_accessor *ATTRIBUTES
|
304
|
+
|
305
|
+
def parse(string)
|
306
|
+
super
|
307
|
+
lm_response, nt_response, domain, user, workstation, session_key, @flag, version, mic = \
|
308
|
+
string.unpack('x12a8a8a8a8a8a8Va8a16')
|
309
|
+
|
310
|
+
@lm_response = fetch_payload(lm_response)
|
311
|
+
@nt_response = fetch_payload(nt_response)
|
312
|
+
@domain = fetch_payload(domain)
|
313
|
+
@user = fetch_payload(user)
|
314
|
+
@workstation = fetch_payload(workstation)
|
315
|
+
@session_key = fetch_payload(session_key) if has_flag?(:NEGOTIATE_KEY_EXCH)
|
316
|
+
@version = decode_version(version) if has_flag?(:NEGOTIATE_VERSION)
|
317
|
+
@mic = mic
|
318
|
+
|
319
|
+
if unicode?
|
320
|
+
@domain = decode_utf16(@domain)
|
321
|
+
@user = decode_utf16(@user)
|
322
|
+
@workstation = decode_utf16(@workstation)
|
323
|
+
end
|
324
|
+
|
325
|
+
self
|
326
|
+
end
|
327
|
+
|
328
|
+
def serialize
|
329
|
+
@buffer = ''
|
330
|
+
@offset = 88 # (8 + 4) + (8 * 6) + 4 + 8 + 16
|
331
|
+
|
332
|
+
lm_response = append_payload(@lm_response)
|
333
|
+
nt_response = append_payload(@nt_response)
|
334
|
+
|
335
|
+
if unicode?
|
336
|
+
domain = append_payload(encode_utf16(@domain))
|
337
|
+
user = append_payload(encode_utf16(@user))
|
338
|
+
workstation = append_payload(encode_utf16(@workstation))
|
339
|
+
else
|
340
|
+
domain = append_payload(@domain)
|
341
|
+
user = append_payload(@user)
|
342
|
+
workstation = append_payload(@workstation)
|
343
|
+
end
|
344
|
+
|
345
|
+
if @session_key
|
346
|
+
set(:NEGOTIATE_KEY_EXCH)
|
347
|
+
session_key = append_payload(@session_key)
|
348
|
+
end
|
349
|
+
|
350
|
+
if @version
|
351
|
+
set(:NEGOTIATE_VERSION)
|
352
|
+
version = encode_version(@version)
|
353
|
+
end
|
354
|
+
|
355
|
+
[SSP_SIGNATURE, TYPE, lm_response, nt_response, domain, user, workstation, session_key, @flag, version, @mic].pack('a8Va8a8a8a8a8a8Va8a16') + @buffer
|
356
|
+
end
|
357
|
+
|
358
|
+
end # Authenticate
|
359
|
+
|
360
|
+
end # Message
|
361
|
+
end # NTLM
|
data/lib/ntlm/smtp.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'ntlm'
|
2
|
+
require 'net/smtp'
|
3
|
+
|
4
|
+
module Net
|
5
|
+
class SMTP
|
6
|
+
|
7
|
+
def capable_ntlm_auth?
|
8
|
+
auth_capable?('NTLM')
|
9
|
+
end
|
10
|
+
|
11
|
+
def auth_ntlm(user, secret)
|
12
|
+
check_auth_args(user, secret)
|
13
|
+
if user.index('\\')
|
14
|
+
domain, user = user.split('\\', 2)
|
15
|
+
else
|
16
|
+
domain = ''
|
17
|
+
end
|
18
|
+
|
19
|
+
res = critical {
|
20
|
+
r = get_response("AUTH NTLM #{::NTLM.negotiate.to_base64}")
|
21
|
+
check_auth_continue(r)
|
22
|
+
challenge = r.string.split(/ /, 2).last.unpack('m').first
|
23
|
+
get_response(::NTLM.authenticate(challenge, user, domain, secret).to_base64)
|
24
|
+
}
|
25
|
+
check_auth_response(res)
|
26
|
+
res
|
27
|
+
end
|
28
|
+
|
29
|
+
end # SMTP
|
30
|
+
end # Net
|
data/lib/ntlm/util.rb
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
# vim: set et sw=2 sts=2:
|
2
|
+
|
3
|
+
require 'openssl'
|
4
|
+
|
5
|
+
module NTLM
|
6
|
+
module Util
|
7
|
+
|
8
|
+
LM_MAGIC_TEXT = 'KGS!@#$%'
|
9
|
+
|
10
|
+
module_function
|
11
|
+
|
12
|
+
if RUBY_VERSION >= '1.9'
|
13
|
+
|
14
|
+
def decode_utf16(str)
|
15
|
+
str.encode(Encoding::UTF_8, Encoding::UTF_16LE)
|
16
|
+
end
|
17
|
+
|
18
|
+
def encode_utf16(str)
|
19
|
+
str.to_s.encode(Encoding::UTF_16LE).force_encoding(Encoding::ASCII_8BIT)
|
20
|
+
end
|
21
|
+
|
22
|
+
else
|
23
|
+
|
24
|
+
require 'iconv'
|
25
|
+
|
26
|
+
def decode_utf16(str)
|
27
|
+
Iconv.conv('UTF-8', 'UTF-16LE', str)
|
28
|
+
end
|
29
|
+
|
30
|
+
def encode_utf16(str)
|
31
|
+
Iconv.conv('UTF-16LE', 'UTF-8', str)
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
def create_des_keys(string)
|
37
|
+
keys = []
|
38
|
+
string = string.dup
|
39
|
+
until (key = string.slice!(0, 7)).empty?
|
40
|
+
# key is 56 bits
|
41
|
+
key = key.unpack('B*').first
|
42
|
+
str = ''
|
43
|
+
until (bits = key.slice!(0, 7)).empty?
|
44
|
+
str << bits
|
45
|
+
str << (bits.count('1').even? ? '1' : '0') # parity
|
46
|
+
end
|
47
|
+
keys << [str].pack('B*')
|
48
|
+
end
|
49
|
+
keys
|
50
|
+
end
|
51
|
+
|
52
|
+
def encrypt(plain_text, key, key_length)
|
53
|
+
key = key.ljust(key_length, "\0")
|
54
|
+
keys = create_des_keys(key[0, key_length])
|
55
|
+
|
56
|
+
result = ''
|
57
|
+
cipher = OpenSSL::Cipher::DES.new
|
58
|
+
keys.each do |k|
|
59
|
+
cipher.encrypt
|
60
|
+
cipher.key = k
|
61
|
+
result << cipher.update(plain_text)
|
62
|
+
end
|
63
|
+
|
64
|
+
result
|
65
|
+
end
|
66
|
+
|
67
|
+
# [MS-NLMP] 3.3.1
|
68
|
+
def lm_v1_hash(password)
|
69
|
+
encrypt(LM_MAGIC_TEXT, password.upcase, 14)
|
70
|
+
end
|
71
|
+
|
72
|
+
# [MS-NLMP] 3.3.1
|
73
|
+
def nt_v1_hash(password)
|
74
|
+
OpenSSL::Digest::MD4.digest(encode_utf16(password))
|
75
|
+
end
|
76
|
+
|
77
|
+
# [MS-NLMP] 3.3.1
|
78
|
+
def ntlm_v1_response(challenge, password, options = {})
|
79
|
+
if options[:ntlm_v2_session]
|
80
|
+
client_challenge = options[:client_challenge] || OpenSSL::Random.random_bytes(8)
|
81
|
+
hash = OpenSSL::Digest::MD5.digest(challenge + client_challenge)[0, 8]
|
82
|
+
nt_response = encrypt(hash, nt_v1_hash(password), 21)
|
83
|
+
lm_response = client_challenge + ("\0" * 16)
|
84
|
+
else
|
85
|
+
nt_response = encrypt(challenge, nt_v1_hash(password), 21)
|
86
|
+
lm_response = encrypt(challenge, lm_v1_hash(password), 21)
|
87
|
+
end
|
88
|
+
|
89
|
+
[nt_response, lm_response]
|
90
|
+
end
|
91
|
+
|
92
|
+
|
93
|
+
# [MS-NLMP] 3.3.2
|
94
|
+
def nt_v2_hash(user, password, domain)
|
95
|
+
user_domain = encode_utf16(user.upcase + domain)
|
96
|
+
OpenSSL::HMAC.digest(OpenSSL::Digest::MD5.new, nt_v1_hash(password), user_domain)
|
97
|
+
end
|
98
|
+
|
99
|
+
# [MS-NLMP] 3.3.2
|
100
|
+
def ntlm_v2_response(*)
|
101
|
+
raise NotImplemnetedError
|
102
|
+
end
|
103
|
+
|
104
|
+
end # Util
|
105
|
+
end # NTLM
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# Compiling the Gem
|
2
|
+
# gem build ruby-ntlm-namespace.gemspec
|
3
|
+
# gem install ./ruby-ntlm-namespace-x.x.x.gem --no-ri --no-rdoc --local
|
4
|
+
#
|
5
|
+
# gem push ruby-ntlm-namespace-x.x.x.gem
|
6
|
+
# gem list -r ruby-ntlm-namespace
|
7
|
+
# gem install ruby-ntlm-namespace
|
8
|
+
|
9
|
+
$:.push File.expand_path('../lib', __FILE__)
|
10
|
+
|
11
|
+
Gem::Specification.new do |s|
|
12
|
+
s.name = 'ruby-ntlm-namespace'
|
13
|
+
s.version = '0.0.1'
|
14
|
+
s.authors = ['Remo Mueller']
|
15
|
+
s.email = 'remosm@gmail.com'
|
16
|
+
s.homepage = 'https://github.com/remomueller'
|
17
|
+
s.summary = "Gem release for mademaxus' fork of macks ruby-ntlm with namespacing fixed"
|
18
|
+
s.description = "Gem release for mademaxus' fork of macks ruby-ntlm with namespacing fixed"
|
19
|
+
|
20
|
+
s.platform = Gem::Platform::RUBY
|
21
|
+
|
22
|
+
s.files = Dir["{app,config,db,lib}/**/*"] + ["ruby-ntlm-namespace.gemspec", "Rakefile", "VERSION", "README.markdown"]
|
23
|
+
s.test_files = Dir["test/**/*"]
|
24
|
+
end
|
data/test/auth_test.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# vim: set et sw=2 sts=2:
|
2
|
+
|
3
|
+
require File.dirname(__FILE__) + '/test_helper'
|
4
|
+
|
5
|
+
class AuthenticationTest < Test::Unit::TestCase
|
6
|
+
|
7
|
+
include NTLM::TestUtility
|
8
|
+
include NTLM::Util
|
9
|
+
|
10
|
+
def setup
|
11
|
+
@challenge = hex_to_bin("4e 54 4c 4d 53 53 50 00 02 00 00 00 0c 00 0c 00 38 00 00 00 05 82 01 00 11 11 11 11 11 11 11 11 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 44 00 6f 00 6d 00 61 00 69 00 6e 00")
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_negotiate
|
15
|
+
assert_equal(hex_to_bin("4e 54 4c 4d 53 53 50 00 01 00 00 00 07 82 08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00"), NTLM.negotiate.to_s)
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_authenticate
|
19
|
+
assert_equal(hex_to_bin("4e 54 4c 4d 53 53 50 00 03 00 00 00 18 00 18 00 58 00 00 00 18 00 18 00 70 00 00 00 0c 00 0c 00 88 00 00 00 08 00 08 00 94 00 00 00 00 00 00 00 9c 00 00 00 00 00 00 00 00 00 00 00 05 82 08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 21 b0 c5 31 28 0e ed 8d 32 c3 1b ce b2 19 5a fd 58 2b b7 8e a0 d5 f2 78 8d 76 96 b7 58 49 16 14 2d 09 f0 a0 1f f2 35 10 be 2c ff 96 82 e0 e3 3b 44 00 6f 00 6d 00 61 00 69 00 6e 00 55 00 73 00 65 00 72 00"), NTLM.authenticate(@challenge, 'User', 'Domain', 'Password').to_s)
|
20
|
+
|
21
|
+
challenge = NTLM::Message::Challenge.parse(@challenge)
|
22
|
+
challenge.set(:NEGOTIATE_EXTENDED_SECURITY)
|
23
|
+
|
24
|
+
assert_equal(hex_to_bin("4e 54 4c 4d 53 53 50 00 03 00 00 00 18 00 18 00 58 00 00 00 18 00 18 00 70 00 00 00 0c 00 0c 00 88 00 00 00 08 00 08 00 94 00 00 00 00 00 00 00 9c 00 00 00 00 00 00 00 00 00 00 00 05 82 08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 22 22 22 22 22 22 22 22 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 c2 1e db 62 54 34 d2 13 34 1a 04 3d f3 01 6d f3 01 c9 32 b4 ae 97 1e ac 44 00 6f 00 6d 00 61 00 69 00 6e 00 55 00 73 00 65 00 72 00"), NTLM.authenticate(challenge.to_s, 'User', 'Domain', 'Password', :client_challenge => "\x22" * 8).to_s)
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# vim: set et sw=2 sts=2:
|
2
|
+
|
3
|
+
require File.dirname(__FILE__) + '/test_helper'
|
4
|
+
|
5
|
+
class FunctionTest < Test::Unit::TestCase
|
6
|
+
# Test pattern is borrowed from pyton-ntlm
|
7
|
+
|
8
|
+
include NTLM::TestUtility
|
9
|
+
include NTLM::Util
|
10
|
+
|
11
|
+
def setup
|
12
|
+
@server_challenge = hex_to_bin('01 23 45 67 89 ab cd ef')
|
13
|
+
@client_challenge = "\xaa" * 8
|
14
|
+
@time = "\0" * 8
|
15
|
+
@workstation = 'COMPUTER'
|
16
|
+
@server_name = 'Server'
|
17
|
+
@user = 'User'
|
18
|
+
@domain = 'Domain'
|
19
|
+
@password = 'Password'
|
20
|
+
@random_session_key = "\55" * 16
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_lm_v1_hash
|
24
|
+
assert_equal(hex_to_bin("e5 2c ac 67 41 9a 9a 22 4a 3b 10 8f 3f a6 cb 6d"), lm_v1_hash(@password))
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_nt_v1_hash
|
28
|
+
assert_equal(hex_to_bin("a4 f4 9c 40 65 10 bd ca b6 82 4e e7 c3 0f d8 52"), nt_v1_hash(@password))
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_ntlm_v1_response
|
32
|
+
nt_response, lm_response = ntlm_v1_response(@server_challenge, @password)
|
33
|
+
assert_equal(hex_to_bin("67 c4 30 11 f3 02 98 a2 ad 35 ec e6 4f 16 33 1c 44 bd be d9 27 84 1f 94"), nt_response, 'nt_response')
|
34
|
+
assert_equal(hex_to_bin("98 de f7 b8 7f 88 aa 5d af e2 df 77 96 88 a1 72 de f1 1c 7d 5c cd ef 13"), lm_response, 'lm_response')
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_ntlm_v1_response_with_ntlm_v2_session_security
|
38
|
+
nt_response, lm_response = ntlm_v1_response(@server_challenge, @password, :ntlm_v2_session => true, :client_challenge => @client_challenge)
|
39
|
+
assert_equal(hex_to_bin("75 37 f8 03 ae 36 71 28 ca 45 82 04 bd e7 ca f8 1e 97 ed 26 83 26 72 32"), nt_response, 'nt_response')
|
40
|
+
assert_equal(hex_to_bin("aa aa aa aa aa aa aa aa 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00"), lm_response, 'lm_response')
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# vim: set et sw=2 sts=2:
|
2
|
+
|
3
|
+
require 'test/unit'
|
4
|
+
|
5
|
+
$LOAD_PATH << File.dirname(__FILE__) + '/../lib'
|
6
|
+
require 'ntlm'
|
7
|
+
|
8
|
+
module NTLM
|
9
|
+
module TestUtility
|
10
|
+
|
11
|
+
def bin_to_hex(bin)
|
12
|
+
bin.unpack('H*').first.gsub(/..(?=.)/, '\0 ')
|
13
|
+
end
|
14
|
+
|
15
|
+
def hex_to_bin(hex)
|
16
|
+
[hex.delete(' ')].pack('H*')
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
metadata
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ruby-ntlm-namespace
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Remo Mueller
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-02-11 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: Gem release for mademaxus' fork of macks ruby-ntlm with namespacing fixed
|
15
|
+
email: remosm@gmail.com
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- lib/ntlm/http.rb
|
21
|
+
- lib/ntlm/imap.rb
|
22
|
+
- lib/ntlm/mechanize.rb
|
23
|
+
- lib/ntlm/message.rb
|
24
|
+
- lib/ntlm/smtp.rb
|
25
|
+
- lib/ntlm/util.rb
|
26
|
+
- lib/ntlm.rb
|
27
|
+
- ruby-ntlm-namespace.gemspec
|
28
|
+
- Rakefile
|
29
|
+
- VERSION
|
30
|
+
- README.markdown
|
31
|
+
- test/auth_test.rb
|
32
|
+
- test/function_test.rb
|
33
|
+
- test/test_helper.rb
|
34
|
+
homepage: https://github.com/remomueller
|
35
|
+
licenses: []
|
36
|
+
post_install_message:
|
37
|
+
rdoc_options: []
|
38
|
+
require_paths:
|
39
|
+
- lib
|
40
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
47
|
+
none: false
|
48
|
+
requirements:
|
49
|
+
- - ! '>='
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: '0'
|
52
|
+
requirements: []
|
53
|
+
rubyforge_project:
|
54
|
+
rubygems_version: 1.8.24
|
55
|
+
signing_key:
|
56
|
+
specification_version: 3
|
57
|
+
summary: Gem release for mademaxus' fork of macks ruby-ntlm with namespacing fixed
|
58
|
+
test_files:
|
59
|
+
- test/auth_test.rb
|
60
|
+
- test/function_test.rb
|
61
|
+
- test/test_helper.rb
|