# * George Moschovitis # (c) 2005 Navel, all rights reserved. # $Id$ require 'cgi' require 'glue/attribute' module N class Cgi # Maximum content length allowed in requests. cattr_accessor :max_content_length, (2 * 1024 * 1024) # Multipart parsing buffer size. cattr_accessor :buffer_size, (10 * 1024) end # CGI utility methods. class CgiUtils # HTTP protocol EOL constants CR = "\x0d" LF = "\x0a" CRLF = "\x0d\x0a" EOL = CRLF # Constants for readable code STATUS_OK = 200 STATUS_PARTIAL_CONTENT = 206 STATUS_MOVED = 301 STATUS_REDIRECT = 302 STATUS_SEE_OTHER = 303 # gmosx: VERIFY THIS STATUS_SEE_OTHER_307 = 307 # gmosx: VERIFY THIS STATUS_NOT_MODIFIED = 304 STATUS_BAD_REQUEST = 400 STATUS_AUTH_REQUIRED = 401 STATUS_FORBIDDEN = 403 STATUS_NOT_FOUND = 404 STATUS_METHOD_NOT_ALLOWED = 405 STATUS_NOT_ACCEPTABLE = 406 STATUS_LENGTH_REQUIRED = 411 STATUS_PRECONDITION_FAILED = 412 STATUS_SERVER_ERROR = 500 STATUS_NOT_IMPLEMENTED = 501 STATUS_BAD_GATEWAY = 502 STATUS_VARIANT_ALSO_VARIES = 506 # Hash to allow id to description maping. STATUS_STRINGS = { 200 => "OK", 206 => "Partial Content", 300 => "Multiple Choices", 301 => "Moved Permanently", 302 => "Found", 303 => "See other", # gmosx: VERIFY THIS 304 => "Not Modified", 307 => "See other 07", # gmosx: VERIFY THIS 400 => "Bad Request", 401 => "Authorization Required", 403 => "Forbidden", 404 => "Not Found", 405 => "Method Not Allowed", 406 => "Not Acceptable", 411 => "Length Required", 412 => "Precondition Failed", 500 => "Internal Server Error", 501 => "Method Not Implemented", 502 => "Bad Gateway", 506 => "Variant Also Negotiates" } # Returns a hash with the pairs from the query # string. The implicit hash construction that is done # in parse_request_params is not done here. # # Parameters in the form xxx[] are converted # to arrays. def self.parse_query_string(query_string) # gmosx, THINK: better return nil here? return {} if (query_string.nil? or query_string.empty?) params = {} query_string.split(/[&;]/).each do |p| key, val = p.split('=') key = CGI.unescape(key) unless key.nil? val = CGI.unescape(val) unless val.nil? if key =~ /(.*)\[\]$/ if params.has_key?($1) params[$1] << val else params[$1] = [val] end else params[key] = val.nil? ? nil : val end end return params end # Parse the HTTP_COOKIE header and returns the # cookies as a key->value hash. For efficiency no # cookie objects are created. # # [+context+] # The context def self.parse_cookies(context) env = context.env # FIXME: dont precreate? context.cookies = {} if env.has_key?('HTTP_COOKIE') or env.has_key?('COOKIE') (env['HTTP_COOKIE'] or env['COOKIE']).split(/; /).each do |c| key, val = c.split(/=/, 2) key = CGI.unescape(key) val = val.split(/&/).collect{|v| CGI::unescape(v)}.join("\0") if context.cookies.include?(key) context.cookies[key] += "\0" + val else context.cookies[key] = val end end end end # Build the response headers for the context. # # [+context+] # The context of the response. # # [+proto+] # If true emmit the protocol line. Useful for MOD_RUBY. #-- # FIXME: return the correct protocol from env. # TODO: Perhaps I can optimize status calc. #++ def self.response_headers(context, proto = false) reason = STATUS_STRINGS[context.status] if proto buf = "HTTP/1.1 #{context.status} #{reason}#{EOL}Date: #{CGI::rfc1123_date(Time.now)}#{EOL}" else buf = "Status: #{context.status} #{reason}#{EOL}" end context.response_headers.each do |key, value| tmp = key.gsub(/\bwww|^te$|\b\w/) { |s| s.upcase } buf << "#{tmp}: #{value}" << EOL end context.response_cookies.each do |cookie| buf << "Set-Cookie: " << cookie.to_s << EOL end if context.response_cookies buf << EOL return buf end # Initialize the request params. # Handles multipart forms (in particular, forms that involve # file uploads). Reads query parameters in the @params field, # and cookies into @cookies. def self.parse_params(context) method = context.method if (:post == method) and %r|\Amultipart/form-data.*boundary=\"?([^\";,]+)\"?|n.match(context.headers['CONTENT_TYPE']) boundary = $1.dup # context.params = read_multipart(boundary, Integer(context.headers['CONTENT_LENGTH']), context.in, context.headers) parse_multipart(context, boundary) else case method when :get, :head context.params = CgiUtils.parse_query_string(context.query_string) when :post context.in.binmode # if defined?(context.in.binmode) context.params = CgiUtils.parse_query_string( context.in.read( Integer(context.headers['CONTENT_LENGTH'])) || '') end end end # Parse a multipart request. #-- # FIXME: implement me, not working #++ def self.parse_multipart(context, boundary) io = context.in env = context.headers io.binmode # if defined?(ins.binmode) buffer = '' bufsize = Cgi.buffer_size boundary = "--" + boundary boundary_size = boundary.size + EOL.size content_length = context.headers['CONTENT_LENGTH'].to_i if content_length > Cgi.max_content_length raise 'Request content length exceeds limit' end context.params = {} status = io.read(boundary_size) if (status.nil?) or ( (boundary + EOL) != status ) raise 'Bad content body' end content_length -= boundary_size head = nil loop do until head and /#{boundary}(?:#{EOL}|--)/n.match(buffer) if (not head) and /#{EOL}#{EOL}/n.match(buffer) buffer = buffer.sub(/\A((?:.|\n)*?#{EOL})#{EOL}/n) do head = $1.dup "" end next end puts "*** #{head}" /Content-Disposition:.* filename="?([^\";]*)"?/ni.match(head) filename = $1 if /Mac/ni.match(env['HTTP_USER_AGENT']) and /Mozilla/ni.match(env['HTTP_USER_AGENT']) and (not /MSIE/ni.match(env['HTTP_USER_AGENT'])) filename = CGI::unescape(filename) end puts "--- f: #{filename}" if filename /Content-Type: (.*)/ni.match(head) content_type = $1 or '' puts "--- c: #{content_type}" end chunk = if bufsize < content_length io.read(bufsize) else io.read(content_length) end raise 'Bad content body' unless chunk buffer << chunk content_length -= chunk.size end end end # Parse a multipart request. # Copied from ruby's cgi.rb def self.read_multipart(boundary, content_length, stdinput, env_table) params = Hash.new([]) boundary = "--" + boundary buf = "" bufsize = 10 * 1024 # start multipart/form-data stdinput.binmode if defined? stdinput.binmode if nil == status raise EOFError, "no content body" elsif boundary + EOL != status raise EOFError, "bad content body" end loop do head = nil if 10240 < content_length require "tempfile" body = Tempfile.new("CGI") else begin require "stringio" body = StringIO.new rescue LoadError require "tempfile" body = Tempfile.new("CGI") end end body.binmode if defined? body.binmode until head and /#{boundary}(?:#{EOL}|--)/n.match(buf) if (not head) and /#{EOL}#{EOL}/n.match(buf) buf = buf.sub(/\A((?:.|\n)*?#{EOL})#{EOL}/n) do head = $1.dup "" end next end if head and ( (EOL + boundary + EOL).size < buf.size ) body.print buf[0 ... (buf.size - (EOL + boundary + EOL).size)] buf[0 ... (buf.size - (EOL + boundary + EOL).size)] = "" end c = if bufsize < content_length stdinput.read(bufsize) else stdinput.read(content_length) end if c.nil? raise EOFError, "bad content body" end buf.concat(c) content_length -= c.size end buf = buf.sub(/\A((?:.|\n)*?)(?:[\r\n]{1,2})?#{boundary}([\r\n]{1,2}|--)/n) do body.print $1 if "--" == $2 content_length = -1 end "" end body.rewind /Content-Disposition:.* filename="?([^\";]*)"?/ni.match(head) filename = ($1 or "") if /Mac/ni.match(env_table['HTTP_USER_AGENT']) and /Mozilla/ni.match(env_table['HTTP_USER_AGENT']) and (not /MSIE/ni.match(env_table['HTTP_USER_AGENT'])) filename = CGI::unescape(filename) end /Content-Type: (.*)/ni.match(head) content_type = ($1 or "") (class << body; self; end).class_eval do alias local_path path define_method(:original_filename) {filename.dup.taint} define_method(:content_type) {content_type.dup.taint} end /Content-Disposition:.* name="?([^\";]*)"?/ni.match(head) name = $1.dup if name =~ /(.*)\[\]$/ if params.has_name?($1) params[$1] << body else params[$1] = [body] end else params[name] = body.nil? ? nil : body end break if buf.size == 0 break if content_length === -1 end params end end end