ruby-ajp 0.1.5

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