ruby-ntlm-namespace 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.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
|