Updating tags for StringIO.
[rbx.git] / lib / webrick / httputils.rb
blobc57af2c860277fad4e8b3e1814fdc87d58b3d559
2 # httputils.rb -- HTTPUtils Module
4 # Author: IPR -- Internet Programming with Ruby -- writers
5 # Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou
6 # Copyright (c) 2002 Internet Programming with Ruby writers. All rights
7 # reserved.
9 # $IPR: httputils.rb,v 1.34 2003/06/05 21:34:08 gotoyuzo Exp $
11 require 'socket'
12 require 'tempfile'
14 module WEBrick
15   CR   = "\x0d"
16   LF   = "\x0a"
17   CRLF = "\x0d\x0a"
19   module HTTPUtils
21     def normalize_path(path)
22       raise "abnormal path `#{path}'" if path[0] != ?/
23       ret = path.dup
25       ret.gsub!(%r{/+}o, '/')                    # //      => /
26       while ret.sub!(%r{/\.(/|\Z)}o, '/'); end   # /.      => /
27       begin                                      # /foo/.. => /foo
28         match = ret.sub!(%r{/([^/]+)/\.\.(/|\Z)}o){
29           if $1 == ".."
30             raise "abnormal path `#{path}'"
31           else
32             "/"
33           end
34         }
35       end while match
37       raise "abnormal path `#{path}'" if %r{/\.\.(/|\Z)} =~ ret
38       ret
39     end
40     module_function :normalize_path
42     #####
44     DefaultMimeTypes = {
45       "ai"    => "application/postscript",
46       "asc"   => "text/plain",
47       "avi"   => "video/x-msvideo",
48       "bin"   => "application/octet-stream",
49       "bmp"   => "image/bmp",
50       "class" => "application/octet-stream",
51       "cer"   => "application/pkix-cert",
52       "crl"   => "application/pkix-crl",
53       "crt"   => "application/x-x509-ca-cert",
54      #"crl"   => "application/x-pkcs7-crl",
55       "css"   => "text/css",
56       "dms"   => "application/octet-stream",
57       "doc"   => "application/msword",
58       "dvi"   => "application/x-dvi",
59       "eps"   => "application/postscript",
60       "etx"   => "text/x-setext",
61       "exe"   => "application/octet-stream",
62       "gif"   => "image/gif",
63       "htm"   => "text/html",
64       "html"  => "text/html",
65       "jpe"   => "image/jpeg",
66       "jpeg"  => "image/jpeg",
67       "jpg"   => "image/jpeg",
68       "lha"   => "application/octet-stream",
69       "lzh"   => "application/octet-stream",
70       "mov"   => "video/quicktime",
71       "mpe"   => "video/mpeg",
72       "mpeg"  => "video/mpeg",
73       "mpg"   => "video/mpeg",
74       "pbm"   => "image/x-portable-bitmap",
75       "pdf"   => "application/pdf",
76       "pgm"   => "image/x-portable-graymap",
77       "png"   => "image/png",
78       "pnm"   => "image/x-portable-anymap",
79       "ppm"   => "image/x-portable-pixmap",
80       "ppt"   => "application/vnd.ms-powerpoint",
81       "ps"    => "application/postscript",
82       "qt"    => "video/quicktime",
83       "ras"   => "image/x-cmu-raster",
84       "rb"    => "text/plain",
85       "rd"    => "text/plain",
86       "rtf"   => "application/rtf",
87       "sgm"   => "text/sgml",
88       "sgml"  => "text/sgml",
89       "tif"   => "image/tiff",
90       "tiff"  => "image/tiff",
91       "txt"   => "text/plain",
92       "xbm"   => "image/x-xbitmap",
93       "xls"   => "application/vnd.ms-excel",
94       "xml"   => "text/xml",
95       "xpm"   => "image/x-xpixmap",
96       "xwd"   => "image/x-xwindowdump",
97       "zip"   => "application/zip",
98     }
100     # Load Apache compatible mime.types file.
101     def load_mime_types(file)
102       open(file){ |io|
103         hash = Hash.new
104         io.each{ |line|
105           next if /^#/ =~ line
106           line.chomp!
107           mimetype, ext0 = line.split(/\s+/, 2)
108           next unless ext0   
109           next if ext0.empty?
110           ext0.split(/\s+/).each{ |ext| hash[ext] = mimetype }
111         }
112         hash
113       }
114     end
115     module_function :load_mime_types
117     def mime_type(filename, mime_tab)
118       suffix1 = (/\.(\w+)$/ =~ filename && $1.downcase)
119       suffix2 = (/\.(\w+)\.[\w\-]+$/ =~ filename && $1.downcase)
120       mime_tab[suffix1] || mime_tab[suffix2] || "application/octet-stream"
121     end
122     module_function :mime_type
124     #####
126     def parse_header(raw)
127       header = Hash.new([].freeze)
128       field = nil
129       raw.each{|line|
130         case line
131         when /^([A-Za-z0-9!\#$%&'*+\-.^_`|~]+):\s*(.*?)\s*\z/om
132           field, value = $1, $2
133           field.downcase!
134           header[field] = [] unless header.has_key?(field)
135           header[field] << value
136         when /^\s+(.*?)\s*\z/om
137           value = $1
138           unless field
139             raise "bad header '#{line.inspect}'."
140           end
141           header[field][-1] << " " << value
142         else
143           raise "bad header '#{line.inspect}'."
144         end
145       }
146       header.each{|key, values|
147         values.each{|value|
148           value.strip!
149           value.gsub!(/\s+/, " ")
150         }
151       }
152       header
153     end
154     module_function :parse_header
156     def split_header_value(str)
157       str.scan(/((?:"(?:\\.|[^"])+?"|[^",]+)+)
158                 (?:,\s*|\Z)/xn).collect{|v| v[0] }
159     end
160     module_function :split_header_value
162     def parse_range_header(ranges_specifier)
163       if /^bytes=(.*)/ =~ ranges_specifier
164         byte_range_set = split_header_value($1)
165         byte_range_set.collect{|range_spec|
166           case range_spec
167           when /^(\d+)-(\d+)/ then $1.to_i .. $2.to_i
168           when /^(\d+)-/      then $1.to_i .. -1
169           when /^-(\d+)/      then -($1.to_i) .. -1
170           else return nil
171           end
172         }
173       end
174     end
175     module_function :parse_range_header
177     def parse_qvalues(value)
178       tmp = []
179       if value
180         parts = value.split(/,\s*/)
181         parts.each {|part|
182           if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
183             val = m[1]
184             q = (m[2] or 1).to_f
185             tmp.push([val, q])
186           end
187         }
188         tmp = tmp.sort_by{|val, q| -q}
189         tmp.collect!{|val, q| val}
190       end
191       return tmp
192     end
193     module_function :parse_qvalues
195     #####
197     def dequote(str)
198       ret = (/\A"(.*)"\Z/ =~ str) ? $1 : str.dup
199       ret.gsub!(/\\(.)/, "\\1")
200       ret
201     end
202     module_function :dequote
204     def quote(str)
205       '"' << str.gsub(/[\\\"]/o, "\\\1") << '"'
206     end
207     module_function :quote
209     #####
211     class FormData < String
212       EmptyRawHeader = [].freeze
213       EmptyHeader = {}.freeze
215       attr_accessor :name, :filename, :next_data
216       protected :next_data
218       def initialize(*args)
219         @name = @filename = @next_data = nil
220         if args.empty?
221           @raw_header = []
222           @header = nil
223           super("")
224         else
225           @raw_header = EmptyRawHeader
226           @header = EmptyHeader 
227           super(args.shift)
228           unless args.empty?
229             @next_data = self.class.new(*args)
230           end
231         end
232       end
234       def [](*key)
235         begin
236           @header[key[0].downcase].join(", ")
237         rescue StandardError, NameError
238           super
239         end
240       end
242       def <<(str)
243         if @header
244           super
245         elsif str == CRLF
246           @header = HTTPUtils::parse_header(@raw_header)
247           if cd = self['content-disposition']
248             if /\s+name="(.*?)"/ =~ cd then @name = $1 end
249             if /\s+filename="(.*?)"/ =~ cd then @filename = $1 end
250           end
251         else
252           @raw_header << str
253         end
254         self
255       end
257       def append_data(data)
258         tmp = self
259         while tmp
260           unless tmp.next_data 
261             tmp.next_data = data
262             break
263           end
264           tmp = tmp.next_data
265         end
266         self
267       end
269       def each_data
270         tmp = self
271         while tmp
272           next_data = tmp.next_data
273           yield(tmp)
274           tmp = next_data
275         end
276       end
278       def list
279         ret = []
280         each_data{|data|
281           ret << data.to_s
282         }
283         ret
284       end
286       alias :to_ary :list
288       def to_s
289         String.new(self)
290       end
291     end
293     def parse_query(str)
294       query = Hash.new
295       if str
296         str.split(/[&;]/).each{|x|
297           next if x.empty? 
298           key, val = x.split(/=/,2)
299           key = unescape_form(key)
300           val = unescape_form(val.to_s)
301           val = FormData.new(val)
302           val.name = key
303           if query.has_key?(key)
304             query[key].append_data(val)
305             next
306           end
307           query[key] = val
308         }
309       end
310       query
311     end
312     module_function :parse_query
314     def parse_form_data(io, boundary)
315       boundary_regexp = /\A--#{boundary}(--)?#{CRLF}\z/
316       form_data = Hash.new
317       return form_data unless io
318       data = nil
319       io.each{|line|
320         if boundary_regexp =~ line
321           if data
322             data.chop!
323             key = data.name
324             if form_data.has_key?(key)
325               form_data[key].append_data(data)
326             else
327               form_data[key] = data 
328             end
329           end
330           data = FormData.new
331           next
332         else
333           if data
334             data << line
335           end
336         end
337       }
338       return form_data
339     end
340     module_function :parse_form_data
342     #####
344     reserved = ';/?:@&=+$,'
345     num      = '0123456789'
346     lowalpha = 'abcdefghijklmnopqrstuvwxyz'
347     upalpha  = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
348     mark     = '-_.!~*\'()'
349     unreserved = num + lowalpha + upalpha + mark
350     control  = (0x0..0x1f).collect{|c| c.chr }.join + "\x7f"
351     space    = " "
352     delims   = '<>#%"'
353     unwise   = '{}|\\^[]`'
354     nonascii = (0x80..0xff).collect{|c| c.chr }.join
356     module_function
358     def _make_regex(str) /([#{Regexp.escape(str)}])/n end
359     def _make_regex!(str) /([^#{Regexp.escape(str)}])/n end
360     def _escape(str, regex) str.gsub(regex){ "%%%02X" % $1[0] } end
361     def _unescape(str, regex) str.gsub(regex){ $1.hex.chr } end
363     UNESCAPED = _make_regex(control+space+delims+unwise+nonascii)
364     UNESCAPED_FORM = _make_regex(reserved+control+delims+unwise+nonascii)
365     NONASCII  = _make_regex(nonascii)
366     ESCAPED   = /%([0-9a-fA-F]{2})/
367     UNESCAPED_PCHAR = _make_regex!(unreserved+":@&=+$,")
369     def escape(str)
370       _escape(str, UNESCAPED)
371     end
373     def unescape(str)
374       _unescape(str, ESCAPED)
375     end
377     def escape_form(str)
378       ret = _escape(str, UNESCAPED_FORM)
379       ret.gsub!(/ /, "+")
380       ret
381     end
383     def unescape_form(str)
384       _unescape(str.gsub(/\+/, " "), ESCAPED)
385     end
387     def escape_path(str)
388       result = ""
389       str.scan(%r{/([^/]*)}).each{|i|
390         result << "/" << _escape(i[0], UNESCAPED_PCHAR)
391       }
392       return result
393     end
395     def escape8bit(str)
396       _escape(str, NONASCII)
397     end
398   end