Module: Puma::Request

Includes:
Const
Included in:
Server
Defined in:
lib/puma/request.rb

Overview

The methods here are included in Server, but are separated into this file. All the methods here pertain to passing the request to the app, then writing the response back to the client.

None of the methods here are called externally, with the exception of #handle_request, which is called in Server#process_client.

Version:

  • 5.0.3

Constant Summary

Constants included from Const

Const::BANNED_HEADER_KEY, Const::CGI_VER, Const::CHUNKED, Const::CHUNK_SIZE, Const::CLOSE, Const::CLOSE_CHUNKED, Const::CODE_NAME, Const::COLON, Const::CONNECTION_CLOSE, Const::CONNECTION_KEEP_ALIVE, Const::CONTENT_LENGTH, Const::CONTENT_LENGTH2, Const::CONTENT_LENGTH_S, Const::CONTINUE, Const::DQUOTE, Const::EARLY_HINTS, Const::ERROR_RESPONSE, Const::FAST_TRACK_KA_TIMEOUT, Const::FIRST_DATA_TIMEOUT, Const::GATEWAY_INTERFACE, Const::HALT_COMMAND, Const::HEAD, Const::HIJACK, Const::HIJACK_IO, Const::HIJACK_P, Const::HTTP, Const::HTTPS, Const::HTTPS_KEY, Const::HTTP_10_200, Const::HTTP_11, Const::HTTP_11_100, Const::HTTP_11_200, Const::HTTP_CONNECTION, Const::HTTP_EXPECT, Const::HTTP_HEADER_DELIMITER, Const::HTTP_HOST, Const::HTTP_VERSION, Const::HTTP_X_FORWARDED_FOR, Const::HTTP_X_FORWARDED_PROTO, Const::HTTP_X_FORWARDED_SCHEME, Const::HTTP_X_FORWARDED_SSL, Const::ILLEGAL_HEADER_KEY_REGEX, Const::ILLEGAL_HEADER_VALUE_REGEX, Const::KEEP_ALIVE, Const::LINE_END, Const::LOCALHOST, Const::LOCALHOST_IP, Const::MAX_BODY, Const::MAX_FAST_INLINE, Const::MAX_HEADER, Const::NEWLINE, Const::PATH_INFO, Const::PERSISTENT_TIMEOUT, Const::PORT_443, Const::PORT_80, Const::PROXY_PROTOCOL_V1_REGEX, Const::PUMA_CONFIG, Const::PUMA_PEERCERT, Const::PUMA_SERVER_STRING, Const::PUMA_SOCKET, Const::PUMA_TMP_BASE, Const::PUMA_VERSION, Const::QUERY_STRING, Const::RACK_AFTER_REPLY, Const::RACK_INPUT, Const::RACK_URL_SCHEME, Const::REMOTE_ADDR, Const::REQUEST_METHOD, Const::REQUEST_PATH, Const::REQUEST_URI, Const::RESTART_COMMAND, Const::SERVER_NAME, Const::SERVER_PORT, Const::SERVER_PROTOCOL, Const::SERVER_SOFTWARE, Const::STOP_COMMAND, Const::TRANSFER_ENCODING, Const::TRANSFER_ENCODING2, Const::TRANSFER_ENCODING_CHUNKED, Const::WRITE_TIMEOUT

Instance Method Summary collapse

Instance Method Details

#default_server_port(env) ⇒ Puma::Const::PORT_443, Puma::Const::PORT_80

Parameters:

  • env (Hash)

    see Puma::Client#env, from request

Returns:



194
195
196
197
198
199
200
# File 'lib/puma/request.rb', line 194

def default_server_port(env)
  if ['on', HTTPS].include?(env[HTTPS_KEY]) || env[HTTP_X_FORWARDED_PROTO].to_s[0...5] == HTTPS || env[HTTP_X_FORWARDED_SCHEME] == HTTPS || env[HTTP_X_FORWARDED_SSL] == "on"
    PORT_443
  else
    PORT_80
  end
end

#handle_request(client, lines, requests) ⇒ Boolean, :async

Takes the request contained in client, invokes the Rack application to construct the response and writes it back to client.io.

It'll return false when the connection is closed, this doesn't mean that the response wasn't successful.

It'll return :async if the connection remains open but will be handled elsewhere, i.e. the connection has been hijacked by the Rack application.

Finally, it'll return true on keep-alive connections.

Parameters:

Returns:

  • (Boolean, :async)


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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/puma/request.rb', line 32

def handle_request(client, lines, requests)
  env = client.env
  io  = client.io   # io may be a MiniSSL::Socket

  return false if closed_socket?(io)

  normalize_env env, client

  env[PUMA_SOCKET] = io

  if env[HTTPS_KEY] && io.peercert
    env[PUMA_PEERCERT] = io.peercert
  end

  env[HIJACK_P] = true
  env[HIJACK] = client

  body = client.body

  head = env[REQUEST_METHOD] == HEAD

  env[RACK_INPUT] = body
  env[RACK_URL_SCHEME] ||= default_server_port(env) == PORT_443 ? HTTPS : HTTP

  if @early_hints
    env[EARLY_HINTS] = lambda { |headers|
      begin
        fast_write io, str_early_hints(headers)
      rescue ConnectionError => e
        @events.debug_error e
        # noop, if we lost the socket we just won't send the early hints
      end
    }
  end

  req_env_post_parse env

  # A rack extension. If the app writes #call'ables to this
  # array, we will invoke them when the request is done.
  #
  after_reply = env[RACK_AFTER_REPLY] = []

  begin
    begin
      status, headers, res_body = @thread_pool.with_force_shutdown do
        @app.call(env)
      end

      return :async if client.hijacked

      status = status.to_i

      if status == -1
        unless headers.empty? and res_body == []
          raise "async response must have empty headers and body"
        end

        return :async
      end
    rescue ThreadPool::ForceShutdown => e
      @events.unknown_error e, client, "Rack app"
      @events.log "Detected force shutdown of a thread"

      status, headers, res_body = lowlevel_error(e, env, 503)
    rescue Exception => e
      @events.unknown_error e, client, "Rack app"

      status, headers, res_body = lowlevel_error(e, env, 500)
    end

    res_info = {}
    res_info[:content_length] = nil
    res_info[:no_body] = head

    res_info[:content_length] = if res_body.kind_of? Array and res_body.size == 1
      res_body[0].bytesize
    else
      nil
    end

    cork_socket io

    str_headers(env, status, headers, res_info, lines, requests, client)

    line_ending = LINE_END

    content_length  = res_info[:content_length]
    response_hijack = res_info[:response_hijack]

    if res_info[:no_body]
      if content_length and status != 204
        lines.append CONTENT_LENGTH_S, content_length.to_s, line_ending
      end

      lines << LINE_END
      fast_write io, lines.to_s
      return res_info[:keep_alive]
    end

    if content_length
      lines.append CONTENT_LENGTH_S, content_length.to_s, line_ending
      chunked = false
    elsif !response_hijack and res_info[:allow_chunked]
      lines << TRANSFER_ENCODING_CHUNKED
      chunked = true
    end

    lines << line_ending

    fast_write io, lines.to_s

    if response_hijack
      response_hijack.call io
      return :async
    end

    begin
      res_body.each do |part|
        next if part.bytesize.zero?
        if chunked
           fast_write io, (part.bytesize.to_s(16) << line_ending)
           fast_write io, part            # part may have different encoding
           fast_write io, line_ending
        else
          fast_write io, part
        end
        io.flush
      end

      if chunked
        fast_write io, CLOSE_CHUNKED
        io.flush
      end
    rescue SystemCallError, IOError
      raise ConnectionError, "Connection error detected during write"
    end

  ensure
    begin
      uncork_socket io

      body.close
      client.tempfile.unlink if client.tempfile
    ensure
      # Whatever happens, we MUST call `close` on the response body.
      # Otherwise Rack::BodyProxy callbacks may not fire and lead to various state leaks
      res_body.close if res_body.respond_to? :close
    end

    begin
      after_reply.each { |o| o.call }
    rescue StandardError => e
      @log_writer.debug_error e
    end
  end

  res_info[:keep_alive]
end

#normalize_env(env, client) ⇒ Object

TODO:

make private in 6.0.0

Given a Hash env for the request read from client, add and fixup keys to comply with Rack's env guidelines.

Parameters:

  • env (Hash)

    see Puma::Client#env, from request

  • client (Puma::Client)

    only needed for Client#peerip



242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
# File 'lib/puma/request.rb', line 242

def normalize_env(env, client)
  if host = env[HTTP_HOST]
    # host can be a hostname, ipv4 or bracketed ipv6. Followed by an optional port.
    if colon = host.rindex("]:") # IPV6 with port
      env[SERVER_NAME] = host[0, colon+1]
      env[SERVER_PORT] = host[colon+2, host.bytesize]
    elsif !host.start_with?("[") && colon = host.index(":") # not hostname or IPV4 with port
      env[SERVER_NAME] = host[0, colon]
      env[SERVER_PORT] = host[colon+1, host.bytesize]
    else
      env[SERVER_NAME] = host
      env[SERVER_PORT] = default_server_port(env)
    end
  else
    env[SERVER_NAME] = LOCALHOST
    env[SERVER_PORT] = default_server_port(env)
  end

  unless env[REQUEST_PATH]
    # it might be a dumbass full host request header
    uri = URI.parse(env[REQUEST_URI])
    env[REQUEST_PATH] = uri.path

    raise "No REQUEST PATH" unless env[REQUEST_PATH]

    # A nil env value will cause a LintError (and fatal errors elsewhere),
    # so only set the env value if there actually is a value.
    env[QUERY_STRING] = uri.query if uri.query
  end

  env[PATH_INFO] = env[REQUEST_PATH]

  # From https://www.ietf.org/rfc/rfc3875 :
  # "Script authors should be aware that the REMOTE_ADDR and
  # REMOTE_HOST meta-variables (see sections 4.1.8 and 4.1.9)
  # may not identify the ultimate source of the request.
  # They identify the client for the immediate request to the
  # server; that client may be a proxy, gateway, or other
  # intermediary acting on behalf of the actual source client."
  #

  unless env.key?(REMOTE_ADDR)
    begin
      addr = client.peerip
    rescue Errno::ENOTCONN
      # Client disconnects can result in an inability to get the
      # peeraddr from the socket; default to localhost.
      addr = LOCALHOST_IP
    end

    # Set unix socket addrs to localhost
    addr = LOCALHOST_IP if addr.empty?

    env[REMOTE_ADDR] = addr
  end
end