innards 0.1.0.pre → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,24 @@
1
+ require 'uri'
2
+
3
+ module Innards
4
+
5
+ # Handles Digest Authentication
6
+ class CookieHandler
7
+
8
+ def initialize(params)
9
+ @params = params
10
+ end
11
+
12
+ def parse cookie_header
13
+ cookie_header.scan(/(\w+)=(.+?);/) {
14
+ @params[:cookies].push([$1, $2])
15
+ }
16
+ end
17
+
18
+ def build
19
+ cookies = @params[:cookies]
20
+ URI.encode_www_form(cookies) unless cookies.empty?
21
+ end
22
+
23
+ end
24
+ end
@@ -0,0 +1,93 @@
1
+ module Innards
2
+
3
+ # Handles Digest Authentication
4
+ class DigestHandler
5
+
6
+ include Innards::Exceptions
7
+
8
+ def initialize(params)
9
+ @params = params
10
+ end
11
+
12
+ def parse www_authenticate_header
13
+ digest = {}
14
+ www_authenticate_header.scan(/(\w+)="(.*?)"/) {
15
+ digest[$1] = $2
16
+ }
17
+ if www_authenticate_header =~ /stale=true$/
18
+ digest[:stale] = true
19
+ raise StaleDigestException.new
20
+ end
21
+ @params[:digest] = digest
22
+ end
23
+
24
+ def build_for path
25
+ digest = @params[:digest]
26
+ return nil if digest.empty?
27
+
28
+ cnonce = generate_md5_hash(random)
29
+ header = [
30
+ %Q(Digest username="#{@params[:login_uri].user}"),
31
+ %Q(realm="#{digest['realm']}"),
32
+ %Q(nonce="#{digest['nonce']}"),
33
+ %Q(uri="#{path}"),
34
+ %Q(response="#{build_digest(path, cnonce)}"),
35
+ ]
36
+
37
+ if has_qop?
38
+ fields = [
39
+ %Q(cnonce="#{cnonce}"),
40
+ %Q(qop="#{digest['qop']}"),
41
+ %Q(nc=#{request_nc})
42
+ ]
43
+ fields.each { |field| header << field }
44
+ end
45
+
46
+ header << %Q(opaque="#{digest['opaque']}") if has_opaque?
47
+ header.join(", ")
48
+ end
49
+
50
+ def build_digest path, cnonce
51
+ digest = @params[:digest]
52
+ digest_parts = [
53
+ generate_md5_hash(generate_ha1_hash),
54
+ digest['nonce'],
55
+ generate_md5_hash(generate_ha2_hash(path))]
56
+ digest_parts.insert(2, request_nc, cnonce, digest['qop']) if has_qop?
57
+ generate_md5_hash(digest_parts.join(":"))
58
+ end
59
+
60
+ def has_opaque?
61
+ digest = @params[:digest]
62
+ digest.has_key?('opaque') and !digest['opaque'].empty?
63
+ end
64
+
65
+ def has_qop?
66
+ digest = @params[:digest]
67
+ digest.has_key?('qop') and !digest['qop'].empty?
68
+ end
69
+
70
+ def generate_ha1_hash
71
+ login_uri = @params[:login_uri]
72
+ digest = @params[:digest]
73
+ [login_uri.user, digest['realm'], login_uri.password].join(":")
74
+ end
75
+
76
+ def generate_ha2_hash(path)
77
+ ["GET", path].join(":")
78
+ end
79
+
80
+ def request_nc
81
+ "%08d" % @params[:request_count]
82
+ end
83
+
84
+ def random
85
+ "%x" % (Time.now.to_i + rand(65535))
86
+ end
87
+
88
+ def generate_md5_hash(str)
89
+ Digest::MD5.hexdigest(str)
90
+ end
91
+
92
+ end
93
+ end
@@ -1,8 +1,35 @@
1
1
  module Innards
2
2
  module Exceptions
3
-
4
- class AuthenticationRequiredException < Exception
5
- end
6
-
3
+
4
+ # Thrown when a invalid URL is passed
5
+ class InvalidUrlException < StandardError; end
6
+
7
+ # Thrown when a invalid/unsupported RETS Version is passed
8
+ class InvalidRetsVersionException < StandardError; end
9
+
10
+ # Thrown when a invalid Auth Mode is passed
11
+ class InvalidAuthModeException < StandardError; end
12
+
13
+ # Thrown when a invalid User Agent is passed
14
+ class InvalidUserAgentException < StandardError; end
15
+
16
+ # Thrown when the rets conection requires authentication
17
+ class AuthenticationRequiredException < StandardError; end
18
+
19
+ # Thrown when digest data is stale and the request needs to be retried
20
+ class StaleDigestException < StandardError; end
21
+
22
+ # Thrown when a function is called which requres a logged in session
23
+ class LoginRequiredException < StandardError; end
24
+
25
+ # Invalid Search Type
26
+ class InvalidSearchTypeException < StandardError; end
27
+
28
+ # Invalid Search Type
29
+ class InvalidClassException < StandardError; end
30
+
31
+ # Invalid Search Type
32
+ class InvalidQueryException < StandardError; end
33
+
7
34
  end
8
35
  end
@@ -0,0 +1,31 @@
1
+ require 'multipart_parser/reader'
2
+
3
+ module Innards
4
+
5
+ # Handles Digest Authentication
6
+ class MultipartHandler
7
+
8
+ def initialize(content_type)
9
+ @boundary = MultipartParser::Reader.extract_boundary_value(content_type)
10
+ rescue MultipartParser::NotMultipartError
11
+ @boundary = ""
12
+ end
13
+
14
+ def parse body
15
+ parts = []
16
+ multipart_reader = MultipartParser::Reader.new @boundary
17
+ multipart_reader.on_part do |part|
18
+ part.on_data do |data|
19
+ parts << part.headers.merge({:data => data})
20
+ end
21
+ end
22
+ multipart_reader.write body.strip
23
+ parts
24
+ end
25
+
26
+ def is_multipart_response?
27
+ !@boundary.empty?
28
+ end
29
+
30
+ end
31
+ end
@@ -0,0 +1,64 @@
1
+
2
+ module Innards
3
+ module Parsers
4
+
5
+ # SAX Parser for RETS Metadata
6
+ class GetMetadataParser < ParserBase
7
+
8
+ METADATA_SECTIONS = {
9
+ "METADATA-RESOURCE" => :resources,
10
+ "METADATA-CLASS" => :classes,
11
+ "METADATA-TABLE" => :tables,
12
+ "METADATA-OBJECT" => :objects,
13
+ "METADATA-SEARCH_HELP" => :search_help,
14
+ "METADATA-EDITMASK" => :edit_masks,
15
+ "METADATA-LOOKUP" => :lookups,
16
+ "METADATA-LOOKUP_TYPE" => :lookup_types
17
+ }
18
+
19
+ def initialize
20
+ super()
21
+ @current_builder = {}
22
+ end
23
+
24
+ def start_element(name)
25
+ super
26
+ end
27
+
28
+ def end_element(name)
29
+ super
30
+ if METADATA_SECTIONS.has_key?(name.to_s)
31
+ @current_builder = {}
32
+ end
33
+ end
34
+
35
+ def attr(name, value)
36
+ super
37
+ METADATA_SECTIONS.each_key do |section|
38
+ if switch_active?(:"#{section}")
39
+ @current_builder[name.to_sym] = value
40
+ end
41
+ end
42
+ end
43
+
44
+ def text(value)
45
+ super
46
+
47
+ if switch_active?(:COLUMNS)
48
+ @current_builder[:columns] = data_splitter(value)
49
+ end
50
+
51
+ METADATA_SECTIONS.each_pair do |section, symbol|
52
+ if switch_active?(:"#{section}") and switch_active?(:DATA)
53
+ parsed_data = data_merger(@current_builder[:columns], data_splitter(value))
54
+ filtered = @current_builder.reject{ |key| key == :columns }
55
+
56
+ @metadata[symbol] = [] unless @metadata.has_key?(symbol)
57
+ @metadata[symbol].push parsed_data.merge(filtered)
58
+ end
59
+ end
60
+ end
61
+
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,29 @@
1
+
2
+ module Innards
3
+ module Parsers
4
+
5
+ # SAX Parser for RETS Login Response
6
+ class LoginParser < ParserBase
7
+
8
+ def start_element(name)
9
+ super
10
+ end
11
+
12
+ def end_element(name)
13
+ super
14
+ end
15
+
16
+ def attr(name, value)
17
+ super
18
+ end
19
+
20
+ def text(value)
21
+ super
22
+ if switch_active?(:RETS)
23
+ @metadata.merge! split_multiline_key_value_pairs(value)
24
+ end
25
+ end
26
+
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,29 @@
1
+
2
+ module Innards
3
+ module Parsers
4
+
5
+ # SAX Parser for RETS Logout Response
6
+ class LogoutParser < ParserBase
7
+
8
+ def start_element(name)
9
+ super
10
+ end
11
+
12
+ def end_element(name)
13
+ super
14
+ end
15
+
16
+ def attr(name, value)
17
+ super
18
+ end
19
+
20
+ def text(value)
21
+ super
22
+ if switch_active?(:"RETS-RESPONSE")
23
+ @metadata.merge! split_multiline_key_value_pairs(value)
24
+ end
25
+ end
26
+
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,73 @@
1
+ require 'uri'
2
+ require 'ox'
3
+
4
+ module Innards
5
+ module Parsers
6
+
7
+ # Base Parser from which all RETS Response Parsers are based
8
+ class ParserBase < ::Ox::Sax
9
+
10
+ attr_reader :rets_response, :metadata
11
+
12
+ def initialize
13
+ @switches = {}
14
+ @metadata = {}
15
+ end
16
+
17
+ def start_element(name)
18
+ element_tracker_switch name, true
19
+ end
20
+
21
+ def end_element(name)
22
+ element_tracker_switch name, false
23
+ end
24
+
25
+ def attr(name, value)
26
+ if switch_active?(:RETS)
27
+ @rets_response = {} unless @rets_response.kind_of?(Hash)
28
+ @rets_response[name] = value
29
+ end
30
+ end
31
+
32
+ def text(value)
33
+ end
34
+
35
+ def split_multiline_key_value_pairs value
36
+ result = {}
37
+ value.scan(/(\w+) = (.*?)$/) do |match|
38
+ result[match[0].to_sym] = match[1].gsub(/http:\/\/.+?\//, "/")
39
+ end
40
+ result
41
+ end
42
+
43
+ def valid_rets_response_received?
44
+ response_code == 0
45
+ end
46
+
47
+ def response_code
48
+ @rets_response[:ReplyCode].to_i if @rets_response.has_key?(:ReplyCode)
49
+ end
50
+
51
+ def element_tracker_switch element, currently_in
52
+ @switches[element] = currently_in
53
+ end
54
+
55
+ def switch_active? element
56
+ (@switches[element] == true)
57
+ end
58
+
59
+ def data_splitter data
60
+ data.split(/\t/)
61
+ end
62
+
63
+ def data_merger columns, data
64
+ merged_data = {}
65
+ columns.each_with_index do |object, index|
66
+ merged_data[object] = data[index] unless index == 0
67
+ end
68
+ merged_data
69
+ end
70
+
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,41 @@
1
+
2
+ module Innards
3
+ module Parsers
4
+
5
+ # SAX Parser for RETS Search Response
6
+ class SearchParser < ParserBase
7
+
8
+ attr_reader :columns, :results
9
+
10
+ def initialize
11
+ super
12
+ @results = []
13
+ end
14
+
15
+ def start_element(name)
16
+ super
17
+ end
18
+
19
+ def end_element(name)
20
+ super
21
+ end
22
+
23
+ def attr(name, value)
24
+ super
25
+ end
26
+
27
+ def text(value)
28
+ super
29
+
30
+ if switch_active?(:COLUMNS)
31
+ @columns = data_splitter(value)
32
+ end
33
+
34
+ if switch_active?(:DATA)
35
+ @results.push data_merger(@columns, data_splitter(value))
36
+ end
37
+ end
38
+
39
+ end
40
+ end
41
+ end
@@ -3,8 +3,8 @@ module Innards
3
3
  MAJOR = 0
4
4
  MINOR = 1
5
5
  PATCH = 0
6
- BUILD = 'pre'
6
+ BUILD = nil
7
7
 
8
8
  STRING = [MAJOR, MINOR, PATCH, BUILD].compact.join('.')
9
9
  end
10
- end
10
+ end
@@ -0,0 +1,232 @@
1
+ require "spec_helper"
2
+
3
+ describe Innards::Connection do
4
+
5
+ context "during intialization" do
6
+ before do
7
+ @valid_hash = {:login_url => "http://www.dis.com:6103/rets/login"}
8
+ @invalid_hash = {:login_url => ""}
9
+ end
10
+
11
+ it "should validate the URL" do
12
+ expect { Innards::Connection.new(@valid_hash) }.not_to raise_error()
13
+ expect { Innards::Connection.new(@invalid_hash) }.to raise_error()
14
+ end
15
+
16
+ it "should valididate RETS Version" do
17
+ expect { Innards::Connection.new(@valid_hash.merge({:rets_version => "1.5"})) }.not_to raise_error()
18
+ expect { Innards::Connection.new(@valid_hash.merge({:rets_version => "1.7"})) }.not_to raise_error()
19
+ expect { Innards::Connection.new(@valid_hash.merge({:rets_version => "1.7.2"})) }.not_to raise_error()
20
+ expect { Innards::Connection.new(@valid_hash.merge({:rets_version => "1.8"})) }.not_to raise_error()
21
+ expect { Innards::Connection.new(@valid_hash.merge({:rets_version => "0.9"})) }.to raise_error()
22
+ end
23
+
24
+ it "should validate auth type" do
25
+ expect { Innards::Connection.new(@valid_hash.merge({:auth_mode => "basic"})) }.not_to raise_error()
26
+ expect { Innards::Connection.new(@valid_hash.merge({:auth_mode => "digest"})) }.not_to raise_error()
27
+ expect { Innards::Connection.new(@valid_hash.merge({:auth_mode => "invalid"})) }.to raise_error()
28
+ end
29
+
30
+ it "doesn't accept a blank user agent" do
31
+ expect { Innards::Connection.new(@valid_hash.merge({:user_agent => ""})) }.to raise_error()
32
+ end
33
+ end
34
+
35
+ context "during connect" do
36
+ before do
37
+ Excon.defaults[:mock] = true
38
+ stub_path = File.expand_path("../../stubs", __FILE__)
39
+ Excon.stub({:method => :get}) do |params|
40
+ rets_version = "RETS/1.7.2"
41
+ rets_server = "StubRETS/0.0.1"
42
+ case params[:path]
43
+ when "/rets/login"
44
+ {:body => File.read("#{stub_path}/login_success.xml"), :headers => {
45
+ "RETS-Version" => rets_version,
46
+ "RETS-Server" => rets_server,
47
+ "WWW-Authenticate" => "Digest realm=\"testrealm@host.com\", qop=\"auth,auth-int\", nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"",
48
+ "Set-Cookie" => "name2=value2; Expires=Wed, 09 Jun 2021 10:18:14 GMT"
49
+ }, :status => 200}
50
+ when "/rets/getMetadata"
51
+ {:body => File.read("#{stub_path}/metadata_success.xml"), :headers => {
52
+ "RETS-Version" => rets_version,
53
+ "RETS-Server" => rets_server,
54
+ }, :status => 200}
55
+ when "/rets/staleDigest"
56
+ {:body => "", :headers => {
57
+ "RETS-Version" => rets_version,
58
+ "RETS-Server" => rets_server,
59
+ "WWW-Authenticate" => "Digest realm=\"testrealm@host.com\", qop=\"auth,auth-int\", nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", opaque=\"5ccc069c403ebaf9f0171e9517f40e41\" stale=true",
60
+ }, :status => 200}
61
+ when "/rets/retsError"
62
+ {:body => File.read("#{stub_path}/rets_error.xml"), :headers => {
63
+ "RETS-Version" => rets_version,
64
+ "RETS-Server" => rets_server,
65
+ }, :status => 200}
66
+ when "/rets/search"
67
+ {:body => File.read("#{stub_path}/search_success.xml"), :headers => {
68
+ "RETS-Version" => rets_version,
69
+ "RETS-Server" => rets_server,
70
+ }, :status => 200}
71
+ when "/rets/logout"
72
+ {:body => File.read("#{stub_path}/logout_success.xml"), :headers => {
73
+ "RETS-Version" => rets_version,
74
+ "RETS-Server" => rets_server,
75
+ }, :status => 200}
76
+ when "/rets/getObject"
77
+ {:body => File.read("#{stub_path}/getobject_success.txt"), :headers => {
78
+ "RETS-Version" => rets_version,
79
+ "RETS-Server" => rets_server,
80
+ "Cache-Control" => "private",
81
+ "MIME-Version" => "1.0",
82
+ "Content-Type" => "multipart/parallel; boundary=\"aa58ae639983623715598443ccfe159ccf009de8\"",
83
+ "Transfer-Encoding" => "chunked"
84
+ }, :status => 200}
85
+ end
86
+ end
87
+ @demo_rets = Innards::Connection.new(:login_url => "http://Joe:Schmoe@www.dis.com:6103/rets/login")
88
+ end
89
+
90
+ after do
91
+ Excon.stubs.clear
92
+ end
93
+
94
+ it "should throw an exception if get metadata called before login" do
95
+ expect { @demo_rets.get_metadata }.to raise_error
96
+ @demo_rets.login!.should be_true
97
+ expect { @demo_rets.get_metadata }.not_to raise_error
98
+ end
99
+
100
+ it "should login and logout of the demo RETS Server returning associated metadata" do
101
+ @demo_rets.login!.should be_true
102
+ @demo_rets.params[:metadata].should be_kind_of(Hash)
103
+ @demo_rets.params[:metadata].should_not be_empty
104
+ @demo_rets.params[:metadata][:MemberName].should match "Joe Schmoe"
105
+ @demo_rets.params[:metadata][:MetadataVersion].should match "1.00.00001"
106
+ @demo_rets.params[:metadata][:MinMetadataVersion].should match "1.00.00001"
107
+ @demo_rets.params[:metadata][:User].should match "Joe,NULL,NULL,NULL"
108
+ @demo_rets.params[:metadata][:Login].should match "/rets/login"
109
+ @demo_rets.params[:metadata][:Logout].should match "/rets/logout"
110
+ @demo_rets.params[:metadata][:Search].should match "/rets/search"
111
+ @demo_rets.params[:metadata][:GetMetadata].should match "/rets/getMetadata"
112
+ @demo_rets.params[:metadata][:GetObject].should match "/rets/getObject"
113
+ @demo_rets.params[:metadata][:Balance].should match "116,805.54"
114
+ @demo_rets.params[:metadata][:TimeoutSeconds].should match "1800"
115
+ end
116
+
117
+ it "should throw an error on a stale digest header" do
118
+ @demo_rets.login!.should be_true
119
+ @demo_rets.params[:metadata][:GetMetadata] = "/rets/staleDigest"
120
+ expect { @demo_rets.get_metadata }.to raise_error
121
+ end
122
+
123
+ it "should get the associated metadata for the server" do
124
+
125
+ @demo_rets.login!.should be_true
126
+ metadata = @demo_rets.get_metadata
127
+
128
+ metadata.should have_key(:resources)
129
+ metadata[:resources].should_not be_empty
130
+
131
+ metadata.should have_key(:classes)
132
+ metadata[:classes].should_not be_empty
133
+
134
+ metadata.should have_key(:tables)
135
+ metadata[:tables].should_not be_empty
136
+
137
+ metadata.should have_key(:objects)
138
+ metadata[:objects].should_not be_empty
139
+
140
+ metadata.should have_key(:search_help)
141
+ metadata[:search_help].should_not be_empty
142
+
143
+ metadata.should have_key(:edit_masks)
144
+ metadata[:edit_masks].should_not be_empty
145
+
146
+ metadata.should have_key(:lookups)
147
+ metadata[:lookups].should_not be_empty
148
+
149
+ metadata.should have_key(:lookup_types)
150
+ metadata[:lookup_types].should_not be_empty
151
+
152
+ end
153
+
154
+ it "should return false on get metadata error" do
155
+ @demo_rets.login!.should be_true
156
+ @demo_rets.params[:metadata][:GetMetadata] = "/rets/retsError"
157
+ @demo_rets.get_metadata.should be_false
158
+ end
159
+
160
+ it "should throw an error if logout called before login" do
161
+ expect { @demo_rets.logout! }.to raise_error
162
+ end
163
+
164
+ it "should correctly log out" do
165
+ @demo_rets.login!.should be_true
166
+ expect { @demo_rets.logout! }.not_to raise_error
167
+ @demo_rets.params[:metadata][:ConnectTime].should match "6"
168
+ @demo_rets.params[:metadata][:Billing].should match "0.04"
169
+ @demo_rets.params[:metadata][:SignOffMessage].should match "Goodbye"
170
+ end
171
+
172
+ it "should return false on logout error" do
173
+ @demo_rets.login!.should be_true
174
+ @demo_rets.params[:metadata][:Logout] = "/rets/retsError"
175
+ @demo_rets.logout!.should be_false
176
+ end
177
+
178
+ it "should perform a basic search" do
179
+ @demo_rets.login!.should be_true
180
+
181
+ expect {
182
+ @demo_rets.search()
183
+ }.to raise_error
184
+
185
+ expect {
186
+ @demo_rets.search(:SearchType => "Property",
187
+ :Class => "",
188
+ :Query => "(ListPrice=0+)")
189
+ }.to raise_error
190
+
191
+ expect {
192
+ @demo_rets.search(:SearchType => "Property",
193
+ :Class => "RES",
194
+ :Query => "")
195
+ }.to raise_error
196
+
197
+ expect {
198
+ @demo_rets.search(:SearchType => "Property",
199
+ :Class => "RES",
200
+ :Query => "(ListPrice=0+)")
201
+ }.not_to raise_error
202
+
203
+ results = @demo_rets.search(:SearchType => "Property",
204
+ :Class => "RES",
205
+ :Query => "(ListPrice=0+)")
206
+ results.should be_kind_of(Array)
207
+ results.each do |result|
208
+ result.should be_kind_of(Hash)
209
+ end
210
+ end
211
+
212
+ it "should return false on search error" do
213
+ @demo_rets.login!.should be_true
214
+ @demo_rets.params[:metadata][:Search] = "/rets/retsError"
215
+ @demo_rets.search(:SearchType => "Property",
216
+ :Class => "RES",
217
+ :Query => "(ListPrice=0+)").should be_false
218
+ end
219
+
220
+ it "should perform a GetObject request" do
221
+ @demo_rets.login!.should be_true
222
+
223
+ result = @demo_rets.get_object(:Type => "Photo",
224
+ :Resource => "Property",
225
+ :ID => "2:*")
226
+ result.should be_true
227
+
228
+ end
229
+
230
+ end
231
+
232
+ end