sean-rets 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gemtest +0 -0
- data/CHANGELOG.md +89 -0
- data/Manifest.txt +28 -0
- data/README.md +47 -0
- data/Rakefile +27 -0
- data/bin/rets +202 -0
- data/lib/rets.rb +45 -0
- data/lib/rets/client.rb +391 -0
- data/lib/rets/client_progress_reporter.rb +44 -0
- data/lib/rets/http_client.rb +91 -0
- data/lib/rets/locking_http_client.rb +34 -0
- data/lib/rets/measuring_http_client.rb +27 -0
- data/lib/rets/metadata.rb +6 -0
- data/lib/rets/metadata/containers.rb +84 -0
- data/lib/rets/metadata/lookup_type.rb +17 -0
- data/lib/rets/metadata/resource.rb +84 -0
- data/lib/rets/metadata/rets_class.rb +48 -0
- data/lib/rets/metadata/root.rb +152 -0
- data/lib/rets/metadata/table.rb +113 -0
- data/lib/rets/parser/compact.rb +62 -0
- data/lib/rets/parser/multipart.rb +40 -0
- data/test/fixtures.rb +212 -0
- data/test/helper.rb +14 -0
- data/test/test_client.rb +238 -0
- data/test/test_locking_http_client.rb +29 -0
- data/test/test_metadata.rb +459 -0
- data/test/test_parser_compact.rb +86 -0
- data/test/test_parser_multipart.rb +39 -0
- data/test/vcr_cassettes/unauthorized_response.yml +262 -0
- metadata +186 -0
data/lib/rets/client.rb
ADDED
@@ -0,0 +1,391 @@
|
|
1
|
+
require 'httpclient'
|
2
|
+
require 'logger'
|
3
|
+
require_relative 'http_client'
|
4
|
+
|
5
|
+
module Rets
|
6
|
+
class HttpError < StandardError ; end
|
7
|
+
|
8
|
+
class Client
|
9
|
+
DEFAULT_OPTIONS = {}
|
10
|
+
|
11
|
+
COUNT = Struct.new(:exclude, :include, :only).new(0,1,2)
|
12
|
+
|
13
|
+
attr_accessor :login_url, :options, :logger
|
14
|
+
attr_writer :capabilities, :metadata
|
15
|
+
|
16
|
+
def initialize(options)
|
17
|
+
@options = options
|
18
|
+
clean_setup
|
19
|
+
end
|
20
|
+
|
21
|
+
def clean_setup
|
22
|
+
self.options = DEFAULT_OPTIONS.merge(@options)
|
23
|
+
self.login_url = self.options[:login_url]
|
24
|
+
|
25
|
+
@cached_metadata = nil
|
26
|
+
@capabilities = nil
|
27
|
+
@metadata = nil
|
28
|
+
@tries = nil
|
29
|
+
self.capabilities = nil
|
30
|
+
|
31
|
+
self.logger = @options[:logger] || FakeLogger.new
|
32
|
+
@client_progress = ClientProgressReporter.new(self.logger, options[:stats_collector], options[:stats_prefix])
|
33
|
+
@cached_metadata = @options[:metadata]
|
34
|
+
if @options[:http_proxy]
|
35
|
+
@http = HTTPClient.new(options.fetch(:http_proxy))
|
36
|
+
|
37
|
+
if @options[:proxy_username]
|
38
|
+
@http.set_proxy_auth(options.fetch(:proxy_username), options.fetch(:proxy_password))
|
39
|
+
end
|
40
|
+
else
|
41
|
+
@http = HTTPClient.new
|
42
|
+
end
|
43
|
+
|
44
|
+
if @options[:receive_timeout]
|
45
|
+
@http.receive_timeout = @options[:receive_timeout]
|
46
|
+
end
|
47
|
+
|
48
|
+
@http.set_cookie_store(options[:cookie_store]) if options[:cookie_store]
|
49
|
+
|
50
|
+
@http_client = Rets::HttpClient.new(@http, @options, @logger, @login_url)
|
51
|
+
if options[:http_timing_stats_collector]
|
52
|
+
@http_client = Rets::MeasuringHttpClient.new(@http_client, options.fetch(:http_timing_stats_collector), options.fetch(:http_timing_stats_prefix))
|
53
|
+
end
|
54
|
+
if options[:lock_around_http_requests]
|
55
|
+
@http_client = Rets::LockingHttpClient.new(@http_client, options.fetch(:locker), options.fetch(:lock_name), options.fetch(:lock_options))
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Attempts to login by making an empty request to the URL
|
60
|
+
# provided in initialize. Returns the capabilities that the
|
61
|
+
# RETS server provides, per http://retsdoc.onconfluence.com/display/rets172/4.10+Capability+URL+List.
|
62
|
+
def login
|
63
|
+
res = http_get(login_url)
|
64
|
+
unless res.status_code == 200
|
65
|
+
raise UnknownResponse, "bad response to login, expected a 200, but got #{res.status_code}. Body was #{res.body}."
|
66
|
+
end
|
67
|
+
self.capabilities = extract_capabilities(Nokogiri.parse(res.body))
|
68
|
+
raise UnknownResponse, "Cannot read rets server capabilities." unless @capabilities
|
69
|
+
@capabilities
|
70
|
+
end
|
71
|
+
|
72
|
+
def logout
|
73
|
+
unless capabilities["Logout"]
|
74
|
+
raise NoLogout.new('No logout method found for rets client')
|
75
|
+
end
|
76
|
+
http_get(capability_url("Logout"))
|
77
|
+
rescue UnknownResponse => e
|
78
|
+
unless e.message.match(/expected a 200, but got 401/)
|
79
|
+
raise e
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Finds records.
|
84
|
+
#
|
85
|
+
# [quantity] Return the first record, or an array of records.
|
86
|
+
# Uses a symbol <tt>:first</tt> or <tt>:all</tt>, respectively.
|
87
|
+
#
|
88
|
+
# [opts] A hash of arguments used to construct the search query,
|
89
|
+
# using the following keys:
|
90
|
+
#
|
91
|
+
# <tt>:search_type</tt>:: Required. The resource to search for.
|
92
|
+
# <tt>:class</tt>:: Required. The class of the resource to search for.
|
93
|
+
# <tt>:query</tt>:: Required. The DMQL2 query string to execute.
|
94
|
+
# <tt>:limit</tt>:: The number of records to request from the server.
|
95
|
+
# <tt>:resolve</tt>:: Provide resolved values that use metadata instead
|
96
|
+
# of raw system values.
|
97
|
+
#
|
98
|
+
# Any other keys are converted to the RETS query format, and passed
|
99
|
+
# to the server as part of the query. For instance, the key <tt>:offset</tt>
|
100
|
+
# will be sent as +Offset+.
|
101
|
+
#
|
102
|
+
def find(quantity, opts = {})
|
103
|
+
case quantity
|
104
|
+
when :first then find_with_retries(opts.merge(:limit => 1)).first
|
105
|
+
when :all then find_with_retries(opts)
|
106
|
+
else raise ArgumentError, "First argument must be :first or :all"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
alias search find
|
111
|
+
|
112
|
+
def find_with_retries(opts = {})
|
113
|
+
retries = 0
|
114
|
+
resolve = opts.delete(:resolve)
|
115
|
+
begin
|
116
|
+
find_every(opts, resolve)
|
117
|
+
rescue AuthorizationFailure, InvalidRequest => e
|
118
|
+
if retries < opts.fetch(:max_retries, 3)
|
119
|
+
retries += 1
|
120
|
+
@client_progress.find_with_retries_failed_a_retry(e, retries)
|
121
|
+
clean_setup
|
122
|
+
retry
|
123
|
+
else
|
124
|
+
@client_progress.find_with_retries_exceeded_retry_count(e)
|
125
|
+
raise e
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def find_every(opts, resolve)
|
131
|
+
params = {"QueryType" => "DMQL2", "Format" => "COMPACT"}.merge(fixup_keys(opts))
|
132
|
+
res = http_post(capability_url("Search"), params)
|
133
|
+
|
134
|
+
if opts[:count] == COUNT.only
|
135
|
+
Parser::Compact.get_count(res.body)
|
136
|
+
else
|
137
|
+
results = Parser::Compact.parse_document(res.body.encode("UTF-8", "binary", :invalid => :replace, :undef => :replace))
|
138
|
+
if resolve
|
139
|
+
rets_class = find_rets_class(opts[:search_type], opts[:class])
|
140
|
+
decorate_results(results, rets_class)
|
141
|
+
else
|
142
|
+
results
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def find_rets_class(resource_name, rets_class_name)
|
148
|
+
metadata.tree[resource_name].find_rets_class(rets_class_name)
|
149
|
+
end
|
150
|
+
|
151
|
+
def decorate_results(results, rets_class)
|
152
|
+
results.map do |result|
|
153
|
+
decorate_result(result, rets_class)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def decorate_result(result, rets_class)
|
158
|
+
result.each do |key, value|
|
159
|
+
table = rets_class.find_table(key)
|
160
|
+
if table
|
161
|
+
result[key] = table.resolve(value.to_s)
|
162
|
+
else
|
163
|
+
#can't resolve just leave the value be
|
164
|
+
raise "Value could not be interpreted. Key #{key} Value #{value}"
|
165
|
+
@client_progress.could_not_resolve_find_metadata(key)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
# Returns an array of all objects associated with the given resource.
|
171
|
+
def all_objects(opts = {})
|
172
|
+
objects("*", opts)
|
173
|
+
end
|
174
|
+
|
175
|
+
# Returns an array of specified objects.
|
176
|
+
def objects(object_ids, opts = {})
|
177
|
+
response = case object_ids
|
178
|
+
when String then fetch_object(object_ids, opts)
|
179
|
+
when Array then fetch_object(object_ids.join(","), opts)
|
180
|
+
else raise ArgumentError, "Expected instance of String or Array, but got #{object_ids.inspect}."
|
181
|
+
end
|
182
|
+
|
183
|
+
create_parts_from_response(response)
|
184
|
+
end
|
185
|
+
|
186
|
+
def create_parts_from_response(response)
|
187
|
+
content_type = response.header["content-type"][0]
|
188
|
+
|
189
|
+
if content_type.nil?
|
190
|
+
raise MalformedResponse, "Unable to read content-type from response: #{response.inspect}"
|
191
|
+
end
|
192
|
+
|
193
|
+
if content_type.include?("multipart")
|
194
|
+
boundary = content_type.scan(/boundary="?([^;"]*)?/).join
|
195
|
+
|
196
|
+
parts = Parser::Multipart.parse(response.body, boundary)
|
197
|
+
|
198
|
+
logger.debug "Rets::Client: Found #{parts.size} parts"
|
199
|
+
|
200
|
+
return parts
|
201
|
+
else
|
202
|
+
# fake a multipart for interface compatibility
|
203
|
+
headers = {}
|
204
|
+
response.headers.each { |k,v| headers[k] = v[0] }
|
205
|
+
|
206
|
+
part = Parser::Multipart::Part.new(headers, response.body)
|
207
|
+
|
208
|
+
return [part]
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
# Returns a single object.
|
213
|
+
#
|
214
|
+
# resource RETS resource as defined in the resource metadata.
|
215
|
+
# object_type an object type defined in the object metadata.
|
216
|
+
# resource_id the KeyField value of the given resource instance.
|
217
|
+
# object_id can be "*" or a colon delimited string of integers or an array of integers.
|
218
|
+
def object(object_id, opts = {})
|
219
|
+
response = fetch_object(Array(object_id).join(':'), opts)
|
220
|
+
response.body
|
221
|
+
end
|
222
|
+
|
223
|
+
def fetch_object(object_id, opts = {})
|
224
|
+
params = {
|
225
|
+
"Resource" => opts.fetch(:resource),
|
226
|
+
"Type" => opts.fetch(:object_type),
|
227
|
+
"ID" => "#{opts.fetch(:resource_id)}:#{object_id}",
|
228
|
+
"Location" => opts.fetch(:location, 0)
|
229
|
+
}
|
230
|
+
|
231
|
+
extra_headers = {
|
232
|
+
"Accept" => "image/jpeg, image/png;q=0.5, image/gif;q=0.1",
|
233
|
+
}
|
234
|
+
|
235
|
+
http_post(capability_url("GetObject"), params, extra_headers)
|
236
|
+
end
|
237
|
+
|
238
|
+
# Changes keys to be camel cased, per the RETS standard for queries.
|
239
|
+
def fixup_keys(hash)
|
240
|
+
fixed_hash = {}
|
241
|
+
|
242
|
+
hash.each do |key, value|
|
243
|
+
camel_cased_key = key.to_s.capitalize.gsub(/_(\w)/) { $1.upcase }
|
244
|
+
|
245
|
+
fixed_hash[camel_cased_key] = value
|
246
|
+
end
|
247
|
+
|
248
|
+
fixed_hash
|
249
|
+
end
|
250
|
+
|
251
|
+
def metadata
|
252
|
+
return @metadata if @metadata
|
253
|
+
|
254
|
+
if @cached_metadata && (@options[:skip_metadata_uptodate_check] ||
|
255
|
+
@cached_metadata.current?(capabilities["MetadataTimestamp"], capabilities["MetadataVersion"]))
|
256
|
+
@client_progress.use_cached_metadata
|
257
|
+
self.metadata = @cached_metadata
|
258
|
+
else
|
259
|
+
@client_progress.bad_cached_metadata(@cached_metadata)
|
260
|
+
self.metadata = Metadata::Root.new(logger, retrieve_metadata)
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
def retrieve_metadata
|
265
|
+
raw_metadata = {}
|
266
|
+
Metadata::METADATA_TYPES.each {|type|
|
267
|
+
raw_metadata[type] = retrieve_metadata_type(type)
|
268
|
+
}
|
269
|
+
raw_metadata
|
270
|
+
end
|
271
|
+
|
272
|
+
def retrieve_metadata_type(type)
|
273
|
+
res = http_post(capability_url("GetMetadata"),
|
274
|
+
{ "Format" => "COMPACT",
|
275
|
+
"Type" => "METADATA-#{type}",
|
276
|
+
"ID" => "0"
|
277
|
+
})
|
278
|
+
res.body
|
279
|
+
end
|
280
|
+
|
281
|
+
# The capabilies as provided by the RETS server during login.
|
282
|
+
#
|
283
|
+
# Currently, only the path in the endpoint URLs is used[1]. Host,
|
284
|
+
# port, other details remaining constant with those provided to
|
285
|
+
# the constructor.
|
286
|
+
#
|
287
|
+
# [1] In fact, sometimes only a path is returned from the server.
|
288
|
+
def capabilities
|
289
|
+
@capabilities || login
|
290
|
+
end
|
291
|
+
|
292
|
+
def capability_url(name)
|
293
|
+
val = capabilities[name] || capabilities[name.downcase]
|
294
|
+
|
295
|
+
raise UnknownCapability.new(name) unless val
|
296
|
+
|
297
|
+
begin
|
298
|
+
if val.downcase.match(/^https?:\/\//)
|
299
|
+
uri = URI.parse(val)
|
300
|
+
else
|
301
|
+
uri = URI.parse(login_url)
|
302
|
+
uri.path = val
|
303
|
+
end
|
304
|
+
rescue URI::InvalidURIError
|
305
|
+
raise MalformedResponse, "Unable to parse capability URL: #{name} => #{val.inspect}"
|
306
|
+
end
|
307
|
+
uri.to_s
|
308
|
+
end
|
309
|
+
|
310
|
+
def extract_capabilities(document)
|
311
|
+
raw_key_values = document.xpath("/RETS/RETS-RESPONSE").text.strip
|
312
|
+
|
313
|
+
hash = Hash.new{|h,k| h.key?(k.downcase) ? h[k.downcase] : nil }
|
314
|
+
|
315
|
+
# ... :(
|
316
|
+
# Feel free to make this better. It has a test.
|
317
|
+
raw_key_values.split(/\n/).
|
318
|
+
map { |r| r.split(/=/, 2) }.
|
319
|
+
each { |k,v| hash[k.strip.downcase] = v.strip }
|
320
|
+
|
321
|
+
hash
|
322
|
+
end
|
323
|
+
|
324
|
+
def save_cookie_store(force=nil)
|
325
|
+
@http_client.save_cookie_store(force)
|
326
|
+
end
|
327
|
+
|
328
|
+
def http_get(url, params=nil, extra_headers={})
|
329
|
+
@http_client.http_get(url, params, extra_headers)
|
330
|
+
end
|
331
|
+
|
332
|
+
def http_post(url, params, extra_headers = {})
|
333
|
+
@http_client.http_post(url, params, extra_headers)
|
334
|
+
end
|
335
|
+
|
336
|
+
def tries
|
337
|
+
@tries ||= 1
|
338
|
+
|
339
|
+
(@tries += 1) - 1
|
340
|
+
end
|
341
|
+
|
342
|
+
class FakeLogger < Logger
|
343
|
+
def initialize
|
344
|
+
super("/dev/null")
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
class ErrorChecker
|
349
|
+
def self.check(response)
|
350
|
+
# some RETS servers returns HTTP code 412 when session cookie expired, yet the response body
|
351
|
+
# passes XML check. We need to special case for this situation.
|
352
|
+
# This method is also called from multipart.rb where there are headers and body but no status_code
|
353
|
+
if response.respond_to?(:status_code) && response.status_code == 412
|
354
|
+
raise HttpError, "HTTP status: #{response.status_code}, body: #{response.body}"
|
355
|
+
end
|
356
|
+
|
357
|
+
# some RETS servers return success code in XML body but failure code 4xx in http status
|
358
|
+
# If xml body is present we ignore http status
|
359
|
+
|
360
|
+
if !response.body.empty?
|
361
|
+
begin
|
362
|
+
xml = Nokogiri::XML.parse(response.body, nil, nil, Nokogiri::XML::ParseOptions::STRICT)
|
363
|
+
|
364
|
+
rets_element = xml.xpath("/RETS")
|
365
|
+
if rets_element.empty?
|
366
|
+
return
|
367
|
+
end
|
368
|
+
reply_text = (rets_element.attr("ReplyText") || rets_element.attr("replyText")).value
|
369
|
+
reply_code = (rets_element.attr("ReplyCode") || rets_element.attr("replyCode")).value.to_i
|
370
|
+
|
371
|
+
if reply_code.nonzero?
|
372
|
+
raise InvalidRequest.new(reply_code, reply_text)
|
373
|
+
else
|
374
|
+
return
|
375
|
+
end
|
376
|
+
rescue Nokogiri::XML::SyntaxError
|
377
|
+
#Not xml
|
378
|
+
end
|
379
|
+
end
|
380
|
+
|
381
|
+
if response.respond_to?(:ok?) && ! response.ok?
|
382
|
+
if response.status_code == 401
|
383
|
+
raise AuthorizationFailure.new(response.status_code, response.body)
|
384
|
+
else
|
385
|
+
raise HttpError, "HTTP status: #{response.status_code}, body: #{response.body}"
|
386
|
+
end
|
387
|
+
end
|
388
|
+
end
|
389
|
+
end
|
390
|
+
end
|
391
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Rets
|
2
|
+
class NullStatsReporter
|
3
|
+
def time(metric_name, &block)
|
4
|
+
block.call
|
5
|
+
end
|
6
|
+
|
7
|
+
def gauge(metric_name, measurement)
|
8
|
+
end
|
9
|
+
|
10
|
+
def count(metric_name, count=1)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class ClientProgressReporter
|
15
|
+
def initialize(logger, stats, stats_prefix)
|
16
|
+
@logger = logger
|
17
|
+
@stats = stats || NullStatsReporter.new
|
18
|
+
@stats_prefix = stats_prefix
|
19
|
+
end
|
20
|
+
|
21
|
+
def find_with_retries_failed_a_retry(exception, retries)
|
22
|
+
@stats.count("#{@stats_prefix}find_with_retries_failed_retry")
|
23
|
+
@logger.warn("Rets::Client: Failed with message: #{exception.message}")
|
24
|
+
@logger.info("Rets::Client: Retry #{retries}/3")
|
25
|
+
end
|
26
|
+
|
27
|
+
def find_with_retries_exceeded_retry_count(exception)
|
28
|
+
@stats.count("#{@stats_prefix}find_with_retries_exceeded_retry_count")
|
29
|
+
end
|
30
|
+
|
31
|
+
def could_not_resolve_find_metadata(key)
|
32
|
+
@stats.count("#{@stats_prefix}could_not_resolve_find_metadata")
|
33
|
+
@logger.warn "Rets::Client: Can't resolve find metadata for #{key.inspect}"
|
34
|
+
end
|
35
|
+
|
36
|
+
def use_cached_metadata
|
37
|
+
@logger.info "Rets::Client: Use cached metadata"
|
38
|
+
end
|
39
|
+
|
40
|
+
def bad_cached_metadata(cached_metadata)
|
41
|
+
@logger.info cached_metadata ? "Rets::Client: Cached metadata out of date" : "Rets::Client: Cached metadata unavailable"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module Rets
|
2
|
+
class HttpClient
|
3
|
+
attr_reader :http, :options, :logger, :login_url
|
4
|
+
|
5
|
+
def initialize(http, options, logger, login_url)
|
6
|
+
@http = http
|
7
|
+
@options = options
|
8
|
+
@logger = logger
|
9
|
+
@login_url = login_url
|
10
|
+
@options.fetch(:ca_certs, []).each {|c| @http.ssl_config.add_trust_ca(c) }
|
11
|
+
end
|
12
|
+
|
13
|
+
def http_get(url, params=nil, extra_headers={})
|
14
|
+
http.set_auth(url, options[:username], options[:password])
|
15
|
+
headers = extra_headers.merge(rets_extra_headers)
|
16
|
+
res = nil
|
17
|
+
log_http_traffic("POST", url, params, headers) do
|
18
|
+
res = http.get(url, params, headers)
|
19
|
+
end
|
20
|
+
Client::ErrorChecker.check(res)
|
21
|
+
res
|
22
|
+
end
|
23
|
+
|
24
|
+
def http_post(url, params, extra_headers = {})
|
25
|
+
http.set_auth(url, options[:username], options[:password])
|
26
|
+
headers = extra_headers.merge(rets_extra_headers)
|
27
|
+
res = nil
|
28
|
+
log_http_traffic("POST", url, params, headers) do
|
29
|
+
res = http.post(url, params, headers)
|
30
|
+
end
|
31
|
+
Client::ErrorChecker.check(res)
|
32
|
+
res
|
33
|
+
end
|
34
|
+
|
35
|
+
def log_http_traffic(method, url, params, headers, &block)
|
36
|
+
# optimization, we don't want to compute log params
|
37
|
+
# if logging is off
|
38
|
+
if logger.debug?
|
39
|
+
logger.debug "Rets::Client >> #{method} #{url}"
|
40
|
+
logger.debug "Rets::Client >> params = #{params.inspect}"
|
41
|
+
logger.debug "Rets::Client >> headers = #{headers.inspect}"
|
42
|
+
end
|
43
|
+
|
44
|
+
res = block.call
|
45
|
+
|
46
|
+
# optimization, we don't want to compute log params
|
47
|
+
# if logging is off, especially when there is a loop just
|
48
|
+
# for logging
|
49
|
+
if logger.debug?
|
50
|
+
logger.debug "Rets::Client << Status #{res.status_code}"
|
51
|
+
res.headers.each { |k, v| logger.debug "Rets::Client << #{k}: #{v}" }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def save_cookie_store(force=nil)
|
56
|
+
if options[:cookie_store]
|
57
|
+
if force
|
58
|
+
@http.cookie_manager.save_all_cookies(true, true, true)
|
59
|
+
else
|
60
|
+
@http.save_cookie_store
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def rets_extra_headers
|
66
|
+
user_agent = options[:agent] || "Client/1.0"
|
67
|
+
rets_version = options[:version] || "RETS/1.7.2"
|
68
|
+
|
69
|
+
headers = {
|
70
|
+
"User-Agent" => user_agent,
|
71
|
+
"RETS-Version" => rets_version
|
72
|
+
}
|
73
|
+
|
74
|
+
if options[:ua_password]
|
75
|
+
up = Digest::MD5.hexdigest "#{user_agent}:#{options[:ua_password]}"
|
76
|
+
session_id = http_cookie('RETS-Session-ID') || ''
|
77
|
+
digest = Digest::MD5.hexdigest "#{up}::#{session_id}:#{rets_version}"
|
78
|
+
headers.merge!("RETS-UA-Authorization" => "Digest #{digest}")
|
79
|
+
end
|
80
|
+
|
81
|
+
headers
|
82
|
+
end
|
83
|
+
|
84
|
+
def http_cookie(name)
|
85
|
+
http.cookies.each do |c|
|
86
|
+
return c.value if c.name.downcase == name.downcase && c.match?(URI.parse(login_url))
|
87
|
+
end
|
88
|
+
nil
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|