=begin

fcgi.rb 0.8.4 - fcgi.so compatible pure-ruby FastCGI library

fastcgi.rb Copyright (C) 2001 Eli Green
fcgi.rb    Copyright (C) 2002-2003 MoonWolf <moonwolf@moonwolf.com>

=end

begin
  raise LoadError if defined?(FCGI_PURE_RUBY) && FCGI_PURE_RUBY
  require "fcgi.so"
rescue LoadError
  require 'socket'
  require 'stringio'

  class FCGI
    class Error < StandardError
    end

    # 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

    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 # Record class

    class BasicServer
      def initialize
        # reasonable defaults ... make these user configurable!
        @max_connections = 1
        @max_requests = 1
        @multiplex = true
        @get_values_results = Hash.new(
          "FCGI_MAX_CONNS" => @max_connections,
          "FCGI_MAX_REQS" => @max_requests,
          "FCGI_MPX_CONNS" => @multiplex
        )
        #
        @requests = Hash.new
        @ns, = @server.accept
      end

      def accept
        rec = Record.new
        if @ns.eof?
          @ns, = @server.accept
        end
        until @ns.eof?
          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
            unless @requests.has_key?(@ns.fileno)
              @requests[@ns.fileno] = Hash.new
            end
            r_stack = @requests[@ns.fileno]

            unless r_stack.has_key?(rec.request_id)
              r_stack[rec.request_id] = FCGI.new(@ns)
            end
            request = r_stack[rec.request_id]

            request.absorb_record(rec)

            if request.ready?
              r_stack.delete(request.id)
              return request
            end
          end # type of request
        end # until @ns.eof
        return nil
      end # accept

      def close
        @server.close
      end

    end # BasicServer class

    class TCP < BasicServer
      def initialize(addr, port)
        @server = TCPServer.open(addr, port)
        super()
      end
    end

    class UNIX < BasicServer
      def initialize(sockname)
        @server = UNIXServer.open(sockname)
        super()
      end
    end

    # this finally works ... beware the evils of Socket.accept! it's an array!
    class FastCGI < BasicServer
      def initialize
        @server = Socket.for_fd($stdin.fileno)
        super()
      end
    end

    # FCGI initialize
    @@server = nil
    @@is_cgi = nil

    def FCGI::server=(server)
      @@server = server
    end

    def FCGI::is_cgi?
      return @@is_cgi unless @@is_cgi.nil?

      begin
        s = Socket.for_fd($stdin.fileno)
        s.getpeername
        @@is_cgi = false
      rescue Errno::ENOTCONN
        @@is_cgi = false
      rescue Errno::ENOTSOCK,Errno::EINVAL
        @@is_cgi = true
      end

      @@is_cgi
    end

    def FCGI::accept
      unless @@server
        @@server = FCGI::FastCGI.new
      end
      @@server.accept
    end

    def FCGI::each
      if block_given?
        while req=FCGI::accept
          yield req
        end
      end
    end

    def FCGI::each_request
      if block_given?
        while req=FCGI::accept
          yield req
        end
      end
    end

    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
    attr_reader :id, :in, :out, :err, :env

    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)
        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

  end # FCGI class
end # begin

# There is no C version of 'each_cgi'
# Note: for ruby-1.6.8 at least, the constants CGI_PARAMS/CGI_COOKIES
# are defined within module 'CGI', even if you have subclassed it

class FCGI
  def self::each_cgi(*args)
    require 'cgi'
    
    eval(<<-EOS,TOPLEVEL_BINDING)
    class CGI
      public :env_table
      def self::remove_params
        if (const_defined?(:CGI_PARAMS))
          remove_const(:CGI_PARAMS)
          remove_const(:CGI_COOKIES)
        end
      end
    end # ::CGI class

    class FCGI
      class CGI < ::CGI
        def initialize(request, *args)
          ::CGI.remove_params
          @request = request
          super(*args)
          @args = *args
        end
        def args
          @args
        end
        def env_table
          @request.env
        end
        def stdinput
          @request.in
        end
        def stdoutput
          @request.out
        end
      end # FCGI::CGI class
    end # FCGI class
    EOS
    
    if FCGI::is_cgi?
      yield ::CGI.new(*args)
    else
      exit_requested = false
      trap('SIGPIPE','IGNORE')
      o_stdout, o_stderr = $stdout, $stderr

      FCGI::each {|request|
        trap('SIGUSR1') { exit_requested = true }
        $stdout, $stderr = request.out, request.err

        yield CGI.new(request, *args)
        
        $stdout, $stderr = o_stdout, o_stderr
        request.finish
        trap('SIGUSR1','DEFAULT')
        raise SignalException, 'SIGUSR1' if exit_requested
      }
      trap('SIGPIPE','DEFAULT')
    end
  end
end
