arachni-typhoeus 0.2.0
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/.gitignore +3 -0
- data/CHANGELOG.markdown +43 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +30 -0
- data/README.textile +6 -0
- data/Rakefile +40 -0
- data/VERSION +1 -0
- data/benchmarks/profile.rb +25 -0
- data/benchmarks/vs_nethttp.rb +35 -0
- data/examples/twitter.rb +21 -0
- data/ext/typhoeus/.gitignore +7 -0
- data/ext/typhoeus/extconf.rb +65 -0
- data/ext/typhoeus/native.c +11 -0
- data/ext/typhoeus/native.h +21 -0
- data/ext/typhoeus/typhoeus_easy.c +220 -0
- data/ext/typhoeus/typhoeus_easy.h +19 -0
- data/ext/typhoeus/typhoeus_multi.c +211 -0
- data/ext/typhoeus/typhoeus_multi.h +16 -0
- data/lib/typhoeus.rb +58 -0
- data/lib/typhoeus/.gitignore +1 -0
- data/lib/typhoeus/easy.rb +366 -0
- data/lib/typhoeus/filter.rb +28 -0
- data/lib/typhoeus/hydra.rb +245 -0
- data/lib/typhoeus/hydra/callbacks.rb +24 -0
- data/lib/typhoeus/hydra/connect_options.rb +61 -0
- data/lib/typhoeus/hydra/stubbing.rb +52 -0
- data/lib/typhoeus/hydra_mock.rb +131 -0
- data/lib/typhoeus/multi.rb +37 -0
- data/lib/typhoeus/normalized_header_hash.rb +58 -0
- data/lib/typhoeus/remote.rb +306 -0
- data/lib/typhoeus/remote_method.rb +108 -0
- data/lib/typhoeus/remote_proxy_object.rb +50 -0
- data/lib/typhoeus/request.rb +210 -0
- data/lib/typhoeus/response.rb +91 -0
- data/lib/typhoeus/service.rb +20 -0
- data/lib/typhoeus/utils.rb +24 -0
- data/profilers/valgrind.rb +24 -0
- data/spec/fixtures/result_set.xml +60 -0
- data/spec/servers/app.rb +84 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +11 -0
- data/spec/typhoeus/easy_spec.rb +284 -0
- data/spec/typhoeus/filter_spec.rb +35 -0
- data/spec/typhoeus/hydra_mock_spec.rb +300 -0
- data/spec/typhoeus/hydra_spec.rb +526 -0
- data/spec/typhoeus/multi_spec.rb +74 -0
- data/spec/typhoeus/normalized_header_hash_spec.rb +41 -0
- data/spec/typhoeus/remote_method_spec.rb +141 -0
- data/spec/typhoeus/remote_proxy_object_spec.rb +65 -0
- data/spec/typhoeus/remote_spec.rb +695 -0
- data/spec/typhoeus/request_spec.rb +276 -0
- data/spec/typhoeus/response_spec.rb +151 -0
- data/spec/typhoeus/utils_spec.rb +22 -0
- data/typhoeus.gemspec +123 -0
- metadata +196 -0
@@ -0,0 +1,50 @@
|
|
1
|
+
module Typhoeus
|
2
|
+
class RemoteProxyObject
|
3
|
+
instance_methods.each { |m| undef_method m unless m =~ /^__|object_id/ }
|
4
|
+
|
5
|
+
def initialize(clear_memoized_store_proc, easy, options = {})
|
6
|
+
@clear_memoized_store_proc = clear_memoized_store_proc
|
7
|
+
@easy = easy
|
8
|
+
@success = options[:on_success]
|
9
|
+
@failure = options[:on_failure]
|
10
|
+
@cache = options.delete(:cache)
|
11
|
+
@cache_key = options.delete(:cache_key)
|
12
|
+
@timeout = options.delete(:cache_timeout)
|
13
|
+
Typhoeus.add_easy_request(@easy)
|
14
|
+
end
|
15
|
+
|
16
|
+
def method_missing(sym, *args, &block)
|
17
|
+
unless @proxied_object
|
18
|
+
if @cache && @cache_key
|
19
|
+
@proxied_object = @cache.get(@cache_key) rescue nil
|
20
|
+
end
|
21
|
+
|
22
|
+
unless @proxied_object
|
23
|
+
Typhoeus.perform_easy_requests
|
24
|
+
response = Response.new(:code => @easy.response_code,
|
25
|
+
:curl_return_code => @easy.curl_return_code,
|
26
|
+
:curl_error_message => @easy.curl_error_message,
|
27
|
+
:headers => @easy.response_header,
|
28
|
+
:body => @easy.response_body,
|
29
|
+
:time => @easy.total_time_taken,
|
30
|
+
:requested_url => @easy.url,
|
31
|
+
:requested_http_method => @easy.method,
|
32
|
+
:start_time => @easy.start_time)
|
33
|
+
if @easy.response_code >= 200 && @easy.response_code < 300
|
34
|
+
Typhoeus.release_easy_object(@easy)
|
35
|
+
@proxied_object = @success.nil? ? response : @success.call(response)
|
36
|
+
|
37
|
+
if @cache && @cache_key
|
38
|
+
@cache.set(@cache_key, @proxied_object, @timeout)
|
39
|
+
end
|
40
|
+
else
|
41
|
+
@proxied_object = @failure.nil? ? response : @failure.call(response)
|
42
|
+
end
|
43
|
+
@clear_memoized_store_proc.call
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
@proxied_object.__send__(sym, *args, &block)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,210 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
module Typhoeus
|
4
|
+
class Request
|
5
|
+
attr_reader :url
|
6
|
+
attr_writer :headers
|
7
|
+
attr_accessor :method, :params, :body, :connect_timeout, :timeout,
|
8
|
+
:user_agent, :response, :cache_timeout, :follow_location,
|
9
|
+
:max_redirects, :proxy, :proxy_username,:proxy_password,
|
10
|
+
:disable_ssl_peer_verification,
|
11
|
+
:ssl_cert, :ssl_cert_type, :ssl_key, :ssl_key_type,
|
12
|
+
:ssl_key_password, :ssl_cacert, :ssl_capath, :verbose,
|
13
|
+
:username, :password, :auth_method, :user_agent,
|
14
|
+
:proxy_auth_method, :proxy_type
|
15
|
+
|
16
|
+
# Initialize a new Request
|
17
|
+
#
|
18
|
+
# Options:
|
19
|
+
# * +url+ : Endpoint (URL) of the request
|
20
|
+
# * +options+ : A hash containing options among :
|
21
|
+
# ** +:method+ : :get (default) / :post / :put
|
22
|
+
# ** +:params+ : params as a Hash
|
23
|
+
# ** +:body+
|
24
|
+
# ** +:timeout+ : timeout (ms)
|
25
|
+
# ** +:connect_timeout+ : connect timeout (ms)
|
26
|
+
# ** +:headers+ : headers as Hash
|
27
|
+
# ** +:user_agent+ : user agent (string)
|
28
|
+
# ** +:cache_timeout+ : cache timeout (ms)
|
29
|
+
# ** +:follow_location
|
30
|
+
# ** +:max_redirects
|
31
|
+
# ** +:proxy
|
32
|
+
# ** +:disable_ssl_peer_verification
|
33
|
+
# ** +:ssl_cert
|
34
|
+
# ** +:ssl_cert_type
|
35
|
+
# ** +:ssl_key
|
36
|
+
# ** +:ssl_key_type
|
37
|
+
# ** +:ssl_key_password
|
38
|
+
# ** +:ssl_cacert
|
39
|
+
# ** +:ssl_capath
|
40
|
+
# ** +:verbose
|
41
|
+
# ** +:username
|
42
|
+
# ** +:password
|
43
|
+
# ** +:auth_method
|
44
|
+
#
|
45
|
+
def initialize(url, options = {})
|
46
|
+
@method = options[:method] || :get
|
47
|
+
@params = options[:params]
|
48
|
+
@body = options[:body]
|
49
|
+
@timeout = options[:timeout]
|
50
|
+
@connect_timeout = options[:connect_timeout]
|
51
|
+
@headers = options[:headers] || {}
|
52
|
+
@user_agent = options[:user_agent] || Typhoeus::USER_AGENT
|
53
|
+
@cache_timeout = options[:cache_timeout]
|
54
|
+
@follow_location = options[:follow_location]
|
55
|
+
@max_redirects = options[:max_redirects]
|
56
|
+
@proxy = options[:proxy]
|
57
|
+
@proxy_type = options[:proxy_type]
|
58
|
+
@proxy_username = options[:proxy_username]
|
59
|
+
@proxy_password = options[:proxy_password]
|
60
|
+
@proxy_auth_method = options[:proxy_auth_method]
|
61
|
+
@disable_ssl_peer_verification = options[:disable_ssl_peer_verification]
|
62
|
+
@ssl_cert = options[:ssl_cert]
|
63
|
+
@ssl_cert_type = options[:ssl_cert_type]
|
64
|
+
@ssl_key = options[:ssl_key]
|
65
|
+
@ssl_key_type = options[:ssl_key_type]
|
66
|
+
@ssl_key_password = options[:ssl_key_password]
|
67
|
+
@ssl_cacert = options[:ssl_cacert]
|
68
|
+
@ssl_capath = options[:ssl_capath]
|
69
|
+
@verbose = options[:verbose]
|
70
|
+
@username = options[:username]
|
71
|
+
@password = options[:password]
|
72
|
+
@auth_method = options[:auth_method]
|
73
|
+
|
74
|
+
if @method == :post
|
75
|
+
@url = url
|
76
|
+
else
|
77
|
+
@url = @params ? "#{url}?#{params_string}" : url
|
78
|
+
end
|
79
|
+
|
80
|
+
@parsed_uri = URI.parse(@url)
|
81
|
+
|
82
|
+
@on_complete = nil
|
83
|
+
@after_complete = nil
|
84
|
+
@handled_response = nil
|
85
|
+
end
|
86
|
+
|
87
|
+
LOCALHOST_ALIASES = %w[ localhost 127.0.0.1 0.0.0.0 ]
|
88
|
+
|
89
|
+
def localhost?
|
90
|
+
LOCALHOST_ALIASES.include?(@parsed_uri.host)
|
91
|
+
end
|
92
|
+
|
93
|
+
def host
|
94
|
+
slash_location = @url.index('/', 8)
|
95
|
+
if slash_location
|
96
|
+
@url.slice(0, slash_location)
|
97
|
+
else
|
98
|
+
query_string_location = @url.index('?')
|
99
|
+
return query_string_location ? @url.slice(0, query_string_location) : @url
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def host_domain
|
104
|
+
@parsed_uri.host
|
105
|
+
end
|
106
|
+
|
107
|
+
def headers
|
108
|
+
@headers["User-Agent"] = @user_agent
|
109
|
+
@headers
|
110
|
+
end
|
111
|
+
|
112
|
+
def params_string
|
113
|
+
params.keys.sort { |a, b| a.to_s <=> b.to_s }.collect do |k|
|
114
|
+
value = params[k]
|
115
|
+
if value.is_a? Hash
|
116
|
+
value.keys.collect {|sk| Typhoeus::Utils.escape("#{k}[#{sk}]") + "=" + Typhoeus::Utils.escape(value[sk].to_s)}
|
117
|
+
elsif value.is_a? Array
|
118
|
+
key = Typhoeus::Utils.escape(k.to_s)
|
119
|
+
value.collect { |v| "#{key}[]=#{Typhoeus::Utils.escape(v.to_s)}" }.join('&')
|
120
|
+
else
|
121
|
+
"#{Typhoeus::Utils.escape(k.to_s)}=#{Typhoeus::Utils.escape(params[k].to_s)}"
|
122
|
+
end
|
123
|
+
end.flatten.join("&")
|
124
|
+
end
|
125
|
+
|
126
|
+
def on_complete(&block)
|
127
|
+
@on_complete = block
|
128
|
+
end
|
129
|
+
|
130
|
+
def on_complete=(proc)
|
131
|
+
@on_complete = proc
|
132
|
+
end
|
133
|
+
|
134
|
+
def after_complete(&block)
|
135
|
+
@after_complete = block
|
136
|
+
end
|
137
|
+
|
138
|
+
def after_complete=(proc)
|
139
|
+
@after_complete = proc
|
140
|
+
end
|
141
|
+
|
142
|
+
def call_handlers
|
143
|
+
if @on_complete
|
144
|
+
@handled_response = @on_complete.call(response)
|
145
|
+
call_after_complete
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def call_after_complete
|
150
|
+
@after_complete.call(@handled_response) if @after_complete
|
151
|
+
end
|
152
|
+
|
153
|
+
def handled_response=(val)
|
154
|
+
@handled_response = val
|
155
|
+
end
|
156
|
+
|
157
|
+
def handled_response
|
158
|
+
@handled_response || response
|
159
|
+
end
|
160
|
+
|
161
|
+
def inspect
|
162
|
+
result = ":method => #{self.method.inspect},\n" <<
|
163
|
+
"\t:url => #{URI.parse(self.url).to_s}"
|
164
|
+
if self.body and !self.body.empty?
|
165
|
+
result << ",\n\t:body => #{self.body.inspect}"
|
166
|
+
end
|
167
|
+
|
168
|
+
if self.params and !self.params.empty?
|
169
|
+
result << ",\n\t:params => #{self.params.inspect}"
|
170
|
+
end
|
171
|
+
|
172
|
+
if self.headers and !self.headers.empty?
|
173
|
+
result << ",\n\t:headers => #{self.headers.inspect}"
|
174
|
+
end
|
175
|
+
|
176
|
+
result
|
177
|
+
end
|
178
|
+
|
179
|
+
def cache_key
|
180
|
+
Digest::SHA1.hexdigest(url)
|
181
|
+
end
|
182
|
+
|
183
|
+
def self.run(url, params)
|
184
|
+
r = new(url, params)
|
185
|
+
Typhoeus::Hydra.hydra.queue r
|
186
|
+
Typhoeus::Hydra.hydra.run
|
187
|
+
r.response
|
188
|
+
end
|
189
|
+
|
190
|
+
def self.get(url, params = {})
|
191
|
+
run(url, params.merge(:method => :get))
|
192
|
+
end
|
193
|
+
|
194
|
+
def self.post(url, params = {})
|
195
|
+
run(url, params.merge(:method => :post))
|
196
|
+
end
|
197
|
+
|
198
|
+
def self.put(url, params = {})
|
199
|
+
run(url, params.merge(:method => :put))
|
200
|
+
end
|
201
|
+
|
202
|
+
def self.delete(url, params = {})
|
203
|
+
run(url, params.merge(:method => :delete))
|
204
|
+
end
|
205
|
+
|
206
|
+
def self.head(url, params = {})
|
207
|
+
run(url, params.merge(:method => :head))
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module Typhoeus
|
2
|
+
class Response
|
3
|
+
attr_accessor :request, :mock
|
4
|
+
attr_reader :code, :headers, :body, :time,
|
5
|
+
:requested_url, :requested_remote_method,
|
6
|
+
:requested_http_method, :start_time,
|
7
|
+
:effective_url, :start_transfer_time,
|
8
|
+
:app_connect_time, :pretransfer_time,
|
9
|
+
:connect_time, :name_lookup_time,
|
10
|
+
:curl_return_code, :curl_error_message
|
11
|
+
|
12
|
+
attr_writer :headers_hash
|
13
|
+
|
14
|
+
def initialize(params = {})
|
15
|
+
@code = params[:code]
|
16
|
+
@curl_return_code = params[:curl_return_code]
|
17
|
+
@curl_error_message = params[:curl_error_message]
|
18
|
+
@status_message = params[:status_message]
|
19
|
+
@http_version = params[:http_version]
|
20
|
+
@headers = params[:headers] || ''
|
21
|
+
@body = params[:body]
|
22
|
+
@time = params[:time]
|
23
|
+
@requested_url = params[:requested_url]
|
24
|
+
@requested_http_method = params[:requested_http_method]
|
25
|
+
@start_time = params[:start_time]
|
26
|
+
@start_transfer_time = params[:start_transfer_time]
|
27
|
+
@app_connect_time = params[:app_connect_time]
|
28
|
+
@pretransfer_time = params[:pretransfer_time]
|
29
|
+
@connect_time = params[:connect_time]
|
30
|
+
@name_lookup_time = params[:name_lookup_time]
|
31
|
+
@request = params[:request]
|
32
|
+
@effective_url = params[:effective_url]
|
33
|
+
@mock = params[:mock] || false # default
|
34
|
+
@headers_hash = NormalizedHeaderHash.new(params[:headers_hash]) if params[:headers_hash]
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns true if this is a mock response.
|
38
|
+
def mock?
|
39
|
+
@mock
|
40
|
+
end
|
41
|
+
|
42
|
+
def headers_hash
|
43
|
+
@headers_hash ||= begin
|
44
|
+
headers.split("\n").map {|o| o.strip}.inject(Typhoeus::NormalizedHeaderHash.new) do |hash, o|
|
45
|
+
if o.empty? || o =~ /^HTTP\/[\d\.]+/
|
46
|
+
hash
|
47
|
+
else
|
48
|
+
i = o.index(":") || o.size
|
49
|
+
key = o.slice(0, i)
|
50
|
+
value = o.slice(i + 1, o.size)
|
51
|
+
value = value.strip unless value.nil?
|
52
|
+
if hash.has_key? key
|
53
|
+
hash[key] = [hash[key], value].flatten
|
54
|
+
else
|
55
|
+
hash[key] = value
|
56
|
+
end
|
57
|
+
|
58
|
+
hash
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def status_message
|
65
|
+
# http://rubular.com/r/eAr1oVYsVa
|
66
|
+
@status_message ||= first_header_line ? first_header_line[/\d{3} (.*)$/, 1].chomp : nil
|
67
|
+
end
|
68
|
+
|
69
|
+
def http_version
|
70
|
+
@http_version ||= first_header_line ? first_header_line[/HTTP\/(\S+)/, 1] : nil
|
71
|
+
end
|
72
|
+
|
73
|
+
def success?
|
74
|
+
@code >= 200 && @code < 300
|
75
|
+
end
|
76
|
+
|
77
|
+
def modified?
|
78
|
+
@code != 304
|
79
|
+
end
|
80
|
+
|
81
|
+
def timed_out?
|
82
|
+
curl_return_code == 28
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def first_header_line
|
88
|
+
@first_header_line ||= headers.split("\n").first
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Typhoeus
|
2
|
+
class Service
|
3
|
+
def initialize(host, port)
|
4
|
+
@host = host
|
5
|
+
@port = port
|
6
|
+
end
|
7
|
+
|
8
|
+
def get(resource, params)
|
9
|
+
end
|
10
|
+
|
11
|
+
def put(resource, params)
|
12
|
+
end
|
13
|
+
|
14
|
+
def post(resource, params)
|
15
|
+
end
|
16
|
+
|
17
|
+
def delete(resource, params)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Typhoeus
|
2
|
+
module Utils
|
3
|
+
# Taken from Rack::Utils, 1.2.1 to remove Rack dependency.
|
4
|
+
def escape(s)
|
5
|
+
s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/u) {
|
6
|
+
'%'+$1.unpack('H2'*bytesize($1)).join('%').upcase
|
7
|
+
}.tr(' ', '+')
|
8
|
+
end
|
9
|
+
module_function :escape
|
10
|
+
|
11
|
+
# Return the bytesize of String; uses String#size under Ruby 1.8 and
|
12
|
+
# String#bytesize under 1.9.
|
13
|
+
if ''.respond_to?(:bytesize)
|
14
|
+
def bytesize(string)
|
15
|
+
string.bytesize
|
16
|
+
end
|
17
|
+
else
|
18
|
+
def bytesize(string)
|
19
|
+
string.size
|
20
|
+
end
|
21
|
+
end
|
22
|
+
module_function :bytesize
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# go to ext/typhoeus and run ruby extconf.rb && make before running
|
3
|
+
# this.
|
4
|
+
|
5
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__) + "/../ext")
|
6
|
+
require File.dirname(__FILE__) + "/../lib/typhoeus"
|
7
|
+
|
8
|
+
klass = Class.new { include Typhoeus }
|
9
|
+
|
10
|
+
loops = ENV['LOOPS'].to_i
|
11
|
+
url = ARGV.first || (raise "requires URL!")
|
12
|
+
|
13
|
+
loops.times do |i|
|
14
|
+
puts "On loop #{i}" if i % 10 == 0
|
15
|
+
results = []
|
16
|
+
5.times do
|
17
|
+
results << klass.get(url)
|
18
|
+
end
|
19
|
+
|
20
|
+
# fire requests
|
21
|
+
results[0].code
|
22
|
+
end
|
23
|
+
|
24
|
+
puts "Ran #{loops} loops on #{url}!"
|
@@ -0,0 +1,60 @@
|
|
1
|
+
<result_set>
|
2
|
+
<ttl>20</ttl>
|
3
|
+
<result>
|
4
|
+
<id>1</id>
|
5
|
+
<name>hello</name>
|
6
|
+
<description>
|
7
|
+
this is a long description for a text field of some kind.
|
8
|
+
this is a long description for a text field of some kind.
|
9
|
+
this is a long description for a text field of some kind.
|
10
|
+
this is a long description for a text field of some kind.
|
11
|
+
this is a long description for a text field of some kind.
|
12
|
+
this is a long description for a text field of some kind.
|
13
|
+
this is a long description for a text field of some kind.
|
14
|
+
this is a long description for a text field of some kind.
|
15
|
+
this is a long description for a text field of some kind.
|
16
|
+
this is a long description for a text field of some kind.
|
17
|
+
this is a long description for a text field of some kind.
|
18
|
+
this is a long description for a text field of some kind.
|
19
|
+
this is a long description for a text field of some kind.
|
20
|
+
</description>
|
21
|
+
</result>
|
22
|
+
<result>
|
23
|
+
<id>2</id>
|
24
|
+
<name>hello</name>
|
25
|
+
<description>
|
26
|
+
this is a long description for a text field of some kind.
|
27
|
+
this is a long description for a text field of some kind.
|
28
|
+
this is a long description for a text field of some kind.
|
29
|
+
this is a long description for a text field of some kind.
|
30
|
+
this is a long description for a text field of some kind.
|
31
|
+
this is a long description for a text field of some kind.
|
32
|
+
this is a long description for a text field of some kind.
|
33
|
+
this is a long description for a text field of some kind.
|
34
|
+
this is a long description for a text field of some kind.
|
35
|
+
this is a long description for a text field of some kind.
|
36
|
+
this is a long description for a text field of some kind.
|
37
|
+
this is a long description for a text field of some kind.
|
38
|
+
this is a long description for a text field of some kind.
|
39
|
+
</description>
|
40
|
+
</result>
|
41
|
+
<result>
|
42
|
+
<id>3</id>
|
43
|
+
<name>hello</name>
|
44
|
+
<description>
|
45
|
+
this is a long description for a text field of some kind.
|
46
|
+
this is a long description for a text field of some kind.
|
47
|
+
this is a long description for a text field of some kind.
|
48
|
+
this is a long description for a text field of some kind.
|
49
|
+
this is a long description for a text field of some kind.
|
50
|
+
this is a long description for a text field of some kind.
|
51
|
+
this is a long description for a text field of some kind.
|
52
|
+
this is a long description for a text field of some kind.
|
53
|
+
this is a long description for a text field of some kind.
|
54
|
+
this is a long description for a text field of some kind.
|
55
|
+
this is a long description for a text field of some kind.
|
56
|
+
this is a long description for a text field of some kind.
|
57
|
+
this is a long description for a text field of some kind.
|
58
|
+
</description>
|
59
|
+
</result>
|
60
|
+
</result_set>
|