This commit is contained in:
@@ -0,0 +1,543 @@
|
||||
# frozen_string_literal: true
|
||||
#
|
||||
# httputils.rb -- HTTPUtils Module
|
||||
#
|
||||
# Author: IPR -- Internet Programming with Ruby -- writers
|
||||
# Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou
|
||||
# Copyright (c) 2002 Internet Programming with Ruby writers. All rights
|
||||
# reserved.
|
||||
#
|
||||
# $IPR: httputils.rb,v 1.34 2003/06/05 21:34:08 gotoyuzo Exp $
|
||||
|
||||
require 'socket'
|
||||
require 'tempfile'
|
||||
|
||||
module WEBrick
|
||||
CR = "\x0d" # :nodoc:
|
||||
LF = "\x0a" # :nodoc:
|
||||
CRLF = "\x0d\x0a" # :nodoc:
|
||||
|
||||
##
|
||||
# HTTPUtils provides utility methods for working with the HTTP protocol.
|
||||
#
|
||||
# This module is generally used internally by WEBrick
|
||||
|
||||
module HTTPUtils
|
||||
|
||||
##
|
||||
# Normalizes a request path. Raises an exception if the path cannot be
|
||||
# normalized.
|
||||
|
||||
def normalize_path(path)
|
||||
raise "abnormal path '#{path}'" if path[0] != ?/
|
||||
ret = path.dup
|
||||
|
||||
ret.gsub!(%r{/+}o, '/') # // => /
|
||||
while ret.sub!(%r'/\.(?:/|\Z)', '/'); end # /. => /
|
||||
while ret.sub!(%r'/(?!\.\./)[^/]+/\.\.(?:/|\Z)', '/'); end # /foo/.. => /foo
|
||||
|
||||
raise "abnormal path '#{path}'" if %r{/\.\.(/|\Z)} =~ ret
|
||||
ret
|
||||
end
|
||||
module_function :normalize_path
|
||||
|
||||
##
|
||||
# Default mime types
|
||||
|
||||
DefaultMimeTypes = {
|
||||
"ai" => "application/postscript",
|
||||
"asc" => "text/plain",
|
||||
"avi" => "video/x-msvideo",
|
||||
"avif" => "image/avif",
|
||||
"bin" => "application/octet-stream",
|
||||
"bmp" => "image/bmp",
|
||||
"class" => "application/octet-stream",
|
||||
"cer" => "application/pkix-cert",
|
||||
"crl" => "application/pkix-crl",
|
||||
"crt" => "application/x-x509-ca-cert",
|
||||
"css" => "text/css",
|
||||
"dms" => "application/octet-stream",
|
||||
"doc" => "application/msword",
|
||||
"dvi" => "application/x-dvi",
|
||||
"eps" => "application/postscript",
|
||||
"etx" => "text/x-setext",
|
||||
"exe" => "application/octet-stream",
|
||||
"gif" => "image/gif",
|
||||
"htm" => "text/html",
|
||||
"html" => "text/html",
|
||||
"ico" => "image/x-icon",
|
||||
"jpe" => "image/jpeg",
|
||||
"jpeg" => "image/jpeg",
|
||||
"jpg" => "image/jpeg",
|
||||
"js" => "application/javascript",
|
||||
"json" => "application/json",
|
||||
"lha" => "application/octet-stream",
|
||||
"lzh" => "application/octet-stream",
|
||||
"mjs" => "application/javascript",
|
||||
"mov" => "video/quicktime",
|
||||
"mp4" => "video/mp4",
|
||||
"mpe" => "video/mpeg",
|
||||
"mpeg" => "video/mpeg",
|
||||
"mpg" => "video/mpeg",
|
||||
"otf" => "font/otf",
|
||||
"pbm" => "image/x-portable-bitmap",
|
||||
"pdf" => "application/pdf",
|
||||
"pgm" => "image/x-portable-graymap",
|
||||
"png" => "image/png",
|
||||
"pnm" => "image/x-portable-anymap",
|
||||
"ppm" => "image/x-portable-pixmap",
|
||||
"ppt" => "application/vnd.ms-powerpoint",
|
||||
"ps" => "application/postscript",
|
||||
"qt" => "video/quicktime",
|
||||
"ras" => "image/x-cmu-raster",
|
||||
"rb" => "text/plain",
|
||||
"rd" => "text/plain",
|
||||
"rtf" => "application/rtf",
|
||||
"sgm" => "text/sgml",
|
||||
"sgml" => "text/sgml",
|
||||
"svg" => "image/svg+xml",
|
||||
"tif" => "image/tiff",
|
||||
"tiff" => "image/tiff",
|
||||
"ttc" => "font/collection",
|
||||
"ttf" => "font/ttf",
|
||||
"txt" => "text/plain",
|
||||
"wasm" => "application/wasm",
|
||||
"webm" => "video/webm",
|
||||
"webmanifest" => "application/manifest+json",
|
||||
"webp" => "image/webp",
|
||||
"woff" => "font/woff",
|
||||
"woff2" => "font/woff2",
|
||||
"xbm" => "image/x-xbitmap",
|
||||
"xhtml" => "text/html",
|
||||
"xls" => "application/vnd.ms-excel",
|
||||
"xml" => "text/xml",
|
||||
"xpm" => "image/x-xpixmap",
|
||||
"xwd" => "image/x-xwindowdump",
|
||||
"zip" => "application/zip",
|
||||
}
|
||||
|
||||
##
|
||||
# Loads Apache-compatible mime.types in +file+.
|
||||
|
||||
def load_mime_types(file)
|
||||
# note: +file+ may be a "| command" for now; some people may
|
||||
# rely on this, but currently we do not use this method by default.
|
||||
File.open(file){ |io|
|
||||
hash = Hash.new
|
||||
io.each{ |line|
|
||||
next if /^#/ =~ line
|
||||
line.chomp!
|
||||
mimetype, ext0 = line.split(/\s+/, 2)
|
||||
next unless ext0
|
||||
next if ext0.empty?
|
||||
ext0.split(/\s+/).each{ |ext| hash[ext] = mimetype }
|
||||
}
|
||||
hash
|
||||
}
|
||||
end
|
||||
module_function :load_mime_types
|
||||
|
||||
##
|
||||
# Returns the mime type of +filename+ from the list in +mime_tab+. If no
|
||||
# mime type was found application/octet-stream is returned.
|
||||
|
||||
def mime_type(filename, mime_tab)
|
||||
suffix1 = (/\.(\w+)$/ =~ filename && $1.downcase)
|
||||
suffix2 = (/\.(\w+)\.[\w\-]+$/ =~ filename && $1.downcase)
|
||||
mime_tab[suffix1] || mime_tab[suffix2] || "application/octet-stream"
|
||||
end
|
||||
module_function :mime_type
|
||||
|
||||
##
|
||||
# Parses an HTTP header +raw+ into a hash of header fields with an Array
|
||||
# of values.
|
||||
|
||||
class SplitHeader < Array
|
||||
def join(separator = ", ")
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
class CookieHeader < Array
|
||||
def join(separator = "; ")
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
HEADER_CLASSES = Hash.new(SplitHeader).update({
|
||||
"cookie" => CookieHeader,
|
||||
})
|
||||
|
||||
def parse_header(raw)
|
||||
header = Hash.new([].freeze)
|
||||
field = nil
|
||||
raw.each_line{|line|
|
||||
case line
|
||||
when /^([A-Za-z0-9!\#$%&'*+\-.^_`|~]+):([^\r\n\0]*?)\r\n\z/om
|
||||
field, value = $1, $2
|
||||
field.downcase!
|
||||
header[field] = HEADER_CLASSES[field].new unless header.has_key?(field)
|
||||
header[field] << value
|
||||
when /^[ \t]+([^\r\n\0]*?)\r\n/om
|
||||
unless field
|
||||
raise HTTPStatus::BadRequest, "bad header '#{line}'."
|
||||
end
|
||||
value = line
|
||||
value.gsub!(/\A[ \t]+/, '')
|
||||
value.slice!(-2..-1)
|
||||
header[field][-1] << " " << value
|
||||
else
|
||||
raise HTTPStatus::BadRequest, "bad header '#{line}'."
|
||||
end
|
||||
}
|
||||
header.each{|key, values|
|
||||
values.each{|value|
|
||||
value.gsub!(/\A[ \t]+/, '')
|
||||
value.gsub!(/[ \t]+\z/, '')
|
||||
}
|
||||
}
|
||||
header
|
||||
end
|
||||
module_function :parse_header
|
||||
|
||||
##
|
||||
# Splits a header value +str+ according to HTTP specification.
|
||||
|
||||
def split_header_value(str)
|
||||
str.scan(%r'\G((?:"(?:\\.|[^"])+?"|[^",]++)+)
|
||||
(?:,[ \t]*|\Z)'xn).flatten
|
||||
end
|
||||
module_function :split_header_value
|
||||
|
||||
##
|
||||
# Parses a Range header value +ranges_specifier+
|
||||
|
||||
def parse_range_header(ranges_specifier)
|
||||
if /^bytes=(.*)/ =~ ranges_specifier
|
||||
byte_range_set = split_header_value($1)
|
||||
byte_range_set.collect{|range_spec|
|
||||
case range_spec
|
||||
when /^(\d+)-(\d+)/ then $1.to_i .. $2.to_i
|
||||
when /^(\d+)-/ then $1.to_i .. -1
|
||||
when /^-(\d+)/ then -($1.to_i) .. -1
|
||||
else return nil
|
||||
end
|
||||
}
|
||||
end
|
||||
end
|
||||
module_function :parse_range_header
|
||||
|
||||
##
|
||||
# Parses q values in +value+ as used in Accept headers.
|
||||
|
||||
def parse_qvalues(value)
|
||||
tmp = []
|
||||
if value
|
||||
parts = value.split(/,[ \t]*/)
|
||||
parts.each {|part|
|
||||
if m = %r{^([^ \t,]+?)(?:;[ \t]*q=(\d+(?:\.\d+)?))?$}.match(part)
|
||||
val = m[1]
|
||||
q = (m[2] or 1).to_f
|
||||
tmp.push([val, q])
|
||||
end
|
||||
}
|
||||
tmp = tmp.sort_by{|val, q| -q}
|
||||
tmp.collect!{|val, q| val}
|
||||
end
|
||||
return tmp
|
||||
end
|
||||
module_function :parse_qvalues
|
||||
|
||||
##
|
||||
# Removes quotes and escapes from +str+
|
||||
|
||||
def dequote(str)
|
||||
ret = (/\A"(.*)"\Z/ =~ str) ? $1 : str.dup
|
||||
ret.gsub!(/\\(.)/, "\\1")
|
||||
ret
|
||||
end
|
||||
module_function :dequote
|
||||
|
||||
##
|
||||
# Quotes and escapes quotes in +str+
|
||||
|
||||
def quote(str)
|
||||
+'"' << str.gsub(/[\\\"]/o, "\\\1") << '"'
|
||||
end
|
||||
module_function :quote
|
||||
|
||||
##
|
||||
# Stores multipart form data. FormData objects are created when
|
||||
# WEBrick::HTTPUtils.parse_form_data is called.
|
||||
|
||||
class FormData < String
|
||||
EmptyRawHeader = [].freeze # :nodoc:
|
||||
EmptyHeader = {}.freeze # :nodoc:
|
||||
|
||||
##
|
||||
# The name of the form data part
|
||||
|
||||
attr_accessor :name
|
||||
|
||||
##
|
||||
# The filename of the form data part
|
||||
|
||||
attr_accessor :filename
|
||||
|
||||
attr_accessor :next_data # :nodoc:
|
||||
protected :next_data
|
||||
|
||||
##
|
||||
# Creates a new FormData object.
|
||||
#
|
||||
# +args+ is an Array of form data entries. One FormData will be created
|
||||
# for each entry.
|
||||
#
|
||||
# This is called by WEBrick::HTTPUtils.parse_form_data for you
|
||||
|
||||
def initialize(*args)
|
||||
@name = @filename = @next_data = nil
|
||||
if args.empty?
|
||||
@raw_header = []
|
||||
@header = nil
|
||||
super("")
|
||||
else
|
||||
@raw_header = EmptyRawHeader
|
||||
@header = EmptyHeader
|
||||
super(args.shift)
|
||||
unless args.empty?
|
||||
@next_data = self.class.new(*args)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
##
|
||||
# Retrieves the header at the first entry in +key+
|
||||
|
||||
def [](*key)
|
||||
begin
|
||||
@header[key[0].downcase].join(", ")
|
||||
rescue StandardError, NameError
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
##
|
||||
# Adds +str+ to this FormData which may be the body, a header or a
|
||||
# header entry.
|
||||
#
|
||||
# This is called by WEBrick::HTTPUtils.parse_form_data for you
|
||||
|
||||
def <<(str)
|
||||
if @header
|
||||
super
|
||||
elsif str == CRLF
|
||||
@header = HTTPUtils::parse_header(@raw_header.join)
|
||||
if cd = self['content-disposition']
|
||||
if /[ \t]+name="(.*?)"/ =~ cd then @name = $1 end
|
||||
if /[ \t]+filename="(.*?)"/ =~ cd then @filename = $1 end
|
||||
end
|
||||
else
|
||||
@raw_header << str
|
||||
end
|
||||
self
|
||||
end
|
||||
|
||||
##
|
||||
# Adds +data+ at the end of the chain of entries
|
||||
#
|
||||
# This is called by WEBrick::HTTPUtils.parse_form_data for you.
|
||||
|
||||
def append_data(data)
|
||||
tmp = self
|
||||
while tmp
|
||||
unless tmp.next_data
|
||||
tmp.next_data = data
|
||||
break
|
||||
end
|
||||
tmp = tmp.next_data
|
||||
end
|
||||
self
|
||||
end
|
||||
|
||||
##
|
||||
# Yields each entry in this FormData
|
||||
|
||||
def each_data
|
||||
tmp = self
|
||||
while tmp
|
||||
next_data = tmp.next_data
|
||||
yield(tmp)
|
||||
tmp = next_data
|
||||
end
|
||||
end
|
||||
|
||||
##
|
||||
# Returns all the FormData as an Array
|
||||
|
||||
def list
|
||||
ret = []
|
||||
each_data{|data|
|
||||
ret << data.to_s
|
||||
}
|
||||
ret
|
||||
end
|
||||
|
||||
##
|
||||
# A FormData will behave like an Array
|
||||
|
||||
alias :to_ary :list
|
||||
|
||||
##
|
||||
# This FormData's body
|
||||
|
||||
def to_s
|
||||
String.new(self)
|
||||
end
|
||||
end
|
||||
|
||||
##
|
||||
# Parses the query component of a URI in +str+
|
||||
|
||||
def parse_query(str)
|
||||
query = Hash.new
|
||||
if str
|
||||
str.split(/[&;]/).each{|x|
|
||||
next if x.empty?
|
||||
key, val = x.split(/=/,2)
|
||||
key = unescape_form(key)
|
||||
val = unescape_form(val.to_s)
|
||||
val = FormData.new(val)
|
||||
val.name = key
|
||||
if query.has_key?(key)
|
||||
query[key].append_data(val)
|
||||
next
|
||||
end
|
||||
query[key] = val
|
||||
}
|
||||
end
|
||||
query
|
||||
end
|
||||
module_function :parse_query
|
||||
|
||||
##
|
||||
# Parses form data in +io+ with the given +boundary+
|
||||
|
||||
def parse_form_data(io, boundary)
|
||||
boundary_regexp = /\A--#{Regexp.quote(boundary)}(--)?#{CRLF}\z/
|
||||
form_data = Hash.new
|
||||
return form_data unless io
|
||||
data = nil
|
||||
io.each_line{|line|
|
||||
if boundary_regexp =~ line
|
||||
if data
|
||||
data.chop!
|
||||
key = data.name
|
||||
if form_data.has_key?(key)
|
||||
form_data[key].append_data(data)
|
||||
else
|
||||
form_data[key] = data
|
||||
end
|
||||
end
|
||||
data = FormData.new
|
||||
next
|
||||
else
|
||||
if data
|
||||
data << line
|
||||
end
|
||||
end
|
||||
}
|
||||
return form_data
|
||||
end
|
||||
module_function :parse_form_data
|
||||
|
||||
#####
|
||||
|
||||
reserved = ';/?:@&=+$,'
|
||||
num = '0123456789'
|
||||
lowalpha = 'abcdefghijklmnopqrstuvwxyz'
|
||||
upalpha = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
mark = '-_.!~*\'()'
|
||||
unreserved = num + lowalpha + upalpha + mark
|
||||
control = (0x0..0x1f).collect{|c| c.chr }.join + "\x7f"
|
||||
space = " "
|
||||
delims = '<>#%"'
|
||||
unwise = '{}|\\^[]`'
|
||||
nonascii = (0x80..0xff).collect{|c| c.chr }.join
|
||||
|
||||
module_function
|
||||
|
||||
# :stopdoc:
|
||||
|
||||
def _make_regex(str) /([#{Regexp.escape(str)}])/n end
|
||||
def _make_regex!(str) /([^#{Regexp.escape(str)}])/n end
|
||||
def _escape(str, regex)
|
||||
str = str.b
|
||||
str.gsub!(regex) {"%%%02X" % $1.ord}
|
||||
# %-escaped string should contain US-ASCII only
|
||||
str.force_encoding(Encoding::US_ASCII)
|
||||
end
|
||||
def _unescape(str, regex)
|
||||
str = str.b
|
||||
str.gsub!(regex) {$1.hex.chr}
|
||||
# encoding of %-unescaped string is unknown
|
||||
str
|
||||
end
|
||||
|
||||
UNESCAPED = _make_regex(control+space+delims+unwise+nonascii)
|
||||
UNESCAPED_FORM = _make_regex(reserved+control+delims+unwise+nonascii)
|
||||
NONASCII = _make_regex(nonascii)
|
||||
ESCAPED = /%([0-9a-fA-F]{2})/
|
||||
UNESCAPED_PCHAR = _make_regex!(unreserved+":@&=+$,")
|
||||
|
||||
# :startdoc:
|
||||
|
||||
##
|
||||
# Escapes HTTP reserved and unwise characters in +str+
|
||||
|
||||
def escape(str)
|
||||
_escape(str, UNESCAPED)
|
||||
end
|
||||
|
||||
##
|
||||
# Unescapes HTTP reserved and unwise characters in +str+
|
||||
|
||||
def unescape(str)
|
||||
_unescape(str, ESCAPED)
|
||||
end
|
||||
|
||||
##
|
||||
# Escapes form reserved characters in +str+
|
||||
|
||||
def escape_form(str)
|
||||
ret = _escape(str, UNESCAPED_FORM)
|
||||
ret.gsub!(/ /, "+")
|
||||
ret
|
||||
end
|
||||
|
||||
##
|
||||
# Unescapes form reserved characters in +str+
|
||||
|
||||
def unescape_form(str)
|
||||
_unescape(str.gsub(/\+/, " "), ESCAPED)
|
||||
end
|
||||
|
||||
##
|
||||
# Escapes path +str+
|
||||
|
||||
def escape_path(str)
|
||||
result = +""
|
||||
str.scan(%r{/([^/]*)}).each{|i|
|
||||
result << "/" << _escape(i[0], UNESCAPED_PCHAR)
|
||||
}
|
||||
return result
|
||||
end
|
||||
|
||||
##
|
||||
# Escapes 8 bit characters in +str+
|
||||
|
||||
def escape8bit(str)
|
||||
_escape(str, NONASCII)
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user