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.
@@ -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
+