Class: Puma::Client

Inherits:
Object
  • Object
show all
Includes:
ClientEnv
Defined in:
lib/puma/client.rb

Overview

An instance of this class wraps a connection/socket. For example, this could be an http request from a browser or from CURL.

An instance of ‘Puma::Client` can be used as if it were an IO object by the reactor. The reactor is expected to call `#to_io` on any non-IO objects it polls. For example, nio4r internally calls `IO::try_convert` (which may call `#to_io`) when a new socket is registered.

Instances of this class are responsible for knowing if the request line, headers and body are fully buffered and verified via the ‘try_to_finish` method. All verification of each request is done in the `Client` object. They can be used to “time out” a response via the `timeout_at` reader.

Most of the code for env processing and verification is contained in ‘Puma::ClientEnv`, which is included.

Constant Summary collapse

ALLOWED_TRANSFER_ENCODING =

this tests all values but the last, which must be chunked

%w[compress deflate gzip].freeze
CHUNK_SIZE_INVALID =

chunked body validation

/[^\h]/.freeze
CHUNK_VALID_ENDING =
Const::LINE_END
CHUNK_VALID_ENDING_SIZE =
CHUNK_VALID_ENDING.bytesize
MAX_CHUNK_HEADER_SIZE =

The maximum number of bytes we’ll buffer looking for a valid chunk header.

4096
MAX_CHUNK_EXCESS =

The maximum amount of excess data the client sends using chunk size extensions before we abort the connection.

16 * 1024
CONTENT_LENGTH_VALUE_INVALID =

Content-Length header value validation

/[^\d]/.freeze
TE_ERR_MSG =
'Invalid Transfer-Encoding'
STRIP_OWS =
/\A[ \t]+|[ \t]+\z/
EmptyBody =

The object used for a request with no body. All requests with no body share this one object since it has no state.

NullIO.new

Constants included from Const

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

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from ClientEnv

#default_server_port, #normalize_env, #req_env_post_parse

Constructor Details

#initialize(io, env = nil) ⇒ Client

Returns a new instance of Client.



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
# File 'lib/puma/client.rb', line 83

def initialize(io, env=nil)
  @io = io
  @to_io = io.to_io
  @io_buffer = IOBuffer.new
  @proto_env = env
  @env = env&.dup

  @parser = HttpParser.new
  @parsed_bytes = 0
  @read_header = true
  @read_proxy = false
  @ready = false

  @body = nil
  @body_read_start = nil
  @buffer = nil
  @tempfile = nil

  @timeout_at = nil

  @requests_served = 0
  @hijacked = false

  @http_content_length_limit = nil
  @http_content_length_limit_exceeded = nil
  @error_status_code = nil

  @peerip = nil
  @peer_family = nil
  @listener = nil
  @remote_addr_header = nil
  @expect_proxy_proto = false

  @body_remain = 0

  @in_last_chunk = false

  # need unfrozen ASCII-8BIT, +'' is UTF-8
  @read_buffer = String.new # rubocop: disable Performance/UnfreezeString
end

Instance Attribute Details

#bodyObject (readonly)

Returns the value of attribute body.



75
76
77
# File 'lib/puma/client.rb', line 75

def body
  @body
end

#envObject (readonly)

Returns the value of attribute env.



75
76
77
# File 'lib/puma/client.rb', line 75

def env
  @env
end

#env_set_http_versionObject

Returns the value of attribute env_set_http_version.



81
82
83
# File 'lib/puma/client.rb', line 81

def env_set_http_version
  @env_set_http_version
end

#error_status_codeObject (readonly)

Returns the value of attribute error_status_code.



75
76
77
# File 'lib/puma/client.rb', line 75

def error_status_code
  @error_status_code
end

#hijackedObject (readonly)

Returns the value of attribute hijacked.



75
76
77
# File 'lib/puma/client.rb', line 75

def hijacked
  @hijacked
end

#http_content_length_limit=(value) ⇒ Object (writeonly)

Sets the attribute http_content_length_limit

Parameters:

  • value

    the value to set the attribute http_content_length_limit to.



79
80
81
# File 'lib/puma/client.rb', line 79

def http_content_length_limit=(value)
  @http_content_length_limit = value
end

#http_content_length_limit_exceededObject (readonly)

Returns the value of attribute http_content_length_limit_exceeded.



75
76
77
# File 'lib/puma/client.rb', line 75

def http_content_length_limit_exceeded
  @http_content_length_limit_exceeded
end

#in_data_phaseObject (readonly)



148
149
150
# File 'lib/puma/client.rb', line 148

def in_data_phase
  !(@read_header || @read_proxy)
end

#inspectObject (readonly)



136
137
138
# File 'lib/puma/client.rb', line 136

def inspect
  "#<Puma::Client:0x#{object_id.to_s(16)} @ready=#{@ready.inspect}>"
end

#ioObject (readonly)

Returns the value of attribute io.



75
76
77
# File 'lib/puma/client.rb', line 75

def io
  @io
end

#io_bufferObject (readonly)

Returns the value of attribute io_buffer.



75
76
77
# File 'lib/puma/client.rb', line 75

def io_buffer
  @io_buffer
end

#listenerObject

Returns the value of attribute listener.



81
82
83
# File 'lib/puma/client.rb', line 81

def listener
  @listener
end

#peeripObject



346
347
348
349
350
351
352
353
354
355
356
# File 'lib/puma/client.rb', line 346

def peerip
  return @peerip if @peerip

  if @remote_addr_header
    hdr = (@env[@remote_addr_header] || socket_peerip).split(/[\s,]/).first
    @peerip = hdr
    return hdr
  end

  @peerip ||= socket_peerip
end

#readyObject (readonly)

Returns the value of attribute ready.



75
76
77
# File 'lib/puma/client.rb', line 75

def ready
  @ready
end

#remote_addr_headerObject

Returns the value of attribute remote_addr_header.



81
82
83
# File 'lib/puma/client.rb', line 81

def remote_addr_header
  @remote_addr_header
end

#requests_servedObject (readonly)

Returns the value of attribute requests_served.



75
76
77
# File 'lib/puma/client.rb', line 75

def requests_served
  @requests_served
end

#supported_http_methods=(value) ⇒ Object (writeonly)

Sets the attribute supported_http_methods

Parameters:

  • value

    the value to set the attribute supported_http_methods to.



79
80
81
# File 'lib/puma/client.rb', line 79

def supported_http_methods=(value)
  @supported_http_methods = value
end

#tempfileObject (readonly)

Returns the value of attribute tempfile.



75
76
77
# File 'lib/puma/client.rb', line 75

def tempfile
  @tempfile
end

#timeoutObject (readonly)

Number of seconds until the timeout elapses.



158
159
160
# File 'lib/puma/client.rb', line 158

def timeout
  [@timeout_at - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0].max
end

#timeout_atObject (readonly)

Returns the value of attribute timeout_at.



75
76
77
# File 'lib/puma/client.rb', line 75

def timeout_at
  @timeout_at
end

#to_ioObject (readonly)

Returns the value of attribute to_io.



75
76
77
# File 'lib/puma/client.rb', line 75

def to_io
  @to_io
end

Instance Method Details

#can_close?Boolean

Returns true if the persistent connection can be closed immediately without waiting for the configured idle/shutdown timeout.

Returns:

  • (Boolean)

Version:

  • 5.0.0



372
373
374
375
# File 'lib/puma/client.rb', line 372

def can_close?
  # Allow connection to close if we're not in the middle of parsing a request.
  @parsed_bytes == 0
end

#closeObject



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

def close
  tempfile_close
  begin
    @io.close
  rescue IOError, Errno::EBADF
  end
end

#closed?Boolean

Remove in Puma 7?

Returns:

  • (Boolean)


125
126
127
# File 'lib/puma/client.rb', line 125

def closed?
  @to_io.closed?
end

#eagerly_finishObject



269
270
271
272
273
274
275
# File 'lib/puma/client.rb', line 269

def eagerly_finish
  return true if @ready
  while @to_io.wait_readable(0) # rubocop: disable Style/WhileUntilModifier
    return true if try_to_finish
  end
  false
end

#expect_proxy_proto=(val) ⇒ Object



377
378
379
380
381
382
383
384
385
386
# File 'lib/puma/client.rb', line 377

def expect_proxy_proto=(val)
  if val
    if @read_header
      @read_proxy = true
    end
  else
    @read_proxy = false
  end
  @expect_proxy_proto = val
end

#finish(timeout) ⇒ Object



277
278
279
280
# File 'lib/puma/client.rb', line 277

def finish(timeout)
  return if @ready
  @to_io.wait_readable(timeout) || timeout! until try_to_finish
end

#full_hijackObject

For the full hijack protocol, ‘env` is set to `client.method :full_hijack`



142
143
144
145
# File 'lib/puma/client.rb', line 142

def full_hijack
  @hijacked = true
  env[HIJACK_IO] ||= @io
end

#has_back_to_back_requests?Boolean

if a client sends back-to-back requests, the buffer may contain one or more of them.

Returns:

  • (Boolean)


190
191
192
# File 'lib/puma/client.rb', line 190

def has_back_to_back_requests?
  !(@buffer.nil? || @buffer.empty?)
end

#io_ok?Boolean

Test to see if io meets a bare minimum of functioning, @to_io needs to be used for MiniSSL::Socket

Returns:

  • (Boolean)


131
132
133
# File 'lib/puma/client.rb', line 131

def io_ok?
  @to_io.is_a?(::BasicSocket) && !closed?
end

#parser_executeInteger

Wraps ‘@parser.execute` and adds meaningful error messages

Returns:

  • (Integer)

    bytes of buffer read by parser



285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
# File 'lib/puma/client.rb', line 285

def parser_execute
  ret = @parser.execute(@env, @buffer, @parsed_bytes)

  if @env[REQUEST_METHOD] && @supported_http_methods != :any && !@supported_http_methods.key?(@env[REQUEST_METHOD])
    raise HttpParserError501, "#{@env[REQUEST_METHOD]} method is not supported"
  end
  ret
rescue => e
  @env[HTTP_CONNECTION] = 'close'
  raise e unless HttpParserError === e && e.message.include?('non-SSL')

  req, _ = @buffer.split "\r\n\r\n"
  request_line, headers = req.split "\r\n", 2

  # below checks for request issues and changes error message accordingly
  if !@env.key? REQUEST_METHOD
    if request_line.count(' ') != 2
       # maybe this is an SSL connection ?
      raise e
    else
      method = request_line[/\A[^ ]+/]
      raise e, "Invalid HTTP format, parsing fails. Bad method #{method}"
    end
  elsif !@env.key? REQUEST_PATH
    path = request_line[/\A[^ ]+ +([^ ?\r\n]+)/, 1]
    raise e, "Invalid HTTP format, parsing fails. Bad path #{path}"
  elsif request_line.match?(/\A[^ ]+ +[^ ?\r\n]+\?/) && !@env.key?(QUERY_STRING)
    query = request_line[/\A[^ ]+ +[^? ]+\?([^ ]+)/, 1]
    raise e, "Invalid HTTP format, parsing fails. Bad query #{query}"
  elsif !@env.key? SERVER_PROTOCOL
    # protocol is bad
    text = request_line[/[^ ]*\z/]
    raise HttpParserError, "Invalid HTTP format, parsing fails. Bad protocol #{text}"
  elsif !headers.empty?
    # headers are bad
    hdrs = headers.split("\r\n").map { |h| h.gsub "\n", '\n'}.join "\n"
    raise HttpParserError, "Invalid HTTP format, parsing fails. Bad headers\n#{hdrs}"
  end
end

#peer_familyObject



358
359
360
361
362
363
364
365
366
# File 'lib/puma/client.rb', line 358

def peer_family
  return @peer_family if @peer_family

  @peer_family ||= begin
                     @io.local_address.afamily
                   rescue
                     Socket::AF_INET
                   end
end

#process_back_to_back_requestsObject

only used with back-to-back requests contained in the buffer



178
179
180
181
182
183
184
185
186
# File 'lib/puma/client.rb', line 178

def process_back_to_back_requests
  if @buffer
    return false unless try_to_parse_proxy_protocol

    @parsed_bytes = parser_execute

    @parser.finished? ? process_env_body : false
  end
end

#process_env_bodyObject

processes the ‘env` and the request body

Raises:



326
327
328
329
330
331
332
# File 'lib/puma/client.rb', line 326

def process_env_body
  temp = setup_body
  normalize_env
  req_env_post_parse
  raise HttpParserError if @error_status_code
  temp
end

#resetObject



162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/puma/client.rb', line 162

def reset
  @parser.reset
  @io_buffer.reset
  @read_header = true
  @read_proxy = !!@expect_proxy_proto
  @env = @proto_env.dup
  @parsed_bytes = 0
  @ready = false
  @body_remain = 0
  @peerip = nil if @remote_addr_header
  @in_last_chunk = false
  @http_content_length_limit_exceeded = nil
  @error_status_code = nil
end

#set_timeout(val) ⇒ Object



152
153
154
# File 'lib/puma/client.rb', line 152

def set_timeout(val)
  @timeout_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + val
end

#tempfile_closeObject



202
203
204
205
206
207
208
209
# File 'lib/puma/client.rb', line 202

def tempfile_close
  tf_path = @tempfile&.path
  @tempfile&.close
  File.unlink(tf_path) if tf_path
  @tempfile = nil
  @body = nil
rescue Errno::ENOENT, IOError
end

#timeout!Object

Raises:



334
335
336
337
# File 'lib/puma/client.rb', line 334

def timeout!
  write_error(408) if in_data_phase
  raise ConnectionError
end

#try_to_finishObject



235
236
237
238
239
240
241
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
# File 'lib/puma/client.rb', line 235

def try_to_finish
  return read_body if in_data_phase

  data = nil
  begin
    data = @io.read_nonblock(CHUNK_SIZE)
  rescue IO::WaitReadable
    return false
  rescue EOFError
    # Swallow error, don't log
  rescue SystemCallError, IOError
    raise ConnectionError, "Connection error detected during read"
  end

  # No data means a closed socket
  unless data
    @buffer = nil
    set_ready
    raise EOFError
  end

  if @buffer
    @buffer << data
  else
    @buffer = data
  end

  return false unless try_to_parse_proxy_protocol

  @parsed_bytes = parser_execute

  @parser.finished? ? process_env_body : false
end

#try_to_parse_proxy_protocolObject

If necessary, read the PROXY protocol from the buffer. Returns false if more data is needed.



213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/puma/client.rb', line 213

def try_to_parse_proxy_protocol
  if @read_proxy
    if @expect_proxy_proto == :v1
      if @buffer.include? "\r\n"
        if md = PROXY_PROTOCOL_V1_REGEX.match(@buffer)
          if md[1]
            @peerip = md[1].split(" ")[0]
          end
          @buffer = md.post_match
        end
        # if the buffer has a \r\n but doesn't have a PROXY protocol
        # request, this is just HTTP from a non-PROXY client; move on
        @read_proxy = false
        return @buffer.size > 0
      else
        return false
      end
    end
  end
  true
end

#write_error(status_code) ⇒ Object



339
340
341
342
343
344
# File 'lib/puma/client.rb', line 339

def write_error(status_code)
  begin
    @io << ERROR_RESPONSE[status_code]
  rescue StandardError
  end
end