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,430 @@
1
+ # == Ruby/AJP server
2
+ # === Examples
3
+ # See example/ directory
4
+ #
5
+ # === Copyright
6
+ # Author:: Yugui (mailto:yugui@yugui.sakura.ne.jp)
7
+ # Copyright:: Copyright (c) 2006 Yugui
8
+ # License:: LGPL
9
+ #
10
+ # This library is free software; you can redistribute it and/or
11
+ # modify it under the terms of the GNU Lesser General Public
12
+ # License as published by the Free Software Foundation; version 2.1
13
+ # of the License any later version.
14
+ #
15
+ # This library is distributed in the hope that it will be useful,
16
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
17
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
18
+ # Lesser General Public License for more details.
19
+ #
20
+ # You should have received a copy of the GNU Lesser General Public
21
+ # License along with this library; if not, write to the Free Software
22
+ # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston,
23
+ # MA 02110-1301 USA
24
+ #
25
+
26
+ require 'socket'
27
+ require 'ipaddr'
28
+ require 'mutex_m'
29
+ require 'timeout'
30
+ require 'stringio'
31
+ require 'net/ajp13'
32
+
33
+ # :stopdoc:
34
+ module Net; end
35
+ module Net::AJP13; end
36
+ # :startdoc:
37
+
38
+ # Provides a skeleton to implement an AJP 1.3 server.
39
+ class Net::AJP13::Server
40
+ include Net::AJP13::Constants
41
+ include Mutex_m
42
+
43
+ #
44
+ # +host+:: Host to bind. If ommited or +nil+, the server will accept requests
45
+ # from any hosts.
46
+ # +serivce+:: Port number to bind. It can be a service name registered in
47
+ # /etc/services (or NIS).
48
+ def initialize(*args) #:args: [host,] service = DEFAULT_PORT
49
+ @host = nil
50
+ @service = DEFAULT_PORT
51
+
52
+ case args.length
53
+ when 2
54
+ @host = args[0] if args[0]
55
+ @service = args[1] if args[1]
56
+ @open_socket = lambda{ TCPServer.new(@host, @service) }
57
+ when 1
58
+ @service = args[0] if args[0]
59
+ @open_socket = lambda{ TCPServer.new(@service) }
60
+ when 0
61
+ @open_socket = lambda{ TCPServer.new(@service) }
62
+ else
63
+ raise ArgumentError, "wrong number of arguments (#{args.length} for 0..2)"
64
+ end
65
+ end
66
+
67
+ # The instance accepts only requests from #host.
68
+ # If host is +nil+, it accepts requests from any hosts.
69
+ # See Also:: TCPServer.new, bind(2)
70
+ attr_reader :host
71
+
72
+ # The port number to bind.
73
+ attr_reader :service
74
+
75
+ # logger
76
+ attr_accessor :logger
77
+ def logger
78
+ unless @logger
79
+ require 'logger'
80
+ @logger = Logger.new(STDOUT)
81
+ end
82
+ @logger
83
+ end
84
+
85
+ def start(sock = nil)
86
+ logger.info("Starting #{self.class}")
87
+ if sock
88
+ @sock = sock
89
+ else
90
+ @sock = @open_socket.call
91
+ end
92
+
93
+ @sock.listen(5)
94
+ begin
95
+ until @shutdown
96
+ accepted = @sock.accept
97
+ Thread.new {
98
+ begin
99
+ until accepted.closed?
100
+ process(accepted)
101
+ end
102
+ rescue StandardError => err
103
+ logger.error("#{err.message} from #{err.backtrace("\n")}")
104
+ rescue Object => err
105
+ logger.fatal("#{err.message} from #{err.backtrace("\n")}")
106
+ else
107
+ logger.debug("closed")
108
+ ensure
109
+ accepted.close unless accepted.closed?
110
+ end
111
+ }
112
+ end
113
+ end
114
+
115
+ logger.info("Exited normally.")
116
+ rescue Interrupt
117
+ logger.info("Exited by Interrupt.")
118
+ ensure
119
+ @sock.close if @sock and !@sock.closed?
120
+ end
121
+
122
+
123
+ # You must override this method. The default implementation simply raises
124
+ # a ProcessRequestNotImplementedError.
125
+ # +request+:: The Net::AJP13::Request object that represents an accepted
126
+ # AJP request.
127
+ # The return value must be a Net::AJP13::Response object.
128
+ def process_request(request)
129
+ raise ProcessRequestNotImplementedError, "Must be overridden."
130
+ end
131
+
132
+ private
133
+
134
+ # +conn+:: Accepted connection. +conn+ is an IO object or something like it.
135
+ def process(conn)
136
+ packet = Net::AJP13::Packet.from_io(conn)
137
+ case packet.message_type
138
+ when FORWARD_REQUEST
139
+ process_forward_request(packet, conn)
140
+ when SHUTDOWN
141
+ process_shutdown(packet, conn)
142
+ when PING
143
+ process_ping(packet, conn)
144
+ when CPING
145
+ process_cping(packet, conn)
146
+ else
147
+ raise AJPPacketError, "Unrecognized packet type #{packet.message_type}"
148
+ end
149
+ end
150
+
151
+ def process_forward_request(packet, conn)
152
+ req = Net::AJP13::Request.from_packet(packet)
153
+ if req['content-length'] and req.content_length > 0
154
+ req.body_stream = BodyInput.new(conn, req.content_length)
155
+ end
156
+
157
+ user_code_error = nil
158
+ begin
159
+ res = process_request(req)
160
+ rescue ProcessRequestNotImplementedError
161
+ raise
162
+ rescue Object => err
163
+ user_code_error = err
164
+ end
165
+
166
+ if user_code_error
167
+ # sends backtrace
168
+ message = user_code_error.message + ": " +
169
+ user_code_error.backtrace.join("\n")
170
+ logger.error(message)
171
+
172
+ message = message[0, MAX_PACKET_SIZE - 4] if
173
+ message.length > MAX_PACKET_SIZE - 4
174
+ res = Net::AJP13::Response.new(500)
175
+ res['content-type'] = 'text/plain'
176
+ res['content-length'] = message.length.to_s
177
+ res.send_to conn
178
+ conn.write "\x41\x42#{[message.length + 4].pack('n')}\x03#{[message.length].pack('n')}#{message}\x00"
179
+ else
180
+ # SEND_HEADERS packet
181
+ res['content-length'] ||= res.body.length.to_s if res.body
182
+ res.send_to conn
183
+
184
+ # SEND_BODY_CHUNK packets
185
+ if res.body
186
+ stream = StringIO.new(res.body)
187
+ until stream.eof?
188
+ chunk = stream.read(MAX_PACKET_SIZE - 4)
189
+ packet = Net::AJP13::Packet.new
190
+ packet.direction = :from_app
191
+ packet.append_byte SEND_BODY_CHUNK
192
+
193
+ # differ from ajpv13a.html, but Tomcat5 acts like this.
194
+ packet.append_string chunk
195
+
196
+ packet.send_to conn
197
+ end
198
+ end
199
+ end
200
+
201
+ # END_RESPONSE packet
202
+ packet = Net::AJP13::Packet.new
203
+ packet.direction = :from_app
204
+ packet.append_byte END_RESPONSE
205
+ packet.append_boolean !user_code_error
206
+ packet.send_to conn
207
+
208
+ conn.close if user_code_error
209
+ end
210
+
211
+ def process_cping(packet, conn)
212
+ packet = Net::AJP13::Packet.new
213
+ packet.direction = :from_app
214
+ packet.append_byte CPONG_REPLY
215
+ packet.send_to conn
216
+ end
217
+
218
+ def process_shutdown(packet, conn)
219
+ if IPAddr.new(conn.addr[3]) == IPAddr.new(conn.peeraddr[3])
220
+ shutdown
221
+ end
222
+ end
223
+
224
+ def shutdown(force = false)
225
+ @shutdown = true
226
+ @sock.close if force
227
+ end
228
+
229
+ class ProcessRequestNotImplementedError < NotImplementedError
230
+ end
231
+
232
+ # Input stream that corresponds the request body from the web server.
233
+ # BodyInput object acts as an IO except writing methods.
234
+ class BodyInput
235
+ include Net::AJP13::Constants
236
+ include Enumerable
237
+
238
+ # +sock+:: socket connection to the web server
239
+ # +length+:: Content-Length
240
+ def initialize(sock, length)
241
+ @sock = sock
242
+ @packet = Net::AJP13::Packet.from_io(sock)
243
+ @length = length
244
+ @read_length = 0
245
+ end
246
+
247
+ # Content-Length
248
+ attr_reader :length
249
+ alias :size :length
250
+
251
+ # Does nothing
252
+ def binmode; self end
253
+
254
+ # Raises TypeError. You can't clone BodyInput.
255
+ def clone
256
+ raise TypeError, "can't clone #{self.class}"
257
+ end
258
+ alias :dup :clone
259
+
260
+ # Closes the BodyInput.
261
+ # Note that this method does not close the internal socket connection.
262
+ def close
263
+ @packet = @sock = nil
264
+ end
265
+
266
+ # Returns true if the BodyInput is closed.
267
+ def closed?
268
+ @sock.nil?
269
+ end
270
+ alias :close_read :close
271
+
272
+ # Same as IO#each_byte
273
+ def each_byte
274
+ while ch = getc
275
+ yield ch
276
+ end
277
+ end
278
+
279
+ def eof?
280
+ @read_length >= @length
281
+ end
282
+ alias :eof :eof?
283
+
284
+ # Raises NotImplementedError
285
+ def fcntl(*args)
286
+ raise NotImplementedError, "#{self} does not support fcntl"
287
+ end
288
+ # Raises NotImplementedError
289
+ def iocntl(*args)
290
+ raise NotImplementedError, "#{self} does not support iocntl"
291
+ end
292
+
293
+ # Always returns nil
294
+ def fileno; nil end
295
+ alias :to_i :fileno
296
+
297
+ def getc
298
+ str = read(1)
299
+ str and str[0]
300
+ end
301
+
302
+ # Returns false
303
+ def isatty; false end
304
+ alias :tty? :isatty
305
+
306
+ def lineno; @lineno end
307
+
308
+ # Returns nil
309
+ def pid; nil end
310
+
311
+ # Returns current read position.
312
+ def pos; @read_length end
313
+ alias :tell :pos
314
+
315
+ def read(length = nil, buf = '') #:args: [length[, buf]]
316
+ raise TypeError, "can't modify frozen stream" if frozen?
317
+ raise IOError, 'closed stream' if closed?
318
+ if length.nil?
319
+ return '' if eof?
320
+ length = [0, @length - @read_length].max
321
+ else
322
+ raise ArgumentError, "negative length #{length} given" if length < 0
323
+ if eof?
324
+ buf[0..-1] = ''
325
+ return nil
326
+ end
327
+ end
328
+
329
+ if @packet.eof?
330
+ written_length = 0
331
+ else
332
+ chunk = @packet.read_bytes(length)
333
+ written_length = chunk.length
334
+ @read_length += written_length
335
+ buf[0, written_length] = chunk
336
+ end
337
+ while written_length < length and !eof?
338
+ packet = Net::AJP13::Packet.new
339
+ packet.direction = :from_app
340
+ packet.append_byte GET_BODY_CHUNK
341
+ packet.append_integer [@length - @read_length, MAX_BODY_CHUNK_SIZE].min
342
+ packet.send_to @sock
343
+
344
+ @packet = Net::AJP13::Packet.from_io(@sock)
345
+ if @packet.length == 0
346
+ # this means eof
347
+ break
348
+ else
349
+ chunk = @packet.read_bytes(length - written_length)
350
+ buf[written_length, chunk.length] = chunk
351
+ written_length += chunk.length
352
+ @read_length += chunk.length
353
+ end
354
+ end
355
+ if written_length < buf.length
356
+ buf[written_length..-1] = ''
357
+ end
358
+
359
+ return buf
360
+ end
361
+
362
+
363
+ def readchar
364
+ str = read(1)
365
+ if str.nil?
366
+ raise EOFError
367
+ else
368
+ str[0]
369
+ end
370
+ end
371
+
372
+ GETS_BLOCK_SIZE = 256 # :nodoc:
373
+ def gets(rs = $/)
374
+ return read if rs.nil?
375
+ @lineno ||= 0
376
+ @line ||= ''
377
+ pattern = /\A.+?#{rs=='' ? "\\r?\\n\\r?\\n" : Regexp.escape(rs)}/
378
+ until md = pattern.match(@line)
379
+ block = read(GETS_BLOCK_SIZE)
380
+ if block.nil?
381
+ line = @line
382
+ @line = nil
383
+ @lineno += 1
384
+ return line == '' ? nil : line
385
+ else
386
+ @line << block
387
+ end
388
+ end
389
+ @line = md.post_match
390
+ @lineno += 1
391
+ return md.to_s
392
+ end
393
+ def readline(rs = $/)
394
+ line = gets(rs)
395
+ if line.nil?
396
+ raise EOFError
397
+ else
398
+ line
399
+ end
400
+ end
401
+ def each_line(rs = $/)
402
+ while line = gets(rs)
403
+ yield line
404
+ end
405
+ end
406
+ alias :each :each_line
407
+
408
+ # Raises NotImplementedError
409
+ def reopen(*args)
410
+ raise NotImplementedError, "#{self.class} does not support reopen"
411
+ end
412
+
413
+ def sync; @sock.sync end
414
+ def sync=(val); @sock.sync = val end
415
+
416
+ alias :sysread :read
417
+
418
+ # Returns self
419
+ def to_io; self end
420
+
421
+ def ungetc(char)
422
+ raise TypeError, "#{char} is not a Fixnum" unless char.is_a? Fixnum
423
+ raise ArgumentError, "#{char} must be a byte, but negative" if char < 0
424
+ raise ArgumentError, "#{char} is too large to treat as a byte" if char > 0xFF
425
+ @packet.unread_byte(char)
426
+ @read_length -= 1
427
+ nil
428
+ end
429
+ end
430
+ end
data/ruby-ajp.gemspec ADDED
@@ -0,0 +1,27 @@
1
+ Gem::Specification.new do |spec|
2
+ spec.name = "ruby-ajp"
3
+ spec.version = "0.1.5"
4
+ spec.required_ruby_version = ">= 1.8.3"
5
+ spec.summary = "An implementation of Apache Jserv Protocol 1.3 in Ruby"
6
+ spec.author = "Yugui"
7
+ spec.email = "yugui@yugui.sakura.ne.jp"
8
+ spec.rubyforge_project = "ruby-ajp"
9
+ spec.files =
10
+ Dir.glob("lib/**/*.rb") +
11
+ Dir.glob("test/**/test_*.rb") +
12
+ Dir.glob("test/**/data/*") +
13
+ Dir.glob("example/**/*.rb") +
14
+ Dir.glob("NEWS.{en,ja}") +
15
+ Dir.glob("Install.{en,ja}") +
16
+ Dir.glob("README.{en,ja}") <<
17
+ "Rakefile" <<
18
+ "ruby-ajp.gemspec" <<
19
+ "setup.rb" <<
20
+ "COPYING"
21
+ spec.files.reject! {|fn| fn.include?('.svn') }
22
+ spec.test_files = [
23
+ 'packet', 'request',
24
+ 'response', 'client'
25
+ ].map{|x| "test/net/test_ajp13#{x}.rb"}
26
+ spec.has_rdoc = true
27
+ end