[18425] | 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
|
---|