barx 0.1.0

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