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