=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 << "name | value |
"
for name, value in request.env
out << "#{name} | #{value} |
"
end
out << "
"
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