# * George Moschovitis # * James Britt # (c) 2005 Navel, all rights reserved. # $Id: cgi.rb 1 2005-04-11 11:04:30Z gmosx $ require 'cgi' require 'stringio' require 'tempfile' require 'glue/attribute' # Speeds things up, more comaptible with OSX. Socket.do_not_reverse_lookup = true module Nitro 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) def self.start( conf ) cgi = CGI.new conf = Flexob.new(conf) unless conf.is_a?(Flexob) context = Context.new(conf) context.headers = ENV CgiUtils.parse_params(context) CgiUtils.parse_cookies(context) # gmosx, TODO: move this into a filter. Og.db.get_connection if defined?(Og) and Og.db context.render( context.path) Og.db.put_connection if defined?(Og) and Og.db cgi.print(CgiUtils.response_headers(context)) cgi.print(context.out) end 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) context.params = 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(context.content_length) || '') end end end # Parse a multipart request. # Adapted from Ruby's cgi.rb #-- # TODO: optimize and rationalize this. #++ def self.parse_multipart(context, boundary) input = context.in content_length = context.content_length env_table = context.env params = Hash.new([]) boundary = "--" + boundary buf = "" input.binmode if defined? input.binmode boundary_size = boundary.size + EOL.size content_length -= boundary_size status = input.read(boundary_size) 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 body = Tempfile.new("CGI") else begin body = StringIO.new rescue LoadError 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 Cgi.buffer_size < content_length input.read(Cgi.buffer_size) else input.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 } # gmosx: this hides the performance hit!! define_method(:to_s) { read } end /Content-Disposition:.* name="?([^\";]*)"?/ni.match(head) name = $1.dup if params.has_key?(name) params[name] = [params[name]] << body else params[name] = body end break if buf.size == 0 break if content_length === -1 end return params end end end