ruby-ajp 0.1.5
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/COPYING +504 -0
- data/Install.en +231 -0
- data/Install.ja +250 -0
- data/NEWS.en +13 -0
- data/NEWS.ja +12 -0
- data/README.en +56 -0
- data/README.ja +54 -0
- data/Rakefile +24 -0
- data/example/dump-server.rb +68 -0
- data/example/error-server.rb +13 -0
- data/example/hello-server.rb +18 -0
- data/lib/net/ajp13.rb +815 -0
- data/lib/net/ajp13client.rb +361 -0
- data/lib/net/ajp13server.rb +430 -0
- data/ruby-ajp.gemspec +27 -0
- data/setup.rb +1585 -0
- data/test/net/data/ajp13request-data.1 +0 -0
- data/test/net/data/ajp13response-header.1 +0 -0
- data/test/net/data/ajp13response-webdav.1 +0 -0
- data/test/net/data/ajp13response-webdav.2 +0 -0
- data/test/net/data/rand +0 -0
- data/test/net/test_ajp13client.rb +353 -0
- data/test/net/test_ajp13packet.rb +320 -0
- data/test/net/test_ajp13request.rb +177 -0
- data/test/net/test_ajp13response.rb +162 -0
- data/test/net/test_ajp13server.rb +548 -0
- metadata +73 -0
@@ -0,0 +1,361 @@
|
|
1
|
+
# = Ruby/AJP client
|
2
|
+
#
|
3
|
+
# Client-side library for AJP1.3
|
4
|
+
#
|
5
|
+
# ==Example
|
6
|
+
#
|
7
|
+
# === Simple GET
|
8
|
+
# require 'net/ajp13client'
|
9
|
+
# Net::AJP13::Client.start('localhost') do |client|
|
10
|
+
# puts client.get('/index.jsp').body
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# === More about GET
|
14
|
+
# require 'net/ajp13client'
|
15
|
+
# req = Net::AJP13::GetRequest.new('/index.jsp')
|
16
|
+
# req.server_port = 80
|
17
|
+
# req['Host'] = 'www.example.com'
|
18
|
+
# res = Net::AJP13::Client.start('localhost') do |client|
|
19
|
+
# client.request(req) do |frag|
|
20
|
+
# $stderr.puts "got a fragment of the content body."
|
21
|
+
# puts frag
|
22
|
+
# end
|
23
|
+
# end
|
24
|
+
# $stderr.puts "Response body was #{res.content_length} bytes"
|
25
|
+
# $stderr.puts "Headers:"
|
26
|
+
# res.each do |name, value|
|
27
|
+
# $stderr.puts "#{name}: #{value}"
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# == Copyright
|
31
|
+
# Author:: Yugui (mailto:yugui@yugui.sakura.ne.jp)
|
32
|
+
# Copyright:: *
|
33
|
+
# * Copyright (c) 2005-2006 Yugui
|
34
|
+
# * Copyright (c) 1999-2005 Yukihiro Matsumoto
|
35
|
+
# * Copyright (c) 1999-2005 Minero Aoki
|
36
|
+
# * Copyright (c) 2001 GOTOU Yuuzou
|
37
|
+
# License:: LGPL
|
38
|
+
#
|
39
|
+
# This library is free software; you can redistribute it and/or
|
40
|
+
# modify it under the terms of the GNU Lesser General Public
|
41
|
+
# License as published by the Free Software Foundation; version 2.1
|
42
|
+
# of the License any later version.
|
43
|
+
#
|
44
|
+
# This library is distributed in the hope that it will be useful,
|
45
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
46
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
47
|
+
# Lesser General Public License for more details.
|
48
|
+
#
|
49
|
+
# You should have received a copy of the GNU Lesser General Public
|
50
|
+
# License along with this library; if not, write to the Free Software
|
51
|
+
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston,
|
52
|
+
# MA 02110-1301 USA
|
53
|
+
#
|
54
|
+
# Net::AJP13::Client was implemented by refering net/http, rev. 1.128.
|
55
|
+
# Especially, its RDoc documentation is derived from Net::HTTP's one, which was
|
56
|
+
# written by Minero Aoki and converted to RDoc by William Webber.
|
57
|
+
#
|
58
|
+
# The original net/http is available from http://www.ruby-lang.org/cgi-bin/cvsweb.cgi/ruby/lib/net/http.rb?rev=1.128
|
59
|
+
#
|
60
|
+
|
61
|
+
require 'socket'
|
62
|
+
require 'timeout'
|
63
|
+
require 'net/ajp13'
|
64
|
+
require 'stringio'
|
65
|
+
|
66
|
+
# :stopdoc:
|
67
|
+
module Net; end
|
68
|
+
module Net::AJP13; end
|
69
|
+
# :startdoc:
|
70
|
+
|
71
|
+
|
72
|
+
#
|
73
|
+
# AJP1.3 client
|
74
|
+
#
|
75
|
+
class Net::AJP13::Client
|
76
|
+
include Net::AJP13::Constants
|
77
|
+
|
78
|
+
# Creates a new Net::AJP13::Client object for the specified +address+.
|
79
|
+
# +address+:: The FQDN of the servlet container to connect toe.
|
80
|
+
# +port+:: The port number to connect to.
|
81
|
+
# This method does not open the TCP connection.
|
82
|
+
def initialize(address, port = DEFAULT_PORT)
|
83
|
+
@address = address
|
84
|
+
@port = port || DEFAULT_PORT
|
85
|
+
@state = nil
|
86
|
+
|
87
|
+
@open_timeout = 30
|
88
|
+
@read_timeout = 60
|
89
|
+
@write_timeout = 60
|
90
|
+
end
|
91
|
+
|
92
|
+
def inspect #:nodoc:
|
93
|
+
"#<#{self.class} #{@address}:#{@port} state=#{@state}>"
|
94
|
+
end
|
95
|
+
|
96
|
+
# The host name to connect to.
|
97
|
+
attr_reader :address
|
98
|
+
# The port number to connect to.
|
99
|
+
attr_reader :port
|
100
|
+
|
101
|
+
# State of the TCP/AJP connection. The value is
|
102
|
+
# +nil+ ::
|
103
|
+
# If neither TCP connection nor AJP session is opened.
|
104
|
+
# :idle ::
|
105
|
+
# If a TCP connection is opened but no request is
|
106
|
+
# being handled over the connection.
|
107
|
+
# :assigned ::
|
108
|
+
# If the TCP connection is opened and handling a specific request.
|
109
|
+
attr_reader :state
|
110
|
+
|
111
|
+
# returns true if the TCP connection is opened.
|
112
|
+
def started?; !!@state end
|
113
|
+
# alias of #started?
|
114
|
+
alias :active? :started?
|
115
|
+
|
116
|
+
# returns true if the TCP connection is handling a specific request.
|
117
|
+
def assigned?; @state == :assigned end
|
118
|
+
|
119
|
+
# returns true if the TCP connection is opened, but no request is being
|
120
|
+
# handled over the connection.
|
121
|
+
def idle?; @state == :idle end
|
122
|
+
|
123
|
+
|
124
|
+
# Seconds to wait until connection is opened.
|
125
|
+
# If the Client object cannot open a connection in this many seconds,
|
126
|
+
# it raises a TimeoutError exception.
|
127
|
+
attr_accessor :open_timeout
|
128
|
+
|
129
|
+
# Seconds to wait until reading one block (by one read(2) call).
|
130
|
+
# It raises a TimeoutError exception when timeout
|
131
|
+
attr_accessor :read_timeout
|
132
|
+
|
133
|
+
# Seconds to wait until writing one block (by one write(2) call).
|
134
|
+
# It raises a TimeoutError exception when timeout
|
135
|
+
attr_accessor :write_timeout
|
136
|
+
|
137
|
+
# Opens TCP connection and AJP session.
|
138
|
+
# Raises IOError if already started.
|
139
|
+
#
|
140
|
+
# When this method is called with block, gives a HTTP object to the block
|
141
|
+
# and closes the TCP connection after the block executed.
|
142
|
+
#
|
143
|
+
# When called with a block, returns the return value of the block;
|
144
|
+
# otherwise, returns self.
|
145
|
+
def start #:yields: self if block given
|
146
|
+
raise IOError, 'AJP session already opended' if @state
|
147
|
+
Timeout.timeout(@open_timeout) {
|
148
|
+
@socket = TCPSocket.new(address, port)
|
149
|
+
}
|
150
|
+
@state = :idle
|
151
|
+
if block_given?
|
152
|
+
begin
|
153
|
+
return yield(self)
|
154
|
+
ensure
|
155
|
+
finish if started?
|
156
|
+
end
|
157
|
+
else
|
158
|
+
return self
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# Creates a new Net::AJP13::Client object for the specified +address+,
|
163
|
+
# and opens its TCP connection and AJP session.
|
164
|
+
#
|
165
|
+
# If the optional block is given, the newly created Net::AJP13::Client object
|
166
|
+
# is passed to it and closed when the block finishes.
|
167
|
+
#
|
168
|
+
# When called with a block, returns the return value of the block;
|
169
|
+
# otherwise, returns the newly created Net::AJP13::Client object.
|
170
|
+
#
|
171
|
+
# +address+:: The FQDN of the servlet container to connect toe.
|
172
|
+
# +port+:: The port number to connect to.
|
173
|
+
def self.start(address, port = DEFAULT_PORT, &block) # :yield: +ajp13+
|
174
|
+
self.new(address, port).start(&block)
|
175
|
+
end
|
176
|
+
|
177
|
+
# Closes TCP connection.
|
178
|
+
# Raises AJPStateError if not started.
|
179
|
+
def finish
|
180
|
+
raise Net::AJP13::AJPStateError, 'AJP session not yet started' unless @state
|
181
|
+
@socket.close unless @socket.closed?
|
182
|
+
@state = @socket = nil
|
183
|
+
end
|
184
|
+
|
185
|
+
# Ensures that +self+ is taking :idle state.
|
186
|
+
def ensure_idling
|
187
|
+
raise Net::AJP13::AJPStateError,
|
188
|
+
'AJP session not yet started' unless started?
|
189
|
+
raise Net::AJP13::AJPStateError,
|
190
|
+
"AJP session has already been in `assigned\'" unless idle?
|
191
|
+
end
|
192
|
+
private :ensure_idling
|
193
|
+
|
194
|
+
# Asks the application server to shut itself down.
|
195
|
+
def ask_to_shutdown
|
196
|
+
ensure_idling
|
197
|
+
|
198
|
+
packet = Net::AJP13::Packet.new
|
199
|
+
packet.direction = :to_app
|
200
|
+
packet.append_byte SHUTDOWN
|
201
|
+
|
202
|
+
packet.send_to(@socket)
|
203
|
+
end
|
204
|
+
|
205
|
+
|
206
|
+
# Sends ping message to the application server.
|
207
|
+
# Raises
|
208
|
+
# [IOError] if the TCP socket raises it.
|
209
|
+
# [TimeOutError]
|
210
|
+
def ping
|
211
|
+
ensure_idling
|
212
|
+
|
213
|
+
packet = Net::AJP13::Packet.new
|
214
|
+
packet.direction = :to_app
|
215
|
+
packet.append_byte CPING
|
216
|
+
|
217
|
+
packet.send_to(@socket)
|
218
|
+
|
219
|
+
packet = Net::AJP13::Packet.from_io(@socket)
|
220
|
+
case packet.message_type
|
221
|
+
when CPONG_REPLY
|
222
|
+
return true
|
223
|
+
when SEND_BODY_CHUNK, SEND_HEADERS, END_RESPONSE, GET_BODY_CHUNK
|
224
|
+
raise Net::AJP13::AJPStateError,
|
225
|
+
"Unexpected packet type #{packet.message_type}"
|
226
|
+
else
|
227
|
+
raise Net::AJP13::AJPPacketError,
|
228
|
+
"Unrecognized packet type #{packet.message_type}"
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
|
233
|
+
# read a bytes from +io+, and returns BODY_CHUNK packet to send the bytes.
|
234
|
+
def body_chunk_packet(io, max_len)
|
235
|
+
chunk = io.eof? ? '' : io.readpartial(max_len)
|
236
|
+
packet = Net::AJP13::Packet.new
|
237
|
+
packet.direction = :to_app
|
238
|
+
packet.append_integer chunk.length
|
239
|
+
packet.append_bytes chunk
|
240
|
+
packet
|
241
|
+
end
|
242
|
+
|
243
|
+
|
244
|
+
# Sends +req+ to the connected application server.
|
245
|
+
#
|
246
|
+
# Returns a Net::AJP13::Reponse object, which represents the received
|
247
|
+
# response.
|
248
|
+
# Raises AJPStateError unless the session state is :idle.
|
249
|
+
#
|
250
|
+
# If called with a block, yields each fragment of the
|
251
|
+
# entity body in turn as a string as it is read from
|
252
|
+
# the socket. Note that in this case, the returned response
|
253
|
+
# object may not contain a (meaningful) body.
|
254
|
+
#
|
255
|
+
# If the application server says that the connection is not `reusable',
|
256
|
+
# this method calls #finish after receiving the response.
|
257
|
+
def request(req) #:yields: +response_body_fragment+
|
258
|
+
ensure_idling
|
259
|
+
@state = :assigned
|
260
|
+
|
261
|
+
begin
|
262
|
+
req.protocol ||= 'HTTP/1.0'
|
263
|
+
req['host'] ||= address
|
264
|
+
req.server_port ||= 80
|
265
|
+
|
266
|
+
if req.body
|
267
|
+
req['content-length'] ||= req.body.length.to_s
|
268
|
+
req['content-type'] ||= 'application/x-www-from-urlencoded'
|
269
|
+
body_stream = StringIO.new(req.body)
|
270
|
+
elsif req.body_stream
|
271
|
+
if req['content-length']
|
272
|
+
body_stream = req.body_stream
|
273
|
+
else
|
274
|
+
if req.body_stream.respond_to?(:length)
|
275
|
+
req['content-length'] = req.body_stream.length.to_s
|
276
|
+
body_stream = req.body_stream
|
277
|
+
else
|
278
|
+
body_stream = StringIO.new(req.body_stream.read)
|
279
|
+
req['content-length'] = body_stream.length.to_s
|
280
|
+
end
|
281
|
+
end
|
282
|
+
req['content-type'] ||= 'application/x-www-from-urlencoded'
|
283
|
+
end
|
284
|
+
|
285
|
+
req.send_to @socket
|
286
|
+
packet = nil
|
287
|
+
if body_stream
|
288
|
+
# Mainly, for StringIO
|
289
|
+
unless body_stream.respond_to?(:readpartial)
|
290
|
+
class << body_stream
|
291
|
+
alias_method :readpartial, :read
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
chunk =
|
296
|
+
body_chunk_packet(body_stream, MAX_BODY_CHUNK_SIZE)
|
297
|
+
chunk.send_to @socket
|
298
|
+
|
299
|
+
loop do
|
300
|
+
packet = Net::AJP13::Packet.from_io(@socket)
|
301
|
+
case packet.message_type
|
302
|
+
when GET_BODY_CHUNK
|
303
|
+
required = packet.read_integer
|
304
|
+
chunk = body_chunk_packet(body_stream,
|
305
|
+
[required, MAX_BODY_CHUNK_SIZE].min)
|
306
|
+
chunk.send_to @socket
|
307
|
+
next
|
308
|
+
when SEND_HEADERS
|
309
|
+
break
|
310
|
+
when SEND_BODY_CHUNK, END_RESPONSE
|
311
|
+
raise Net::AJP13::AJPStateError, 'Unexpected state'
|
312
|
+
else
|
313
|
+
raise Net::AJP13::AJPPacketError,
|
314
|
+
"Unrecognized packet type : #{packet.message_type}"
|
315
|
+
end
|
316
|
+
end
|
317
|
+
else
|
318
|
+
packet = Net::AJP13::Packet.from_io(@socket)
|
319
|
+
end
|
320
|
+
|
321
|
+
res = Net::AJP13::Response.from_packet(packet)
|
322
|
+
loop do
|
323
|
+
packet = Net::AJP13::Packet.from_io(@socket)
|
324
|
+
case packet_type = packet.read_byte
|
325
|
+
when GET_BODY_CHUNK, SEND_HEADERS
|
326
|
+
raise AJPError, 'Unexpected state'
|
327
|
+
when SEND_BODY_CHUNK
|
328
|
+
len = packet.read_integer
|
329
|
+
body = packet.read_bytes(len)
|
330
|
+
terminator = packet.read_byte # TODO: This terminator is undocumented.
|
331
|
+
# raise AJPError, 'Block packet' unless packet.eof?
|
332
|
+
if block_given?
|
333
|
+
yield body
|
334
|
+
else
|
335
|
+
res.body ||= ''
|
336
|
+
res.body << body
|
337
|
+
end
|
338
|
+
next
|
339
|
+
when END_RESPONSE
|
340
|
+
is_reusable = packet.read_boolean
|
341
|
+
finish unless is_reusable
|
342
|
+
break
|
343
|
+
else
|
344
|
+
raise Net::AJP13::AJPPacketError,
|
345
|
+
"Unrecoginized packet type #{packet_type}"
|
346
|
+
end
|
347
|
+
end
|
348
|
+
ensure
|
349
|
+
@state = :idle
|
350
|
+
end
|
351
|
+
|
352
|
+
return res
|
353
|
+
end
|
354
|
+
|
355
|
+
# Equals #request(GetRequest.new(+path+, +header+)) in this version.
|
356
|
+
def get(path, header = nil, &block)
|
357
|
+
request(Net::AJP13::GetRequest.new(path, header), &block)
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
|