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)}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
|
---|
36 |
|
---|
37 | raise "abnormal path `#{path}'" if %r{/\.\.(/|\Z)} =~ ret
|
---|
38 | ret
|
---|
39 | end
|
---|
40 | module_function :normalize_path
|
---|
41 |
|
---|
42 | #####
|
---|
43 |
|
---|
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 | }
|
---|
99 |
|
---|
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
|
---|
116 |
|
---|
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
|
---|
123 |
|
---|
124 | #####
|
---|
125 |
|
---|
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
|
---|
155 |
|
---|
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
|
---|
161 |
|
---|
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
|
---|
176 |
|
---|
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
|
---|
194 |
|
---|
195 | #####
|
---|
196 |
|
---|
197 | def dequote(str)
|
---|
198 | ret = (/\A"(.*)"\Z/ =~ str) ? $1 : str.dup
|
---|
199 | ret.gsub!(/\\(.)/, "\\1")
|
---|
200 | ret
|
---|
201 | end
|
---|
202 | module_function :dequote
|
---|
203 |
|
---|
204 | def quote(str)
|
---|
205 | '"' << str.gsub(/[\\\"]/o, "\\\1") << '"'
|
---|
206 | end
|
---|
207 | module_function :quote
|
---|
208 |
|
---|
209 | #####
|
---|
210 |
|
---|
211 | class FormData < String
|
---|
212 | EmptyRawHeader = [].freeze
|
---|
213 | EmptyHeader = {}.freeze
|
---|
214 |
|
---|
215 | attr_accessor :name, :filename, :next_data
|
---|
216 | protected :next_data
|
---|
217 |
|
---|
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
|
---|
233 |
|
---|
234 | def [](*key)
|
---|
235 | begin
|
---|
236 | @header[key[0].downcase].join(", ")
|
---|
237 | rescue StandardError, NameError
|
---|
238 | super
|
---|
239 | end
|
---|
240 | end
|
---|
241 |
|
---|
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
|
---|
256 |
|
---|
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
|
---|
268 |
|
---|
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
|
---|
277 |
|
---|
278 | def list
|
---|
279 | ret = []
|
---|
280 | each_data{|data|
|
---|
281 | ret << data.to_s
|
---|
282 | }
|
---|
283 | ret
|
---|
284 | end
|
---|
285 |
|
---|
286 | alias :to_ary :list
|
---|
287 |
|
---|
288 | def to_s
|
---|
289 | String.new(self)
|
---|
290 | end
|
---|
291 | end
|
---|
292 |
|
---|
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
|
---|
313 |
|
---|
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
|
---|
341 |
|
---|
342 | #####
|
---|
343 |
|
---|
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
|
---|
355 |
|
---|
356 | module_function
|
---|
357 |
|
---|
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
|
---|
362 |
|
---|
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+":@&=+$,")
|
---|
368 |
|
---|
369 | def escape(str)
|
---|
370 | _escape(str, UNESCAPED)
|
---|
371 | end
|
---|
372 |
|
---|
373 | def unescape(str)
|
---|
374 | _unescape(str, ESCAPED)
|
---|
375 | end
|
---|
376 |
|
---|
377 | def escape_form(str)
|
---|
378 | ret = _escape(str, UNESCAPED_FORM)
|
---|
379 | ret.gsub!(/ /, "+")
|
---|
380 | ret
|
---|
381 | end
|
---|
382 |
|
---|
383 | def unescape_form(str)
|
---|
384 | _unescape(str.gsub(/\+/, " "), ESCAPED)
|
---|
385 | end
|
---|
386 |
|
---|
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
|
---|
394 |
|
---|
395 | def escape8bit(str)
|
---|
396 | _escape(str, NONASCII)
|
---|
397 | end
|
---|
398 | end
|
---|
399 | end
|
---|