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,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