File: webrick/httputils.rb

Overview
Module Structure
Class Hierarchy
Code

Overview

Module Structure

  module: <Toplevel Module>
  module: WEBrick#14
has properties
constant: CR #15
constant: LF #16
constant: CRLF #17
  module: HTTPUtils#19
has properties
function: normalize_path / 1 #21
constant: DefaultMimeTypes #36
function: load_mime_types / 1 #93
function: mime_type / 2 #109
function: parse_header / 1 #118
function: split_header_value / 1 #148
function: parse_range_header / 1 #154
function: parse_qvalues / 1 #169
function: dequote / 1 #189
function: quote / 1 #196
function: parse_query / 1 #285
function: parse_form_data / 2 #306
function: _make_regex / 1 #350
method: _make_regex! / 1 #351
method: _escape / 2 #352
method: _unescape / 2 #353
constant: UNESCAPED #355
constant: UNESCAPED_FORM #356
constant: NONASCII #357
constant: ESCAPED #358
constant: UNESCAPED_PCHAR #359
method: escape / 1 #361
method: unescape / 1 #365
method: escape_form / 1 #369
method: unescape_form / 1 #375
method: escape_path / 1 #379
method: escape8bit / 1 #387
  class: FormData#203
inherits from
  String ( Builtin-Module )
has properties
constant: EmptyRawHeader #204
constant: EmptyHeader #205
attribute: name [RW] #207
attribute: filename [RW] #207
attribute: next_data [RW] #207
method: initialize / 1 #210
method: [] / 1 #226
method: << / 1 #234
method: append_data / 1 #249
method: each_data #261
method: list #270
alias: to_ary list #278
method: to_s #280

Class Hierarchy

Code

   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