=begin == NAME fastcgi.rb - fastcgi support libary Version 0.7 Copyright (C) 2001 Eli Green == EXAMPLE server = FastCGI::TCP.new('localhost', 9000) requests = 0 begin server.each_request do |request| requests+=1 out = request.out out << "Content-type: text/html\n\n" out << "" out << "FastCGI/Ruby" out << "#{requests} requests served so far.
" out << "

environment variables

" out << "" for name, value in request.env out << "" end out << "
namevalue
#{name}#{value}
" out << "Input was: " << request.in.read out << "" request.finish end ensure server.close end =end require 'socket' require 'stringio' module FastCGI # Set various FastCGI constants # Maximum number of requests that can be handled #FCGI_MAX_REQS = 1 #FCGI_MAX_CONNS = 1 # Supported version of the FastCGI protocol #FCGI_VERSION_1 = 1 # Boolean: can this application multiplex connections? #FCGI_MPXS_CONNS = 0 # Record types FCGI_BEGIN_REQUEST = 1 FCGI_ABORT_REQUEST = 2 FCGI_END_REQUEST = 3 FCGI_PARAMS = 4 FCGI_STDIN = 5 FCGI_STDOUT = 6 FCGI_STDERR = 7 FCGI_DATA = 8 FCGI_GET_VALUES = 9 FCGI_GET_VALUES_RESULT = 10 FCGI_UNKNOWN_TYPE = 11 FCGI_MAXTYPE = FCGI_UNKNOWN_TYPE # Types of management records MANAGEMENT_TYPES = [FCGI_GET_VALUES] FCGI_NULL_REQUEST_ID = 0 # Masks for flags component of FCGI_BEGIN_REQUEST FCGI_KEEP_CONN = 1 # Values for role component of FCGI_BEGIN_REQUEST FCGI_RESPONDER = 1 FCGI_AUTHORIZER = 2 FCGI_FILTER = 3 # Values for protocolStatus component of FCGI_END_REQUEST FCGI_REQUEST_COMPLETE = 0 # Request completed nicely FCGI_CANT_MPX_CONN = 1 # This app can't multiplex FCGI_OVERLOADED = 2 # New request rejected; too busy FCGI_UNKNOWN_ROLE = 3 # Role value not known =begin Under the FastCGI protocol, the "length" field for name/value pairs is either a 1 byte or 4 byte integer, with 1 bit being used as a control to let you know which size to use. It's a fair bit faster under ruby to pull the entire number out of a string buffer, then apply the 1 bit change to it than to pull it out 1 byte at a time and then shift/convert to character. Dealing with 31 bit numbers can be very strange indeed... =end VP_CLEAR_BIT = (127<<24) + (255<<16) + (255<<8) + (255) VP_SET_BIT = (128<<24) class Record def initialize @version = 1 @type = FCGI_UNKNOWN_TYPE @request_id = FCGI_NULL_REQUEST_ID @content = '' end # I could have made a class out of these, but I just end up storing the names # and values in a hash, so it seemed unnecessary. I guess my brain is still # largely stuck in Java mode, where creating objects is bloody expensive. def readValuePair(buf, pos) nlen = buf[pos] if nlen > 127 nlen, = buf[pos,4].unpack('N') nlen &= VP_CLEAR_BIT pos+=4 else pos+=1 end vlen = buf[pos] if vlen > 127 vlen, = buf[pos,4].unpack('N') vlen &= VP_CLEAR_BIT pos+=4 else pos+=1 end name = buf[pos,nlen] pos += nlen value = buf[pos,vlen] pos += vlen return pos, name, value end def writeValuePair(name, value) buf = "" nl = name.length if nl > 127 buf << [nl | VP_SET_BIT].pack('N') #(((nl>>24)|128)&255).chr << ((nl>>16)&255).chr << ((nl>>8)&255).chr << (nl&255).chr else buf << nl.chr end vl = value.length if vl > 127 buf << [nl | VP_SET_BIT].pack('N') else buf << vl.chr end buf << name buf << value return buf end def read(sock) buf = sock.read(8) @version, @type, @request_id, @content_length, padding_length = buf.unpack('ccnnc') buf = '' while buf.length < @content_length buf << sock.read(@content_length - buf.length) end if padding_length sock.read(padding_length) end case @type when FCGI_BEGIN_REQUEST @role, @flags = buf.unpack('nC') when FCGI_UNKNOWN_TYPE # Hopefully, I'll keep on top of the spec, and this'll # never happen. =) @unknown_type = buf[0] when FCGI_GET_VALUES, FCGI_PARAMS @values = Hash.new() pos = 0 while pos < @content_length pos, name, value = readValuePair(buf, pos) #print "+ #{name} = #{value}\n" @values[name] = value # print " (pos is ", pos, ")\n" end when FCGI_END_REQUEST @application_status, @protocol_status = buf.unpack('LC') end @content = buf end def write(sock) buf = '' case @type when FCGI_END_REQUEST s = @application_status buf << [@application_status, @protocol_status, 0, 0, 0].pack('LCc3') #buf << ((s>>24)&255).chr << ((s>>16)&255).chr << ((s>>8)&255).chr << (s&255).chr #buf << @protocol_status.chr << "\000\000\000" when FCGI_GET_VALUES_RESULT for name, value in params buf << writeValuePair(name, value) end when FCGI_UNKNOWN_TYPE buf << [@unknown_type, 0, 0, 0, 0, 0, 0, 0].pack('c8') #buf << @unknown_type.chr #buf << ("\000" * 7) else buf = @content end clen = buf.length padlen = clen % 8 #print("Content Length was #{clen}, pad length was #{padlen}\n") hdr = [@version, @type, @request_id, clen, padlen, 0].pack('ccnncc') sock << hdr << buf << ("\000" * padlen) end attr_accessor :version, :type, :request_id, :role, :flags, :content, :values, :application_status, :protocol_status, :unknown_type, :content_length end class Request def initialize(sock) @id = 0 @sock = sock @remaining = 1 @ready = false @in = StringIO.new @out = StringIO.new @err = StringIO.new @data = '' @env = Hash.new end def absorb_record(rec) case rec.type when FCGI_BEGIN_REQUEST #print "id == ", rec.request_id, "\n" @id = rec.request_id case rec.role when FCGI_AUTHORIZER @remaining = 1 when FCGI_RESPONDER @remaining = 2 when FCGI_FILTER @remaining = 3 end when FCGI_PARAMS if rec.content_length == 0 @remaining -= 1 else @env.update(rec.values) #for name, value in rec.values # @env[name] = value #end end when FCGI_STDIN if rec.content_length == 0 @remaining -= 1 else @in << rec.content end when FCGI_DATA # I'm pretty sure we never use this... if rec.content_length == 0 @remaining -= 1 else @data << rec.content end end if @remaining == 0 @in.pos = 0 @ready = true end end def finish begin rec = Record.new rec.request_id = @id rec.type = FCGI_STDERR @err.pos = 0 if @err.string.length > 0 until @err.eof? rec.content = @err.read(16384) rec.write(@sock) end end rec.content = '' rec.write(@sock) # FCGI_STDOUT's rec.type = FCGI_STDOUT @out.pos = 0 until @out.eof? rec.content = @out.read(16384) rec.write(@sock) end rec.content = '' rec.write(@sock) rec.type = FCGI_END_REQUEST rec.application_status = 0 rec.protocol_status = FCGI_REQUEST_COMPLETE rec.write(@sock) @sock.flush return true rescue # probably a broken UNIX socket or closed TCP connection... #print "#{$!}\n" #print $!.backtrace.collect {|i| "#{i}\n"} #return false end end def ready? return @ready end attr_reader("id", "in", "out", "err", "env") end =begin Server class. Should never be directly instantiated. See the subclasses TCPServer and UNIXServer. =end class BasicServer def initialize # reasonable defaults ... make these user configurable! @max_connections = 10 @max_requests = 10 @multiplex = true @get_values_results = Hash.new( "FCGI_MAX_CONNS" => @max_connections, "FCGI_MAX_REQS" => @max_requests, "FCGI_MPX_CONNS" => @multiplex ) end def each_request #socks = [@server] #print socks.class, "\n" rec = Record.new @requests = Hash.new while true # nsock = select(socks, nil, nil, nil) #next if nsock == nil ns, = @server.accept while not ns.eof?#true # for ns in nsock[0] # if ns == @server # newsock, = ns.accept # socks << newsock # next # end # #if ns.eof? # TODO: do error checking here to make sure we don't have any # unfinished requests in the request stack for this socket... # if we do ... do something? #@requests.delete(ns.fileno) # socks.delete(ns) # next #end rec.read(ns) if MANAGEMENT_TYPES.index(rec.type) != nil # handle management type case rec.type when FCGI_GET_VALUES reply = Record.new reply.type = FCGI_GET_VALUES_RESULT for name, value in rec.params reply.params[name] = @get_values_results[name] end reply.write(ns) end elsif rec.request_id == 0 # unknown management type reply = Record.new reply.type = FCGI_UNKNOWN_TYPE reply.unknown_type = rec.type reply.write(ns) else if not @requests.has_key?(ns.fileno) @requests[ns.fileno] = Hash.new end r_stack = @requests[ns.fileno] if not r_stack.has_key?(rec.request_id) r_stack[rec.request_id] = Request.new(ns) end request = r_stack[rec.request_id] request.absorb_record(rec) if request.ready? r_stack.delete(request.id) yield request end end # type of request end # while not ns.eof? end end def close @server.close end end class TCP < BasicServer def initialize(addr, port) super() @server = TCPServer.open(addr, port) end end class UNIX < BasicServer def initialize(sockname) super() @server = UNIXServer.open(sockname) end end # this finally works ... beware the evils of Socket.accept! it's an array! class FCGI < BasicServer def initialize super() @server = Socket.for_fd($stdin.fileno) end end end