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/lib/net/ajp13.rb ADDED
@@ -0,0 +1,815 @@
1
+ # = Ruby/AJP
2
+ # An implementation of AJP(Apache Jserv Protocol) 1.3 in Ruby,
3
+ # based on http://tomcat.apache.org/connectors-doc/common/ajpv13a.html.
4
+ #
5
+ # [Net::AJP13::Client] provides high-level API to implement AJP clients.
6
+ # The interface of the client-side library is similar to
7
+ # net/http.
8
+ # see ajp13client.rb[link:files/lib/net/ajp13client_rb.html]
9
+ # for more detail.
10
+ # [Net::AJP13::Server] provides high-level API to implement AJP servers.
11
+ # see ajp13server.rb[link:files/lib/net/ajp13server_rb.html]
12
+ # for more detail.
13
+ #
14
+ # Author:: Yugui (mailto:yugui@yugui.sakura.ne.jp)
15
+ # Copyright:: Copyright (c) 2005-2006 Yugui
16
+ # License:: LGPL
17
+ #
18
+ # This library is free software; you can redistribute it and/or
19
+ # modify it under the terms of the GNU Lesser General Public
20
+ # License as published by the Free Software Foundation; version 2.1
21
+ # of the License any later version.
22
+ #
23
+ # This library is distributed in the hope that it will be useful,
24
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
25
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
26
+ # Lesser General Public License for more details.
27
+ #
28
+ # You should have received a copy of the GNU Lesser General Public
29
+ # License along with this library; if not, write to the Free Software
30
+ # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston,
31
+ # MA 02110-1301 USA
32
+ #
33
+
34
+ require 'net/http'
35
+
36
+ # :stopdoc:
37
+ module Net; end
38
+ # :startdoc:
39
+
40
+ module Net::AJP13
41
+ module Constants
42
+ # default AJP port
43
+ DEFAULT_PORT = 8009
44
+
45
+ # :stopdoc:
46
+ FORWARD_REQUEST = 0x02
47
+ SHUTDOWN = 0x07
48
+ PING = 0x08
49
+ CPING = 0x0A
50
+
51
+ SEND_BODY_CHUNK = 0x03
52
+ SEND_HEADERS = 0x04
53
+ END_RESPONSE = 0x05
54
+ GET_BODY_CHUNK = 0x06
55
+ CPONG_REPLY = 0x09
56
+
57
+ MAX_PACKET_SIZE = 8 * 1024 - 4 # limitaion of Jakarta's implementation
58
+ MAX_BODY_CHUNK_SIZE = MAX_PACKET_SIZE - 2
59
+ # :startdoc:
60
+ end
61
+ end
62
+
63
+ #
64
+ # Represents AJP1.3 Request.
65
+ #
66
+ # Mixes int the Net::HTTPHeader module
67
+ #
68
+ class Net::AJP13::Request
69
+ include Net::HTTPHeader
70
+ include Net::AJP13::Constants
71
+
72
+ # :stopdoc:
73
+
74
+ # maps HTTP/WebDAV method into its 16bit code
75
+ REQUEST_METHOD_CODES = {
76
+ :OPTIONS => 1, :GET => 2, :HEAD => 3, :POST => 4,
77
+ :PUT => 5, :DELETE => 6, :TRACE => 7, :PROPFIND => 8,
78
+ :PROPPATCH => 9, :MKCOL => 10, :COPY => 11, :MOVE => 12,
79
+ :LOCK => 13, :UNLOCK => 14, :ACL => 15, :REPORT => 16,
80
+ 'VERSION-CONTROL'.intern => 17, :CHECKIN => 18, :CHECKOUT => 19,
81
+ :UNCHECKOUT => 20, :SEARCH => 21
82
+ }.freeze
83
+
84
+ # Maps core HTTP headers into its 16bit code
85
+ SC_REQ_HEADER_NAMES = {
86
+ 'accept' => 0xA001, 'accept-charset' => 0xA002,
87
+ 'accept-encoding' => 0xA003, 'accept-language' => 0xA004,
88
+ 'authorization' => 0xA005, 'connection' => 0xA006,
89
+ 'content-type' => 0xA007, 'content-length' => 0xA008,
90
+ 'cookie' => 0xA009, 'cookie2' => 0xA00A,
91
+ 'host' => 0xA00B, 'pragma' => 0xA00C,
92
+ 'referer' => 0xA00D, 'user_agent' => 0xA00E
93
+ }.freeze
94
+ SC_REQ_HEADER_NAMES.each_key {|k| k.freeze}
95
+
96
+ SC_A_REQ_ATTRIBUTE = 0xA0
97
+ # Maps request attribute names into their codes
98
+ SC_A_NAMES = {
99
+ :context => 0x01,
100
+ :servlet_path => 0x02,
101
+ :remote_user => 0x03,
102
+ :auth_type => 0x04,
103
+ :query_string => 0x05,
104
+ :jvm_route => 0x06,
105
+ :ssl_cert => 0x07,
106
+ :ssl_cipher => 0x08,
107
+ :ssl_session => 0x09,
108
+ :req_attribute => SC_A_REQ_ATTRIBUTE,
109
+ :ssl_key_size => 0x0B
110
+ }.freeze
111
+
112
+ # The termination byte of AJP request packet.
113
+ REQUEST_TERMINATOR = 0xFF
114
+
115
+ # :startdoc:
116
+
117
+ #
118
+ # Creates a AJP13 request object.
119
+ # +path+:: path to request
120
+ #
121
+ def initialize(path, init_header = nil)
122
+ @method = self.class::METHOD
123
+ @request_has_body = self.class::REQUEST_HAS_BODY
124
+ @response_has_body = self.class::RESPONSE_HAS_BODY
125
+ @path, qs = path.split(/\?/, 2)
126
+ initialize_http_header init_header
127
+ @attributes = {}
128
+ add_attribute('query_string', qs) if qs
129
+
130
+ @is_ssl = false
131
+ @protocol = nil
132
+ @remote_addr = nil
133
+ @remote_host = nil
134
+ @server_name = nil
135
+ @server_port = nil # Net::HTTP.default_port
136
+ end
137
+
138
+ METHOD = nil
139
+ REQUEST_HAS_BODY = nil
140
+ RESPONSE_HAS_BODY = nil
141
+
142
+ # Reads a AJP packet from +io+, creates the new AJP13Request object
143
+ # corresponding to the packet, and returns the object.
144
+ #
145
+ # When the request has the entity body, also reads some packets from +io+
146
+ # until the body ends. You can get the entity body as +request+.+body+.
147
+ #
148
+ # +io+:: packet source
149
+ # [pre-condition]
150
+ # The read position of +io+ is the top of an AJP packet.
151
+ # [post-condition]
152
+ # The read position of +io+ is just behind of the AJP packets
153
+ # corresponding to the returned AJP13Request object.
154
+ # If an exception raised, the read position is unknown.
155
+ #
156
+ # If called with a block, yields the given block with a fragment of the body
157
+ # on reading the body.
158
+ #
159
+ def self.from_io(io)
160
+ from_packet(Net::AJP13::Packet.from_io(io))
161
+ end
162
+
163
+ def self.from_packet(packet) #:nodoc: internal use
164
+ raise 'The AJP packet is not to application server' unless packet.to_app?
165
+ raise 'The AJP packet is not a forward request' unless
166
+ packet.message_type == FORWARD_REQUEST
167
+
168
+ method = packet.read_byte
169
+ method =
170
+ REQUEST_METHOD_CODES.index(method) ||
171
+ raise("Unrecognized HTTP method code #{method}")
172
+
173
+ protocol = packet.read_string
174
+ path = packet.read_string
175
+
176
+ req = self.new(path)
177
+ req.method = method.to_s.upcase
178
+ req.protocol = protocol
179
+
180
+ req.remote_addr = packet.read_string
181
+ req.remote_host = packet.read_string
182
+ req.server_name = packet.read_string
183
+ req.server_port = packet.read_integer
184
+ req.is_ssl = packet.read_boolean
185
+
186
+ num_headers = packet.read_integer
187
+ 1.upto(num_headers) do
188
+ if packet.peek_byte == 0xA0
189
+ header_name = packet.read_integer
190
+ header_name =
191
+ SC_REQ_HEADER_NAMES.index(header_name) ||
192
+ raise("Unrecognized HTTP header code #{header_name}")
193
+ header_name = header_name.tr('_', '-')
194
+ else
195
+ header_name = packet.read_string
196
+ end
197
+ req[header_name] = packet.read_string
198
+ end
199
+ loop do
200
+ case attr_name = packet.read_byte
201
+ when nil
202
+ raise 'Missing AJP request packet terminator'
203
+ when REQUEST_TERMINATOR
204
+ break
205
+ when SC_A_REQ_ATTRIBUTE
206
+ attr_name = packet.read_string
207
+ else
208
+ attr_name = SC_A_NAMES.index(attr_name) ||
209
+ raise("Unrecognized AJP request attribute #{attr_name}")
210
+ attr_name = attr_name.to_s
211
+ end
212
+ attr_value = packet.read_string
213
+ req.add_attribute(attr_name, attr_value)
214
+ end
215
+
216
+ return req
217
+ end
218
+
219
+ def inspect #:nodoc:
220
+ "\#<#{self.class} #{@method}>"
221
+ end
222
+
223
+ # Path to request
224
+ attr_reader :path
225
+ alias :request_uri :path
226
+
227
+ # HTTP request method
228
+ attr_reader :method
229
+ attr_writer :method #:nodoc: internal use
230
+
231
+ # IP address of the HTTP client
232
+ attr_accessor :remote_addr
233
+ # Host name of the HTTP client
234
+ attr_accessor :remote_host
235
+ # HTTP server name
236
+ attr_accessor :server_name
237
+ # HTTP port
238
+ attr_accessor :server_port
239
+
240
+ # Returns if it is permitted that the client sends this request
241
+ # with a request body.
242
+ def request_body_permitted?
243
+ @request_has_body
244
+ end
245
+
246
+ # Returns if it is permitted that the server sends the corresponding response
247
+ # with a response body
248
+ def response_body_permitted?
249
+ @response_has_body
250
+ end
251
+
252
+ # HTTP-side connection is over SSL or not.
253
+ attr_accessor :is_ssl
254
+ alias :is_ssl? :is_ssl
255
+ def is_ssl=(value) #:nodoc:
256
+ @is_ssl = !!value
257
+ end
258
+
259
+ # HTTP protocol name/version
260
+ attr_accessor :protocol
261
+
262
+ # Adds request attribute instead of replace
263
+ # Second argument +value+ must be a String
264
+ #
265
+ # See also #set_attribute
266
+ def add_attribute(name, value)
267
+ name = name.downcase
268
+ @attributes[name] ||= []
269
+ @attributes[name] << value
270
+ end
271
+
272
+ # Set the request attribute corresponding to the case-insensitive name.
273
+ # See also #add_attribute
274
+ def set_attribute(name, value)
275
+ @attributes[name.downcase] = [value]
276
+ end
277
+
278
+ # Returns an array of attribute values corresponding to the case-insensitive
279
+ # +name+.
280
+ def get_attributes(name)
281
+ name = name.downcase
282
+ values = @attributes[name]
283
+ values and values.dup
284
+ end
285
+
286
+ # Deletes request attributes whose name are +name+
287
+ def delete_attribute(name)
288
+ @attributes.delete(name.downcase)
289
+ end
290
+
291
+ # Iterates for each request attribute values.
292
+ # +name+ can appears more than once if multiple values exist for the +name+.
293
+ def each_attribute(&block) #:yield: +name+, +value+
294
+ @attributes.each do |name, values|
295
+ values.each do |value|
296
+ yield name, value
297
+ end
298
+ end
299
+ end
300
+
301
+ # Request body part
302
+ attr_accessor :body
303
+ # Stream like a IO object that provides the request body part
304
+ attr_accessor :body_stream
305
+
306
+ def send_to(io)
307
+ to_packet.send_to(io)
308
+ end
309
+
310
+ # Returns an AJP packet object that represents the FORWARD_REQUEST packet
311
+ # corresponding to this request.
312
+ def to_packet
313
+ packet = Net::AJP13::Packet.new
314
+ packet.direction = :to_app
315
+ packet.append_byte FORWARD_REQUEST
316
+ packet.append_byte REQUEST_METHOD_CODES[method.upcase.intern]
317
+
318
+ # Mandatory parameters
319
+ [:protocol, :request_uri].each do |name|
320
+ raise "Mandatory parameter #{name} not supplied." unless __send__(name)
321
+ packet.append_string self.__send__(name)
322
+ end
323
+ [:remote_addr, :remote_host, :server_name].each do |name|
324
+ packet.append_string self.__send__(name)
325
+ end
326
+ raise "Mandatory parameter server_port not supplied" unless server_port
327
+ packet.append_integer server_port
328
+
329
+ packet.append_boolean is_ssl?
330
+
331
+ packet.append_integer self.length
332
+ self.each_header do |key, val|
333
+ if packed_name = SC_REQ_HEADER_NAMES[key.downcase]
334
+ packet.append_integer packed_name
335
+ packet.append_string val
336
+ else
337
+ packet.append_string key
338
+ packet.append_string val
339
+ end
340
+ end
341
+ self.each_attribute do |key, val|
342
+ if packed_name = SC_A_NAMES[key.downcase.to_sym]
343
+ packet.append_byte packed_name
344
+ packet.append_string val
345
+ else
346
+ packet.append_byte SC_A_REQ_ATTRIBUTE
347
+ packet.append_string key
348
+ packet.append_string val
349
+ end
350
+ end
351
+ packet.append_byte REQUEST_TERMINATOR
352
+
353
+ return packet
354
+ end
355
+ end
356
+
357
+ [
358
+ [ 'OPTIONS', true, true, 'Options' ],
359
+ [ 'GET', false, true, 'Get' ],
360
+ [ 'HEAD', false, false, 'Head' ],
361
+ [ 'POST', true, true, 'Post' ],
362
+ [ 'PUT', true, true, 'Put' ],
363
+ [ 'DELETE', false, true, 'Delete' ],
364
+ [ 'TRACE', true, true, 'Trace' ],
365
+ [ 'PROPFIIND', true, true, 'PropFind' ],
366
+ [ 'PROPPATCH', true, true, 'PropPatch' ],
367
+ [ 'MKCOL', true, true, 'MkCol' ],
368
+ [ 'COPY', true, true, 'Copy' ],
369
+ [ 'MOVE', true, true, 'Move' ],
370
+ [ 'LOCK', true, true, 'Lock' ],
371
+ [ 'UNLOCK', false, false, 'Unlock' ],
372
+ [ 'ACL', true, true, 'Acl' ],
373
+ [ 'REPORT', true, true, 'Report' ],
374
+ [ 'VERSION-CONTROL', true, true, 'VersionControl' ],
375
+ [ 'CHECKIN', true, true, 'CheckIn' ],
376
+ [ 'CHECKOUT', true, true, 'CheckOut' ],
377
+ [ 'UNCHECKOUT', true, true, 'UnCheckOut' ],
378
+ [ 'SEARCH', true, true, 'Search' ],
379
+ [ 'MKWORKSPACE', true, true, 'MkWorkSpace' ],
380
+ [ 'UPDATE', true, true, 'Update' ],
381
+ [ 'LABEL', true, true, 'Label' ],
382
+ [ 'MERGE', true, true, 'Merge' ],
383
+ [ 'BASELINE_CONTROL',true, true, 'BaseLineControl'],
384
+ [ 'MKACTIVITY', true, true, 'MkActivity' ]
385
+ ].each do |method_name, has_req_body, has_res_body, class_name, klass|
386
+ klass = Class.new(Net::AJP13::Request) do
387
+ const_set :METHOD, method_name
388
+ const_set :REQUEST_HAS_BODY, has_req_body
389
+ const_set :RESPONSE_HAS_BODY, has_res_body
390
+ end
391
+ Net::AJP13.const_set "#{class_name}Request", klass
392
+ end
393
+
394
+ # Represents AJP1.3 response
395
+ class Net::AJP13::Response
396
+ include Net::HTTPHeader
397
+ include Net::AJP13::Constants
398
+
399
+ # :stopdoc:
400
+ REASON_PHRASES = {
401
+ 100 => 'Continue',
402
+ 101 => 'Switching Protocols',
403
+ 200 => 'OK',
404
+ 201 => 'Created',
405
+ 202 => 'Accepted',
406
+ 203 => 'Non-Authoritative Information',
407
+ 204 => 'No Content',
408
+ 205 => 'Reset Content',
409
+ 206 => 'Partial Content',
410
+ 300 => 'Multiple Choices',
411
+ 301 => 'Moved Permanently',
412
+ 302 => 'Moved Temporarily',
413
+ 303 => 'See Other',
414
+ 304 => 'Not Modified',
415
+ 305 => 'Use Proxy',
416
+ 400 => 'Bad Request',
417
+ 401 => 'Unauthorized',
418
+ 402 => 'Payment Required',
419
+ 403 => 'Forbidden',
420
+ 404 => 'Not Found',
421
+ 405 => 'Method Not Allowed',
422
+ 406 => 'Not Acceptable',
423
+ 407 => 'Proxy Authentication Required',
424
+ 408 => 'Request Time-out',
425
+ 409 => 'Conflict',
426
+ 410 => 'Gone',
427
+ 411 => 'Length Required',
428
+ 412 => 'Precondition Failed',
429
+ 413 => 'Request Entity Too Large',
430
+ 414 => 'Request-URI Too Large',
431
+ 415 => 'Unsupported Media Type',
432
+ 500 => 'Internal Server Error',
433
+ 501 => 'Not Implemented',
434
+ 502 => 'Bad Gateway',
435
+ 503 => 'Service Unavailable',
436
+ 504 => 'Gateway Time-out',
437
+ }.freeze
438
+ REASON_PHRASES.each do |k,v|; v.freeze end
439
+
440
+ # Maps HTTP response header names into their codes
441
+ SC_RES_HEADER_NAMES = {
442
+ 0xA001 => 'Content-Type',
443
+ 0xA002 => 'Content-Language',
444
+ 0xA003 => 'Content-Length',
445
+ 0xA004 => 'Date',
446
+ 0xA005 => 'Last-Modified',
447
+ 0xA006 => 'Location',
448
+ 0xA007 => 'Set-Cookie',
449
+ 0xA008 => 'Set-Cookie2',
450
+ 0xA009 => 'Servlet-Engine',
451
+ 0xA00A => 'Status',
452
+ 0xA00B => 'WWW-AUthenticate'
453
+ }.freeze
454
+ SC_RES_HEADER_NAMES.each do |k,v|; v.freeze end
455
+
456
+ # Maps HTTP response header codes into their names.
457
+ SC_RES_HEADER_CODES = SC_RES_HEADER_NAMES.inject({}) do |memo, item|
458
+ code, name = item
459
+ memo[name.downcase.freeze] = code
460
+ memo
461
+ end
462
+ # :startdoc:
463
+
464
+ #
465
+ # Creates a new AJP13Response object
466
+ # +status+:: HTTP response status code. (in integer)
467
+ # +options+:: Hash that contains response headers.
468
+ # If key is :reason_phrase, it overrides the reason phrase.
469
+ def initialize(status, options = {})
470
+ @status = status.to_i
471
+ @reason_phrase =
472
+ options[:reason_phrase] || options['reason_phrase'] ||
473
+ REASON_PHRASES[@status]
474
+ initialize_http_header options.reject{|k, v| k.to_sym == :reason_phrase }
475
+ end
476
+
477
+ # Status Code
478
+ attr_reader :status
479
+ # Status Code as String
480
+ attr_reader :code
481
+ def code #:nodoc:
482
+ status.to_s
483
+ end
484
+
485
+ # Reason Phrase
486
+ attr_reader :reason_phrase
487
+ alias :message :reason_phrase
488
+
489
+ # The response body
490
+ attr_accessor :body
491
+ # Input stream like an IO object, which provides the response body.
492
+ attr_accessor :body_stream
493
+
494
+ # Creates a new AJP13::Response object based on bytes read from +io+.
495
+ # [Pre-Condition] The read position of +io+ is the head of an AJP packet
496
+ # whose prefix code is SEND_HEADERS.
497
+ # [Post-Condition] The read positioin of +io+ is just behind of the AJP
498
+ # packet if no exception raised.
499
+ # The read position is unspecified if an exception raised.
500
+ # Raises
501
+ # AJPPacketError:: if the given packet is bloken.
502
+ # ArgumentError:: if the given packet is not a SEND_HEADERS packet.
503
+ def self.from_io(io)
504
+ from_packet(Net::AJP13::Packet.from_io(io))
505
+ end
506
+
507
+ def self.from_packet(packet) #:nodoc: internal use
508
+ raise ArgumentError, 'The AJP response packet is not from an application container' unless packet.from_app?
509
+
510
+ raise ArgumentError, "The AJP response packet is not SEND_HEADERS but #{packet.messge_type}" unless packet.message_type == SEND_HEADERS
511
+
512
+ status = packet.read_integer
513
+ phrase = packet.read_string
514
+ res = self.new(status, :reason_phrase => phrase)
515
+
516
+ num_headers = packet.read_integer
517
+ 1.upto(num_headers) do
518
+ if packet.peek_byte == 0xA0
519
+ header_code = packet.read_integer
520
+ header_name = SC_RES_HEADER_NAMES[header_code]
521
+ raise Net::AJP13::AJPPacketError, "Unrecognized header code #{header_code}" unless header_name
522
+ else
523
+ header_name = packet.read_string
524
+ end
525
+ header_value = packet.read_string
526
+ res.add_field(header_name, header_value)
527
+ end
528
+
529
+ return res
530
+ end
531
+
532
+ def send_to(io)
533
+ to_packet.send_to(io)
534
+ end
535
+
536
+ # Returns a Net::AJP13::Packet object that represents the SEND_HEADER packet
537
+ # corresponding to this response.
538
+ def to_packet
539
+ packet = Net::AJP13::Packet.new
540
+ packet.direction = :from_app
541
+ packet.append_byte SEND_HEADERS
542
+ packet.append_integer self.status
543
+ packet.append_string self.reason_phrase
544
+ packet.append_integer self.length
545
+ self.each_header do |key, val|
546
+ if packed_name = SC_RES_HEADER_CODES[key.downcase]
547
+ packet.append_integer packed_name
548
+ else
549
+ packet.append_string key
550
+ end
551
+ packet.append_string val
552
+ end
553
+ packet
554
+ end
555
+ end
556
+
557
+ # Raised when ajp session takes an illegal state.
558
+ class Net::AJP13::AJPStateError < IOError
559
+ end
560
+
561
+ # Represents errors on AJP packet format
562
+ class Net::AJP13::AJPPacketError < IOError
563
+ end
564
+
565
+ # :stopdoc:
566
+ # Represents AJP1.3 Packet
567
+ class Net::AJP13::Packet
568
+ include Net::AJP13::Constants
569
+
570
+ # The magic number that represents a packet is from a web server to
571
+ # an app server.
572
+ AJP13_WS_HEADER = "\x12\x34"
573
+
574
+ # The magic number that represents a packet is from an app server to
575
+ # a web server.
576
+ # corresponds to 'AB' in ASCII.
577
+ AJP13_SW_HEADER = "\x41\x42"
578
+
579
+ # :stopdoc:
580
+ # suiting Ruby 1.9 feature
581
+ if RUBY_VERSION < '1.9'
582
+ # calls private methods
583
+ alias_method :fcall, :__send__
584
+ end
585
+ # :startdoc:
586
+
587
+ # Creates a new packet object.
588
+ def initialize
589
+ @direction = nil
590
+ @packet_length = 0
591
+ @buf = ''
592
+ end
593
+
594
+ # Reads an AJP packet from +io+, and creates a Packet object based on it.
595
+ # [Pre-condition] The read position of +io+ is the head of an AJP packet.
596
+ # [Post-condition] The read position of +io+ is just behind of the AJP packet
597
+ # header when the object creation succeeded.
598
+ # The read position is unspecified if an exception raised.
599
+ #
600
+ # Raises
601
+ # [AJPPacketError] when the read packet has invalid format.
602
+ # [IOError] when +io+ raises it.
603
+ def self.from_io(io)
604
+ p = self.new
605
+ p.fcall(:initialize_by_io, io)
606
+ p
607
+ end
608
+
609
+ # Initializer used by Packet.from_io
610
+ def initialize_by_io(io) #:nodoc: internal use
611
+ header = io.read(4)
612
+ raise Net::AJP13::AJPPacketError, "The AJP packet is broken" unless
613
+ header and header.length == 4
614
+
615
+ case header[0..1]
616
+ when AJP13_WS_HEADER
617
+ @direction = :to_app
618
+ when AJP13_SW_HEADER
619
+ @direction = :from_app
620
+ else
621
+ raise Net::AJP13::AJPPacketError, "Unrecognized AJP packet direction"
622
+ end
623
+
624
+ @packet_length = header[2..3].unpack('n')[0]
625
+
626
+ @byte_stream = io
627
+ @pos = 0 # read position
628
+
629
+ @buf = nil
630
+ end
631
+ private :initialize_by_io
632
+
633
+ # The value is
634
+ # [:from_app] if this packet is from an app server to a web server.
635
+ # [:to_app] if this packet is from an web server to an app server.
636
+ # [nil] if this packet's direction has not been not specified yet.
637
+ attr_accessor :direction
638
+
639
+ def direction=(val) #:nodoc:
640
+ if [nil, :to_app, :from_app].include? val
641
+ @direction = val
642
+ else
643
+ raise ArgumentError, "Illegal packet direction value #{val}"
644
+ end
645
+ end
646
+
647
+ # returns whether the packet is from a web server to an app server
648
+ def to_app?
649
+ direction == :to_app
650
+ end
651
+
652
+ # return wheter the packet is from an app server to a web server
653
+ def from_app?
654
+ direction == :from_app
655
+ end
656
+
657
+ def append_bytes(bytes)
658
+ @buf << bytes
659
+ end
660
+
661
+ def append_byte(val)
662
+ raise ArgumentError, "Too large to pack into a byte: #{val}" if val > 0xFF
663
+ @buf << [val].pack('C')
664
+ end
665
+
666
+ def append_boolean(val)
667
+ @buf <<
668
+ if val then "\x01" else "\x00" end
669
+ end
670
+ def append_integer(val)
671
+ val = val.to_int
672
+ raise ArgumentError, "#{val} is too large to store into an AJP packet" if
673
+ val > 0xFFFF
674
+ @buf << [val].pack('n')
675
+ end
676
+ def append_string(str)
677
+ if str.nil?
678
+ @buf << "\xFF\xFF"
679
+ else
680
+ str = str.to_str
681
+ @buf << [str.length].pack('n') << str << "\0"
682
+ end
683
+ end
684
+
685
+ # returns whether the read position is over the packet length.
686
+ def eof?
687
+ @byte_stream.eof? or
688
+ @pos >= @packet_length
689
+ end
690
+
691
+ # Type of message contained in the packet.
692
+ #
693
+ # Reads a byte (type code) from the packet when called first.
694
+ def message_type
695
+ @message_type ||= read_byte
696
+ end
697
+
698
+ # Reads bytes from the packet, and advance the read position.
699
+ # returns +nil+ if the read position is over the packet length.
700
+ def read_bytes(length)
701
+ return nil unless @pos < @packet_length
702
+ length = @packet_length - @pos if @packet_length - @pos < length
703
+ bytes = @byte_stream.read(length)
704
+ @pos += bytes.length if bytes
705
+ bytes
706
+ end
707
+
708
+ # reads a byte from the packet, and advance the read position.
709
+ # returns +nil+ if the read position is over the packet length.
710
+ def read_byte
711
+ return nil unless @pos < @packet_length
712
+ byte = @byte_stream.getc
713
+ @pos += 1 if byte
714
+ byte
715
+ end
716
+
717
+ def unread_byte(byte)
718
+ @byte_stream.ungetc(byte)
719
+ @pos -= 1
720
+ nil
721
+ end
722
+
723
+ # reads a byte from the packet, but does not advance the read position.
724
+ # returns +nil+ if the read position is over the packet length.
725
+ def peek_byte
726
+ return nil unless @pos < @packet_length
727
+ byte = @byte_stream.getc
728
+ @byte_stream.ungetc(byte) if byte
729
+ byte
730
+ end
731
+
732
+ # reads a boolean value from the packet, and advance the read position.
733
+ # returns +nil+ if the read position is over the packet length.
734
+ def read_boolean
735
+ byte = read_byte
736
+
737
+ case byte
738
+ when nil: nil
739
+ when 0x00: false
740
+ when 0x01: true
741
+ else
742
+ raise Net::AJP13::AJPPacketError,
743
+ "Can't recognize #{byte} as an boolean value"
744
+ end
745
+ end
746
+
747
+ # reads a 16bit integer from the packet, and advance the read position.
748
+ # returns +nil+ if the read position is over the packet length.
749
+ def read_integer
750
+ return nil unless @pos < @packet_length
751
+ raise Net::AJP13::AJPPacketError, "too short to read as integer" if
752
+ @packet_length - @pos < 2
753
+
754
+ int = @byte_stream.read(2)
755
+ raise Net::AJP13::AJPPacketError, "broken packet" if int.nil?
756
+ @pos += int.length
757
+ raise Net::AJP13::AJPPacketError, "broken packet" if int.length < 2
758
+ int.unpack('n')[0]
759
+ end
760
+
761
+ # reads a string from the packet, and advance the read position.
762
+ # returns +nil+ if the read position is over the packet length.
763
+ def read_string
764
+ len = read_integer
765
+ return nil unless len and len != 0xFFFF
766
+ raise Net::AJP13::AJPPacketError, "str length too large: #{len}" if
767
+ len > @packet_length - @pos
768
+
769
+ str = @byte_stream.read(len)
770
+ @pos += str.length
771
+ if str.nil? or str.length != len
772
+ raise Net::AJP13::AJPPacketError, "Invalid string format"
773
+ end
774
+
775
+ trailer = @byte_stream.getc
776
+ @pos += 1 if trailer
777
+ raise Net::AJP13::AJPPacketError, "Missing trailing NUL" unless trailer == 0x00
778
+
779
+ str
780
+ end
781
+
782
+ # Sends this packet into the specified socket
783
+ # +io+:: IO object to which this packet will be written.
784
+ def send_to(io)
785
+ raise 'The packet is too large' if @buf.length > 0xFFFF + 4
786
+ warn "The packet is too larget for some implementations: #{@buf.length} bytes" if @buf.length > MAX_PACKET_SIZE
787
+
788
+ header = "\0\0\0\0"
789
+ header[0..1] =
790
+ case @direction
791
+ when :from_app
792
+ AJP13_SW_HEADER
793
+ when :to_app
794
+ AJP13_WS_HEADER
795
+ else
796
+ raise Net::AJP13::AJPPacketError, 'Packet direction not specified'
797
+ end
798
+
799
+ header[2..3] = [@buf.length].pack('n')
800
+ io.write header
801
+ io.write @buf
802
+ io.flush
803
+ end
804
+
805
+ # length of the packet content, without including the payload
806
+ def length
807
+ if @buf
808
+ @buf.length
809
+ else
810
+ @packet_length
811
+ end
812
+ end
813
+ end
814
+ # :startdoc:
815
+