rubyforge 0.0.1 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,542 @@
1
+ # HTTP - HTTP container.
2
+ # Copyright (C) 2001, 2002, 2003, 2005 NAKAMURA, Hiroshi.
3
+ #
4
+ # This module is copyrighted free software by NAKAMURA, Hiroshi.
5
+ # You can redistribute it and/or modify it under the same term as Ruby.
6
+
7
+ require 'uri'
8
+ require 'time'
9
+
10
+ module HTTP
11
+
12
+
13
+ module Status
14
+ OK = 200
15
+ MOVED_PERMANENTLY = 301
16
+ FOUND = 302
17
+ SEE_OTHER = 303
18
+ TEMPORARY_REDIRECT = MOVED_TEMPORARILY = 307
19
+ BAD_REQUEST = 400
20
+ INTERNAL = 500
21
+
22
+ def self.redirect?(status)
23
+ [
24
+ MOVED_PERMANENTLY, FOUND, SEE_OTHER,
25
+ TEMPORARY_REDIRECT, MOVED_TEMPORARILY
26
+ ].include?(status)
27
+ end
28
+ end
29
+
30
+
31
+ class Error < StandardError; end
32
+ class BadResponseError < Error; end
33
+
34
+ class << self
35
+ def http_date(a_time)
36
+ a_time.gmtime.strftime("%a, %d %b %Y %H:%M:%S GMT")
37
+ end
38
+
39
+ ProtocolVersionRegexp = Regexp.new('^(?:HTTP/|)(\d+)\.(\d+)$')
40
+ def keep_alive_enabled?(version)
41
+ ProtocolVersionRegexp =~ version
42
+ if !($1 and $2)
43
+ false
44
+ elsif $1.to_i > 1
45
+ true
46
+ elsif $1.to_i == 1 and $2.to_i >= 1
47
+ true
48
+ else
49
+ false
50
+ end
51
+ end
52
+ end
53
+
54
+
55
+ # HTTP::Message -- HTTP message.
56
+ #
57
+ # DESCRIPTION
58
+ # A class that describes 1 HTTP request / response message.
59
+ #
60
+ class Message
61
+ CRLF = "\r\n"
62
+
63
+ # HTTP::Message::Headers -- HTTP message header.
64
+ #
65
+ # DESCRIPTION
66
+ # A class that describes header part of HTTP message.
67
+ #
68
+ class Headers
69
+ # HTTP version string in a HTTP header.
70
+ attr_accessor :http_version
71
+ # Content-type.
72
+ attr_accessor :body_type
73
+ # Charset.
74
+ attr_accessor :body_charset
75
+ # Size of body.
76
+ attr_reader :body_size
77
+ # A milestone of body.
78
+ attr_accessor :body_date
79
+ # Chunked or not.
80
+ attr_reader :chunked
81
+ # Request method.
82
+ attr_reader :request_method
83
+ # Requested URI.
84
+ attr_reader :request_uri
85
+ # HTTP status reason phrase.
86
+ attr_accessor :reason_phrase
87
+
88
+ StatusCodeMap = {
89
+ Status::OK => 'OK',
90
+ Status::MOVED_PERMANENTLY => 'Moved Permanently',
91
+ Status::FOUND => 'Found',
92
+ Status::SEE_OTHER => 'See Other',
93
+ Status::TEMPORARY_REDIRECT => 'Temporary Redirect',
94
+ Status::MOVED_TEMPORARILY => 'Temporary Redirect',
95
+ Status::BAD_REQUEST => 'Bad Request',
96
+ Status::INTERNAL => 'Internal Server Error',
97
+ }
98
+
99
+ CharsetMap = {
100
+ 'NONE' => 'us-ascii',
101
+ 'EUC' => 'euc-jp',
102
+ 'SJIS' => 'shift_jis',
103
+ 'UTF8' => 'utf-8',
104
+ }
105
+
106
+ # SYNOPSIS
107
+ # HTTP::Message.new
108
+ #
109
+ # ARGS
110
+ # N/A
111
+ #
112
+ # DESCRIPTION
113
+ # Create a instance of HTTP request or HTTP response. Specify
114
+ # status_code for HTTP response.
115
+ #
116
+ def initialize
117
+ @is_request = nil # true, false and nil
118
+ @http_version = 'HTTP/1.1'
119
+ @body_type = nil
120
+ @body_charset = nil
121
+ @body_size = nil
122
+ @body_date = nil
123
+ @header_item = []
124
+ @chunked = false
125
+ @response_status_code = nil
126
+ @reason_phrase = nil
127
+ @request_method = nil
128
+ @request_uri = nil
129
+ @request_query = nil
130
+ @request_via_proxy = nil
131
+ end
132
+
133
+ def init_request(method, uri, query = nil, via_proxy = nil)
134
+ @is_request = true
135
+ @request_method = method
136
+ @request_uri = if uri.is_a?(URI)
137
+ uri
138
+ else
139
+ URI.parse(uri.to_s)
140
+ end
141
+ @request_query = create_query_uri(@request_uri, query)
142
+ @request_via_proxy = via_proxy
143
+ end
144
+
145
+ def init_response(status_code)
146
+ @is_request = false
147
+ self.response_status_code = status_code
148
+ end
149
+
150
+ attr_accessor :request_via_proxy
151
+
152
+ attr_reader :response_status_code
153
+ def response_status_code=(status_code)
154
+ @response_status_code = status_code
155
+ @reason_phrase = StatusCodeMap[@response_status_code]
156
+ end
157
+
158
+ def contenttype
159
+ self['content-type'][0]
160
+ end
161
+
162
+ def contenttype=(contenttype)
163
+ self['content-type'] = contenttype
164
+ end
165
+
166
+ # body_size == nil means that the body is_a? IO
167
+ def body_size=(body_size)
168
+ @body_size = body_size
169
+ if @body_size
170
+ @chunked = false
171
+ else
172
+ @chunked = true
173
+ end
174
+ end
175
+
176
+ def dump(dev = '')
177
+ set_header
178
+ if @is_request
179
+ dev << request_line
180
+ else
181
+ dev << response_status_line
182
+ end
183
+ dev << @header_item.collect { |key, value|
184
+ dump_line("#{ key }: #{ value }")
185
+ }.join
186
+ dev
187
+ end
188
+
189
+ def set(key, value)
190
+ @header_item.push([key, value])
191
+ end
192
+
193
+ def get(key = nil)
194
+ if !key
195
+ @header_item
196
+ else
197
+ @header_item.find_all { |pair| pair[0].upcase == key.upcase }
198
+ end
199
+ end
200
+
201
+ def []=(key, value)
202
+ set(key, value)
203
+ end
204
+
205
+ def [](key)
206
+ get(key).collect { |item| item[1] }
207
+ end
208
+
209
+ private
210
+
211
+ def request_line
212
+ path = if @request_via_proxy
213
+ if @request_uri.port
214
+ "#{ @request_uri.scheme }://#{ @request_uri.host }:#{ @request_uri.port }#{ @request_query }"
215
+ else
216
+ "#{ @request_uri.scheme }://#{ @request_uri.host }#{ @request_query }"
217
+ end
218
+ else
219
+ @request_query
220
+ end
221
+ dump_line("#{ @request_method } #{ path } #{ @http_version }")
222
+ end
223
+
224
+ def response_status_line
225
+ if defined?(Apache)
226
+ dump_line("#{ @http_version } #{ response_status_code } #{ @reason_phrase }")
227
+ else
228
+ dump_line("Status: #{ response_status_code } #{ @reason_phrase }")
229
+ end
230
+ end
231
+
232
+ def set_header
233
+ if defined?(Apache)
234
+ set('Date', HTTP.http_date(Time.now))
235
+ end
236
+
237
+ keep_alive = HTTP.keep_alive_enabled?(@http_version)
238
+ set('Connection', 'close') unless keep_alive
239
+
240
+ if @chunked
241
+ set('Transfer-Encoding', 'chunked')
242
+ else
243
+ if keep_alive or @body_size != 0
244
+ set('Content-Length', @body_size.to_s)
245
+ end
246
+ end
247
+
248
+ if @body_date
249
+ set('Last-Modified', HTTP.http_date(@body_date))
250
+ end
251
+
252
+ if @is_request == true
253
+ if @http_version >= 'HTTP/1.1'
254
+ if @request_uri.port == @request_uri.default_port
255
+ set('Host', "#{@request_uri.host}")
256
+ else
257
+ set('Host', "#{@request_uri.host}:#{@request_uri.port}")
258
+ end
259
+ end
260
+ elsif @is_request == false
261
+ set('Content-Type', "#{ @body_type || 'text/html' }; charset=#{ CharsetMap[@body_charset || $KCODE] }")
262
+ end
263
+ end
264
+
265
+ def dump_line(str)
266
+ str + CRLF
267
+ end
268
+
269
+ def create_query_uri(uri, query)
270
+ path = uri.path.dup
271
+ path = '/' if path.empty?
272
+ query_str = nil
273
+ if uri.query
274
+ query_str = uri.query
275
+ end
276
+ if query
277
+ if query_str
278
+ query_str << '&' << Message.create_query_part_str(query)
279
+ else
280
+ query_str = Message.create_query_part_str(query)
281
+ end
282
+ end
283
+ if query_str
284
+ path << '?' << query_str
285
+ end
286
+ path
287
+ end
288
+ end
289
+
290
+ class Body
291
+ attr_accessor :type, :charset, :date, :chunk_size
292
+
293
+ def initialize(body = nil, date = nil, type = nil, charset = nil,
294
+ boundary = nil)
295
+ @body = nil
296
+ @boundary = boundary
297
+ set_content(body || '', boundary)
298
+ @type = type
299
+ @charset = charset
300
+ @date = date
301
+ @chunk_size = 4096
302
+ end
303
+
304
+ def size
305
+ if @body.respond_to?(:read)
306
+ nil
307
+ else
308
+ @body.size
309
+ end
310
+ end
311
+
312
+ def dump(dev = '')
313
+ if @body.respond_to?(:read)
314
+ begin
315
+ while true
316
+ chunk = @body.read(@chunk_size)
317
+ break if chunk.nil?
318
+ dev << dump_chunk(chunk)
319
+ end
320
+ rescue EOFError
321
+ end
322
+ dev << (dump_last_chunk + CRLF)
323
+ else
324
+ dev << @body
325
+ end
326
+ dev
327
+ end
328
+
329
+ def content
330
+ @body
331
+ end
332
+
333
+ def set_content(body, boundary = nil)
334
+ if body.respond_to?(:read)
335
+ @body = body
336
+ elsif boundary
337
+ @body = Message.create_query_multipart_str(body, boundary)
338
+ else
339
+ @body = Message.create_query_part_str(body)
340
+ end
341
+ end
342
+
343
+ private
344
+
345
+ def dump_chunk(str)
346
+ dump_chunk_size(str.size) << (str + CRLF)
347
+ end
348
+
349
+ def dump_last_chunk
350
+ dump_chunk_size(0)
351
+ end
352
+
353
+ def dump_chunk_size(size)
354
+ sprintf("%x", size) << CRLF
355
+ end
356
+ end
357
+
358
+ def initialize
359
+ @body = @header = nil
360
+ end
361
+
362
+ class << self
363
+ alias __new new
364
+ undef new
365
+ end
366
+
367
+ def self.new_request(method, uri, query = nil, body = nil, proxy = nil,
368
+ boundary = nil)
369
+ m = self.__new
370
+ m.header = Headers.new
371
+ m.header.init_request(method, uri, query, proxy)
372
+ m.body = Body.new(body, nil, nil, nil, boundary)
373
+ m
374
+ end
375
+
376
+ def self.new_response(body = '')
377
+ m = self.__new
378
+ m.header = Headers.new
379
+ m.header.init_response(Status::OK)
380
+ m.body = Body.new(body)
381
+ m
382
+ end
383
+
384
+ def dump(dev = '')
385
+ sync_header
386
+ dev = header.dump(dev)
387
+ dev << CRLF
388
+ dev = body.dump(dev) if body
389
+ dev
390
+ end
391
+
392
+ def load(str)
393
+ buf = str.dup
394
+ unless self.header.load(buf)
395
+ self.body.load(buf)
396
+ end
397
+ end
398
+
399
+ def header
400
+ @header
401
+ end
402
+
403
+ def header=(header)
404
+ @header = header
405
+ sync_body
406
+ end
407
+
408
+ def content
409
+ @body.content
410
+ end
411
+
412
+ def body
413
+ @body
414
+ end
415
+
416
+ def body=(body)
417
+ @body = body
418
+ sync_header
419
+ end
420
+
421
+ def status
422
+ @header.response_status_code
423
+ end
424
+
425
+ def status=(status)
426
+ @header.response_status_code = status
427
+ end
428
+
429
+ def version
430
+ @header.http_version
431
+ end
432
+
433
+ def version=(version)
434
+ @header.http_version = version
435
+ end
436
+
437
+ def reason
438
+ @header.reason_phrase
439
+ end
440
+
441
+ def reason=(reason)
442
+ @header.reason_phrase = reason
443
+ end
444
+
445
+ def contenttype
446
+ @header.contenttype
447
+ end
448
+
449
+ def contenttype=(contenttype)
450
+ @header.contenttype = contenttype
451
+ end
452
+
453
+ class << self
454
+ def create_query_part_str(query)
455
+ if multiparam_query?(query)
456
+ escape_query(query)
457
+ else
458
+ query.to_s
459
+ end
460
+ end
461
+
462
+ def create_query_multipart_str(query, boundary)
463
+ if multiparam_query?(query)
464
+ query.collect { |attr, value|
465
+ value ||= ''
466
+ if value.is_a? File
467
+ params = {
468
+ 'filename' => value.path,
469
+ # Creation time is not available from File::Stat
470
+ # 'creation-date' => value.ctime.rfc822,
471
+ 'modification-date' => value.mtime.rfc822,
472
+ 'read-date' => value.atime.rfc822,
473
+ }
474
+ param_str = params.to_a.collect { |k, v|
475
+ "#{k}=\"#{v}\""
476
+ }.join("; ")
477
+ "--#{boundary}\n" +
478
+ %{Content-Disposition: form-data; name="#{attr.to_s}"; #{param_str}\n} +
479
+ "Content-Type: #{mime_type(value.path)}\n\n#{value.read}\n"
480
+ else
481
+ "--#{boundary}\n" +
482
+ %{Content-Disposition: form-data; name="#{attr.to_s}"\n} +
483
+ "\n#{value.to_s}\n"
484
+ end
485
+ }.join('') + "--#{boundary}--\n"
486
+ else
487
+ query.to_s
488
+ end
489
+ end
490
+
491
+ def multiparam_query?(query)
492
+ query.is_a?(Array) or query.is_a?(Hash)
493
+ end
494
+
495
+ def escape_query(query)
496
+ query.collect { |attr, value|
497
+ escape(attr.to_s) << '=' << escape(value.to_s)
498
+ }.join('&')
499
+ end
500
+
501
+ # from CGI.escape
502
+ def escape(str)
503
+ str.gsub(/([^ a-zA-Z0-9_.-]+)/n) {
504
+ '%' + $1.unpack('H2' * $1.size).join('%').upcase
505
+ }.tr(' ', '+')
506
+ end
507
+
508
+ def mime_type(path)
509
+ case path
510
+ when /.(htm|html)$/
511
+ 'text/html'
512
+ when /.doc$/
513
+ 'application/msword'
514
+ else
515
+ 'text/plain'
516
+ end
517
+ end
518
+ end
519
+
520
+ private
521
+
522
+ def sync_header
523
+ if @header and @body
524
+ @header.body_type = @body.type
525
+ @header.body_charset = @body.charset
526
+ @header.body_size = @body.size
527
+ @header.body_date = @body.date
528
+ end
529
+ end
530
+
531
+ def sync_body
532
+ if @header and @body
533
+ @body.type = @header.body_type
534
+ @body.charset = @header.body_charset
535
+ @body.size = @header.body_size
536
+ @body.date = @header.body_date
537
+ end
538
+ end
539
+ end
540
+
541
+
542
+ end