arachni-typhoeus 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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>
|