about summary refs log tree commit homepage
path: root/lib/unicorn/cgi_wrapper.rb
blob: b6eeb338343edb531c29f3dc2cc7bd17980580e9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# -*- encoding: binary -*-

# This code is based on the original CGIWrapper from Mongrel
# Copyright (c) 2005 Zed A. Shaw
# Copyright (c) 2009 Eric Wong
# You can redistribute it and/or modify it under the same terms as Ruby.
#
# Additional work donated by contributors.  See CONTRIBUTORS for more info.

require 'cgi'

module Unicorn; end

# The beginning of a complete wrapper around Unicorn's internal HTTP
# processing system but maintaining the original Ruby CGI module.  Use
# this only as a crutch to get existing CGI based systems working.  It
# should handle everything, but please notify us if you see special
# warnings.  This work is still very alpha so we need testers to help
# work out the various corner cases.
class Unicorn::CGIWrapper < ::CGI
  undef_method :env_table
  attr_reader :env_table
  attr_reader :body

  # these are stripped out of any keys passed to CGIWrapper.header function
  NPH = 'nph'.freeze # Completely ignored, Unicorn outputs the date regardless
  CONNECTION = 'connection'.freeze # Completely ignored. Why is CGI doing this?
  CHARSET = 'charset'.freeze # this gets appended to Content-Type
  COOKIE = 'cookie'.freeze # maps (Hash,Array,String) to "Set-Cookie" headers
  STATUS = 'status'.freeze # stored as @status
  Status = 'Status'.freeze # code + human-readable text, Rails sets this

  # some of these are common strings, but this is the only module
  # using them and the reason they're not in Unicorn::Const
  SET_COOKIE = 'Set-Cookie'.freeze
  CONTENT_TYPE = 'Content-Type'.freeze
  CONTENT_LENGTH = 'Content-Length'.freeze # this is NOT Const::CONTENT_LENGTH
  RACK_INPUT = 'rack.input'.freeze
  RACK_ERRORS = 'rack.errors'.freeze

  # this maps CGI header names to HTTP header names
  HEADER_MAP = {
    'status' => Status,
    'type' => CONTENT_TYPE,
    'server' => 'Server'.freeze,
    'language' => 'Content-Language'.freeze,
    'expires' => 'Expires'.freeze,
    'length' => CONTENT_LENGTH,
  }

  # Takes an a Rackable environment, plus any additional CGI.new
  # arguments These are used internally to create a wrapper around the
  # real CGI while maintaining Rack/Unicorn's view of the world.  This
  # this will NOT deal well with large responses that take up a lot of
  # memory, but neither does the CGI nor the original CGIWrapper from
  # Mongrel...
  def initialize(rack_env, *args)
    @env_table = rack_env
    @status = nil
    @head = {}
    @headv = Hash.new { |hash,key| hash[key] = [] }
    @body = StringIO.new("")
    super(*args)
  end

  # finalizes the response in a way Rack applications would expect
  def rack_response
    # @head[CONTENT_LENGTH] ||= @body.size
    @headv[SET_COOKIE].concat(@output_cookies) if @output_cookies
    @headv.each_pair do |key,value|
      @head[key] ||= value.join("\n") unless value.empty?
    end

    # Capitalized "Status:", with human-readable status code (e.g. "200 OK")
    @status ||= @head.delete(Status)

    [ @status || 500, @head, [ @body.string ] ]
  end

  # The header is typically called to send back the header.  In our case we
  # collect it into a hash for later usage.  This can be called multiple
  # times to set different cookies.
  def header(options = "text/html")
    # if they pass in a string then just write the Content-Type
    if String === options
      @head[CONTENT_TYPE] ||= options
    else
      HEADER_MAP.each_pair do |from, to|
        from = options.delete(from) or next
        @head[to] = from.to_s
      end

      @head[CONTENT_TYPE] ||= "text/html"
      if charset = options.delete(CHARSET)
        @head[CONTENT_TYPE] << "; charset=#{charset}"
      end

      # lots of ways to set cookies
      if cookie = options.delete(COOKIE)
        set_cookies = @headv[SET_COOKIE]
        case cookie
        when Array
          cookie.each { |c| set_cookies << c.to_s }
        when Hash
          cookie.each_value { |c| set_cookies << c.to_s }
        else
          set_cookies << cookie.to_s
        end
      end
      @status ||= options.delete(STATUS) # all lower-case

      # drop the keys we don't want anymore
      options.delete(NPH)
      options.delete(CONNECTION)

      # finally, set the rest of the headers as-is, allowing duplicates
      options.each_pair { |k,v| @headv[k] << v }
    end

    # doing this fakes out the cgi library to think the headers are empty
    # we then do the real headers in the out function call later
    ""
  end

  # The dumb thing is people can call header or this or both and in
  # any order.  So, we just reuse header and then finalize the
  # HttpResponse the right way.  This will have no effect if called
  # the second time if the first "outputted" anything.
  def out(options = "text/html")
    header(options)
    @body.size == 0 or return
    @body << yield if block_given?
  end

  # Used to wrap the normal stdinput variable used inside CGI.
  def stdinput
    @env_table[RACK_INPUT]
  end

  # return a pointer to the StringIO body since it's STDOUT-like
  def stdoutput
    @body
  end

end