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