puma 3.12.0 → 4.3.8

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of puma might be problematic. Click here for more details.

Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +164 -0
  3. data/README.md +76 -48
  4. data/docs/architecture.md +1 -0
  5. data/docs/deployment.md +24 -4
  6. data/docs/plugins.md +20 -10
  7. data/docs/restart.md +4 -2
  8. data/docs/systemd.md +27 -9
  9. data/docs/tcp_mode.md +96 -0
  10. data/ext/puma_http11/PumaHttp11Service.java +2 -0
  11. data/ext/puma_http11/extconf.rb +13 -0
  12. data/ext/puma_http11/http11_parser.c +40 -63
  13. data/ext/puma_http11/http11_parser.java.rl +21 -37
  14. data/ext/puma_http11/http11_parser.rl +3 -1
  15. data/ext/puma_http11/http11_parser_common.rl +3 -3
  16. data/ext/puma_http11/mini_ssl.c +86 -4
  17. data/ext/puma_http11/org/jruby/puma/Http11.java +106 -114
  18. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +91 -106
  19. data/ext/puma_http11/org/jruby/puma/IOBuffer.java +72 -0
  20. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +15 -4
  21. data/ext/puma_http11/puma_http11.c +3 -0
  22. data/lib/puma.rb +8 -0
  23. data/lib/puma/accept_nonblock.rb +7 -1
  24. data/lib/puma/app/status.rb +37 -29
  25. data/lib/puma/binder.rb +47 -68
  26. data/lib/puma/cli.rb +6 -0
  27. data/lib/puma/client.rb +244 -199
  28. data/lib/puma/cluster.rb +55 -30
  29. data/lib/puma/commonlogger.rb +2 -0
  30. data/lib/puma/configuration.rb +6 -3
  31. data/lib/puma/const.rb +32 -18
  32. data/lib/puma/control_cli.rb +41 -14
  33. data/lib/puma/detect.rb +2 -0
  34. data/lib/puma/dsl.rb +311 -77
  35. data/lib/puma/events.rb +6 -1
  36. data/lib/puma/io_buffer.rb +3 -6
  37. data/lib/puma/jruby_restart.rb +2 -0
  38. data/lib/puma/launcher.rb +99 -55
  39. data/lib/puma/minissl.rb +37 -17
  40. data/lib/puma/minissl/context_builder.rb +76 -0
  41. data/lib/puma/null_io.rb +2 -0
  42. data/lib/puma/plugin.rb +7 -2
  43. data/lib/puma/plugin/tmp_restart.rb +2 -0
  44. data/lib/puma/rack/builder.rb +4 -1
  45. data/lib/puma/rack/urlmap.rb +2 -0
  46. data/lib/puma/rack_default.rb +2 -0
  47. data/lib/puma/reactor.rb +112 -57
  48. data/lib/puma/runner.rb +13 -3
  49. data/lib/puma/server.rb +119 -48
  50. data/lib/puma/single.rb +5 -3
  51. data/lib/puma/state_file.rb +2 -0
  52. data/lib/puma/tcp_logger.rb +2 -0
  53. data/lib/puma/thread_pool.rb +17 -33
  54. data/lib/puma/util.rb +2 -6
  55. data/lib/rack/handler/puma.rb +6 -3
  56. data/tools/docker/Dockerfile +16 -0
  57. data/tools/jungle/init.d/puma +6 -6
  58. data/tools/trickletest.rb +0 -1
  59. metadata +26 -14
  60. data/lib/puma/compat.rb +0 -14
  61. data/lib/puma/convenient.rb +0 -23
  62. data/lib/puma/daemon_ext.rb +0 -31
  63. data/lib/puma/delegation.rb +0 -11
  64. data/lib/puma/java_io_buffer.rb +0 -45
  65. data/lib/puma/rack/backports/uri/common_193.rb +0 -33
@@ -10,6 +10,7 @@
10
10
  #include "ext_help.h"
11
11
  #include <assert.h>
12
12
  #include <string.h>
13
+ #include <ctype.h>
13
14
  #include "http11_parser.h"
14
15
 
15
16
  #ifndef MANAGED_STRINGS
@@ -200,6 +201,8 @@ void http_field(puma_parser* hp, const char *field, size_t flen,
200
201
  f = rb_str_new(hp->buf, new_size);
201
202
  }
202
203
 
204
+ while (vlen > 0 && isspace(value[vlen - 1])) vlen--;
205
+
203
206
  /* check for duplicate header */
204
207
  v = rb_hash_aref(hp->request, f);
205
208
 
data/lib/puma.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Standard libraries
2
4
  require 'socket'
3
5
  require 'tempfile'
@@ -20,4 +22,10 @@ module Puma
20
22
  def self.stats
21
23
  @get_stats.stats
22
24
  end
25
+
26
+ # Thread name is new in Ruby 2.3
27
+ def self.set_thread_name(name)
28
+ return unless Thread.current.respond_to?(:name=)
29
+ Thread.current.name = "puma #{name}"
30
+ end
23
31
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'openssl'
2
4
 
3
5
  module OpenSSL
@@ -13,7 +15,11 @@ module OpenSSL
13
15
  ssl.accept if @start_immediately
14
16
  ssl
15
17
  rescue SSLError => ex
16
- sock.close
18
+ if ssl
19
+ ssl.close
20
+ else
21
+ sock.close
22
+ end
17
23
  raise ex
18
24
  end
19
25
  end
@@ -1,26 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Puma
2
4
  module App
5
+ # Check out {#call}'s source code to see what actions this web application
6
+ # can respond to.
3
7
  class Status
4
- def initialize(cli)
5
- @cli = cli
6
- @auth_token = nil
7
- end
8
8
  OK_STATUS = '{ "status": "ok" }'.freeze
9
9
 
10
- attr_accessor :auth_token
11
-
12
- def authenticate(env)
13
- return true unless @auth_token
14
- env['QUERY_STRING'].to_s.split(/&;/).include?("token=#{@auth_token}")
15
- end
16
-
17
- def rack_response(status, body, content_type='application/json')
18
- headers = {
19
- 'Content-Type' => content_type,
20
- 'Content-Length' => body.bytesize.to_s
21
- }
22
-
23
- [status, headers, [body]]
10
+ def initialize(cli, token = nil)
11
+ @cli = cli
12
+ @auth_token = token
24
13
  end
25
14
 
26
15
  def call(env)
@@ -28,47 +17,66 @@ module Puma
28
17
  return rack_response(403, 'Invalid auth token', 'text/plain')
29
18
  end
30
19
 
20
+ if env['PATH_INFO'] =~ /\/(gc-stats|stats|thread-backtraces)$/
21
+ require 'json'
22
+ end
23
+
31
24
  case env['PATH_INFO']
32
25
  when /\/stop$/
33
26
  @cli.stop
34
- return rack_response(200, OK_STATUS)
27
+ rack_response(200, OK_STATUS)
35
28
 
36
29
  when /\/halt$/
37
30
  @cli.halt
38
- return rack_response(200, OK_STATUS)
31
+ rack_response(200, OK_STATUS)
39
32
 
40
33
  when /\/restart$/
41
34
  @cli.restart
42
- return rack_response(200, OK_STATUS)
35
+ rack_response(200, OK_STATUS)
43
36
 
44
37
  when /\/phased-restart$/
45
38
  if !@cli.phased_restart
46
- return rack_response(404, '{ "error": "phased restart not available" }')
39
+ rack_response(404, '{ "error": "phased restart not available" }')
47
40
  else
48
- return rack_response(200, OK_STATUS)
41
+ rack_response(200, OK_STATUS)
49
42
  end
50
43
 
51
44
  when /\/reload-worker-directory$/
52
45
  if !@cli.send(:reload_worker_directory)
53
- return rack_response(404, '{ "error": "reload_worker_directory not available" }')
46
+ rack_response(404, '{ "error": "reload_worker_directory not available" }')
54
47
  else
55
- return rack_response(200, OK_STATUS)
48
+ rack_response(200, OK_STATUS)
56
49
  end
57
50
 
58
51
  when /\/gc$/
59
52
  GC.start
60
- return rack_response(200, OK_STATUS)
53
+ rack_response(200, OK_STATUS)
61
54
 
62
55
  when /\/gc-stats$/
63
- json = "{" + GC.stat.map { |k, v| "\"#{k}\": #{v}" }.join(",") + "}"
64
- return rack_response(200, json)
56
+ rack_response(200, GC.stat.to_json)
65
57
 
66
58
  when /\/stats$/
67
- return rack_response(200, @cli.stats)
59
+ rack_response(200, @cli.stats)
68
60
  else
69
61
  rack_response 404, "Unsupported action", 'text/plain'
70
62
  end
71
63
  end
64
+
65
+ private
66
+
67
+ def authenticate(env)
68
+ return true unless @auth_token
69
+ env['QUERY_STRING'].to_s.split(/&;/).include?("token=#{@auth_token}")
70
+ end
71
+
72
+ def rack_response(status, body, content_type='application/json')
73
+ headers = {
74
+ 'Content-Type' => content_type,
75
+ 'Content-Length' => body.bytesize.to_s
76
+ }
77
+
78
+ [status, headers, [body]]
79
+ end
72
80
  end
73
81
  end
74
82
  end
data/lib/puma/binder.rb CHANGED
@@ -1,8 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'uri'
2
4
  require 'socket'
3
5
 
4
6
  require 'puma/const'
5
7
  require 'puma/util'
8
+ require 'puma/minissl/context_builder'
6
9
 
7
10
  module Puma
8
11
  class Binder
@@ -40,7 +43,7 @@ module Puma
40
43
  @ios = []
41
44
  end
42
45
 
43
- attr_reader :listeners, :ios
46
+ attr_reader :ios
44
47
 
45
48
  def env(sock)
46
49
  @envs.fetch(sock, @proto_env)
@@ -48,7 +51,6 @@ module Puma
48
51
 
49
52
  def close
50
53
  @ios.each { |i| i.close }
51
- @unix_paths.each { |i| File.unlink i }
52
54
  end
53
55
 
54
56
  def import_from_env
@@ -90,19 +92,29 @@ module Puma
90
92
  case uri.scheme
91
93
  when "tcp"
92
94
  if fd = @inherited_fds.delete(str)
93
- logger.log "* Inherited #{str}"
94
95
  io = inherit_tcp_listener uri.host, uri.port, fd
96
+ logger.log "* Inherited #{str}"
95
97
  elsif sock = @activated_sockets.delete([ :tcp, uri.host, uri.port ])
96
- logger.log "* Activated #{str}"
97
98
  io = inherit_tcp_listener uri.host, uri.port, sock
99
+ logger.log "* Activated #{str}"
98
100
  else
99
101
  params = Util.parse_query uri.query
100
102
 
101
103
  opt = params.key?('low_latency')
102
104
  bak = params.fetch('backlog', 1024).to_i
103
105
 
104
- logger.log "* Listening on #{str}"
105
106
  io = add_tcp_listener uri.host, uri.port, opt, bak
107
+
108
+ @ios.each do |i|
109
+ next unless TCPServer === i
110
+ addr = if i.local_address.ipv6?
111
+ "[#{i.local_address.ip_unpack[0]}]:#{i.local_address.ip_unpack[1]}"
112
+ else
113
+ i.local_address.ip_unpack.join(':')
114
+ end
115
+
116
+ logger.log "* Listening on tcp://#{addr}"
117
+ end
106
118
  end
107
119
 
108
120
  @listeners << [str, io] if io
@@ -110,14 +122,12 @@ module Puma
110
122
  path = "#{uri.host}#{uri.path}".gsub("%20", " ")
111
123
 
112
124
  if fd = @inherited_fds.delete(str)
113
- logger.log "* Inherited #{str}"
114
125
  io = inherit_unix_listener path, fd
126
+ logger.log "* Inherited #{str}"
115
127
  elsif sock = @activated_sockets.delete([ :unix, path ])
116
- logger.log "* Activated #{str}"
117
128
  io = inherit_unix_listener path, sock
129
+ logger.log "* Activated #{str}"
118
130
  else
119
- logger.log "* Listening on #{str}"
120
-
121
131
  umask = nil
122
132
  mode = nil
123
133
  backlog = 1024
@@ -139,76 +149,23 @@ module Puma
139
149
  end
140
150
 
141
151
  io = add_unix_listener path, umask, mode, backlog
152
+ logger.log "* Listening on #{str}"
142
153
  end
143
154
 
144
155
  @listeners << [str, io]
145
156
  when "ssl"
146
157
  params = Util.parse_query uri.query
147
- require 'puma/minissl'
148
-
149
- MiniSSL.check
150
-
151
- ctx = MiniSSL::Context.new
152
-
153
- if defined?(JRUBY_VERSION)
154
- unless params['keystore']
155
- @events.error "Please specify the Java keystore via 'keystore='"
156
- end
157
-
158
- ctx.keystore = params['keystore']
159
-
160
- unless params['keystore-pass']
161
- @events.error "Please specify the Java keystore password via 'keystore-pass='"
162
- end
163
-
164
- ctx.keystore_pass = params['keystore-pass']
165
- ctx.ssl_cipher_list = params['ssl_cipher_list'] if params['ssl_cipher_list']
166
- else
167
- unless params['key']
168
- @events.error "Please specify the SSL key via 'key='"
169
- end
170
-
171
- ctx.key = params['key']
172
-
173
- unless params['cert']
174
- @events.error "Please specify the SSL cert via 'cert='"
175
- end
176
-
177
- ctx.cert = params['cert']
178
-
179
- if ['peer', 'force_peer'].include?(params['verify_mode'])
180
- unless params['ca']
181
- @events.error "Please specify the SSL ca via 'ca='"
182
- end
183
- end
184
-
185
- ctx.ca = params['ca'] if params['ca']
186
- ctx.ssl_cipher_filter = params['ssl_cipher_filter'] if params['ssl_cipher_filter']
187
- end
188
-
189
- if params['verify_mode']
190
- ctx.verify_mode = case params['verify_mode']
191
- when "peer"
192
- MiniSSL::VERIFY_PEER
193
- when "force_peer"
194
- MiniSSL::VERIFY_PEER | MiniSSL::VERIFY_FAIL_IF_NO_PEER_CERT
195
- when "none"
196
- MiniSSL::VERIFY_NONE
197
- else
198
- @events.error "Please specify a valid verify_mode="
199
- MiniSSL::VERIFY_NONE
200
- end
201
- end
158
+ ctx = MiniSSL::ContextBuilder.new(params, @events).context
202
159
 
203
160
  if fd = @inherited_fds.delete(str)
204
161
  logger.log "* Inherited #{str}"
205
162
  io = inherit_ssl_listener fd, ctx
206
163
  elsif sock = @activated_sockets.delete([ :tcp, uri.host, uri.port ])
207
- logger.log "* Activated #{str}"
208
164
  io = inherit_ssl_listener sock, ctx
165
+ logger.log "* Activated #{str}"
209
166
  else
210
- logger.log "* Listening on #{str}"
211
167
  io = add_ssl_listener uri.host, uri.port, ctx
168
+ logger.log "* Listening on #{str}"
212
169
  end
213
170
 
214
171
  @listeners << [str, io] if io
@@ -348,7 +305,7 @@ module Puma
348
305
  # Tell the server to listen on +path+ as a UNIX domain socket.
349
306
  #
350
307
  def add_unix_listener(path, umask=nil, mode=nil, backlog=1024)
351
- @unix_paths << path
308
+ @unix_paths << path unless File.exist? path
352
309
 
353
310
  # Let anyone connect by default
354
311
  umask ||= 0
@@ -386,7 +343,7 @@ module Puma
386
343
  end
387
344
 
388
345
  def inherit_unix_listener(path, fd)
389
- @unix_paths << path
346
+ @unix_paths << path unless File.exist? path
390
347
 
391
348
  if fd.kind_of? TCPServer
392
349
  s = fd
@@ -402,5 +359,27 @@ module Puma
402
359
  s
403
360
  end
404
361
 
362
+ def close_listeners
363
+ @listeners.each do |l, io|
364
+ io.close
365
+ uri = URI.parse(l)
366
+ next unless uri.scheme == 'unix'
367
+ unix_path = "#{uri.host}#{uri.path}"
368
+ File.unlink unix_path if @unix_paths.include? unix_path
369
+ end
370
+ end
371
+
372
+ def close_unix_paths
373
+ @unix_paths.each { |up| File.unlink(up) if File.exist? up }
374
+ end
375
+
376
+ def redirects_for_restart
377
+ redirects = {:close_others => true}
378
+ @listeners.each_with_index do |(l, io), i|
379
+ ENV["PUMA_INHERIT_#{i}"] = "#{io.to_i}:#{l}"
380
+ redirects[io.to_i] = io.to_i
381
+ end
382
+ redirects
383
+ end
405
384
  end
406
385
  end
data/lib/puma/cli.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'optparse'
2
4
  require 'uri'
3
5
 
@@ -159,6 +161,10 @@ module Puma
159
161
  user_config.prune_bundler
160
162
  end
161
163
 
164
+ o.on "--extra-runtime-dependencies GEM1,GEM2", "Defines any extra needed gems when using --prune-bundler" do |arg|
165
+ user_config.extra_runtime_dependencies arg.split(',')
166
+ end
167
+
162
168
  o.on "-q", "--quiet", "Do not log requests internally (default true)" do
163
169
  user_config.quiet
164
170
  end
data/lib/puma/client.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class IO
2
4
  # We need to use this for a jruby work around on both 1.8 and 1.9.
3
5
  # So this either creates the constant (on 1.8), or harmlessly
@@ -7,8 +9,8 @@ class IO
7
9
  end
8
10
 
9
11
  require 'puma/detect'
10
- require 'puma/delegation'
11
12
  require 'tempfile'
13
+ require 'forwardable'
12
14
 
13
15
  if Puma::IS_JRUBY
14
16
  # We have to work around some OpenSSL buffer/io-readiness bugs
@@ -22,19 +24,24 @@ module Puma
22
24
  class ConnectionError < RuntimeError; end
23
25
 
24
26
  # An instance of this class represents a unique request from a client.
25
- # For example a web request from a browser or from CURL. This
27
+ # For example, this could be a web request from a browser or from CURL.
26
28
  #
27
29
  # An instance of `Puma::Client` can be used as if it were an IO object
28
- # for example it is passed into `IO.select` inside of the `Puma::Reactor`.
29
- # This is accomplished by the `to_io` method which gets called on any
30
- # non-IO objects being used with the IO api such as `IO.select.
30
+ # by the reactor. The reactor is expected to call `#to_io`
31
+ # on any non-IO objects it polls. For example, nio4r internally calls
32
+ # `IO::try_convert` (which may call `#to_io`) when a new socket is
33
+ # registered.
31
34
  #
32
35
  # Instances of this class are responsible for knowing if
33
36
  # the header and body are fully buffered via the `try_to_finish` method.
34
37
  # They can be used to "time out" a response via the `timeout_at` reader.
35
38
  class Client
39
+ # The object used for a request with no body. All requests with
40
+ # no body share this one object since it has no state.
41
+ EmptyBody = NullIO.new
42
+
36
43
  include Puma::Const
37
- extend Puma::Delegation
44
+ extend Forwardable
38
45
 
39
46
  def initialize(io, env=nil)
40
47
  @io = io
@@ -52,6 +59,7 @@ module Puma
52
59
  @ready = false
53
60
 
54
61
  @body = nil
62
+ @body_read_start = nil
55
63
  @buffer = nil
56
64
  @tempfile = nil
57
65
 
@@ -62,6 +70,10 @@ module Puma
62
70
 
63
71
  @peerip = nil
64
72
  @remote_addr_header = nil
73
+
74
+ @body_remain = 0
75
+
76
+ @in_last_chunk = false
65
77
  end
66
78
 
67
79
  attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
@@ -71,7 +83,7 @@ module Puma
71
83
 
72
84
  attr_accessor :remote_addr_header
73
85
 
74
- forward :closed?, :@io
86
+ def_delegators :@io, :closed?
75
87
 
76
88
  def inspect
77
89
  "#<Puma::Client:0x#{object_id.to_s(16)} @ready=#{@ready.inspect}>"
@@ -100,6 +112,9 @@ module Puma
100
112
  @tempfile = nil
101
113
  @parsed_bytes = 0
102
114
  @ready = false
115
+ @body_remain = 0
116
+ @peerip = nil
117
+ @in_last_chunk = false
103
118
 
104
119
  if @buffer
105
120
  @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
@@ -112,9 +127,16 @@ module Puma
112
127
  end
113
128
 
114
129
  return false
115
- elsif fast_check &&
116
- IO.select([@to_io], nil, nil, FAST_TRACK_KA_TIMEOUT)
117
- return try_to_finish
130
+ else
131
+ begin
132
+ if fast_check &&
133
+ IO.select([@to_io], nil, nil, FAST_TRACK_KA_TIMEOUT)
134
+ return try_to_finish
135
+ end
136
+ rescue IOError
137
+ # swallow it
138
+ end
139
+
118
140
  end
119
141
  end
120
142
 
@@ -126,180 +148,21 @@ module Puma
126
148
  end
127
149
  end
128
150
 
129
- # The object used for a request with no body. All requests with
130
- # no body share this one object since it has no state.
131
- EmptyBody = NullIO.new
132
-
133
- def setup_chunked_body(body)
134
- @chunked_body = true
135
- @partial_part_left = 0
136
- @prev_chunk = ""
137
-
138
- @body = Tempfile.new(Const::PUMA_TMP_BASE)
139
- @body.binmode
140
- @tempfile = @body
141
-
142
- return decode_chunk(body)
143
- end
144
-
145
- def decode_chunk(chunk)
146
- if @partial_part_left > 0
147
- if @partial_part_left <= chunk.size
148
- @body << chunk[0..(@partial_part_left-3)] # skip the \r\n
149
- chunk = chunk[@partial_part_left..-1]
150
- else
151
- @body << chunk
152
- @partial_part_left -= chunk.size
153
- return false
154
- end
155
- end
156
-
157
- if @prev_chunk.empty?
158
- io = StringIO.new(chunk)
159
- else
160
- io = StringIO.new(@prev_chunk+chunk)
161
- @prev_chunk = ""
162
- end
163
-
164
- while !io.eof?
165
- line = io.gets
166
- if line.end_with?("\r\n")
167
- len = line.strip.to_i(16)
168
- if len == 0
169
- @body.rewind
170
- rest = io.read
171
- @buffer = rest.empty? ? nil : rest
172
- @requests_served += 1
173
- @ready = true
174
- return true
175
- end
176
-
177
- len += 2
178
-
179
- part = io.read(len)
180
-
181
- unless part
182
- @partial_part_left = len
183
- next
184
- end
185
-
186
- got = part.size
187
-
188
- case
189
- when got == len
190
- @body << part[0..-3] # to skip the ending \r\n
191
- when got <= len - 2
192
- @body << part
193
- @partial_part_left = len - part.size
194
- when got == len - 1 # edge where we get just \r but not \n
195
- @body << part[0..-2]
196
- @partial_part_left = len - part.size
197
- end
198
- else
199
- @prev_chunk = line
200
- return false
201
- end
202
- end
203
-
204
- return false
205
- end
206
-
207
- def read_chunked_body
208
- while true
209
- begin
210
- chunk = @io.read_nonblock(4096)
211
- rescue Errno::EAGAIN
212
- return false
213
- rescue SystemCallError, IOError
214
- raise ConnectionError, "Connection error detected during read"
215
- end
216
-
217
- # No chunk means a closed socket
218
- unless chunk
219
- @body.close
220
- @buffer = nil
221
- @requests_served += 1
222
- @ready = true
223
- raise EOFError
224
- end
225
-
226
- return true if decode_chunk(chunk)
227
- end
228
- end
229
-
230
- def setup_body
231
- if @env[HTTP_EXPECT] == CONTINUE
232
- # TODO allow a hook here to check the headers before
233
- # going forward
234
- @io << HTTP_11_100
235
- @io.flush
236
- end
237
-
238
- @read_header = false
239
-
240
- body = @parser.body
241
-
242
- te = @env[TRANSFER_ENCODING2]
243
-
244
- if te && CHUNKED.casecmp(te) == 0
245
- return setup_chunked_body(body)
246
- end
247
-
248
- @chunked_body = false
249
-
250
- cl = @env[CONTENT_LENGTH]
251
-
252
- unless cl
253
- @buffer = body.empty? ? nil : body
254
- @body = EmptyBody
255
- @requests_served += 1
256
- @ready = true
257
- return true
258
- end
259
-
260
- remain = cl.to_i - body.bytesize
261
-
262
- if remain <= 0
263
- @body = StringIO.new(body)
264
- @buffer = nil
265
- @requests_served += 1
266
- @ready = true
267
- return true
268
- end
269
-
270
- if remain > MAX_BODY
271
- @body = Tempfile.new(Const::PUMA_TMP_BASE)
272
- @body.binmode
273
- @tempfile = @body
274
- else
275
- # The body[0,0] trick is to get an empty string in the same
276
- # encoding as body.
277
- @body = StringIO.new body[0,0]
278
- end
279
-
280
- @body.write body
281
-
282
- @body_remain = remain
283
-
284
- return false
285
- end
286
-
287
151
  def try_to_finish
288
152
  return read_body unless @read_header
289
153
 
290
154
  begin
291
155
  data = @io.read_nonblock(CHUNK_SIZE)
292
- rescue Errno::EAGAIN
156
+ rescue IO::WaitReadable
293
157
  return false
294
- rescue SystemCallError, IOError
158
+ rescue SystemCallError, IOError, EOFError
295
159
  raise ConnectionError, "Connection error detected during read"
296
160
  end
297
161
 
298
162
  # No data means a closed socket
299
163
  unless data
300
164
  @buffer = nil
301
- @requests_served += 1
302
- @ready = true
165
+ set_ready
303
166
  raise EOFError
304
167
  end
305
168
 
@@ -335,8 +198,7 @@ module Puma
335
198
  # No data means a closed socket
336
199
  unless data
337
200
  @buffer = nil
338
- @requests_served += 1
339
- @ready = true
201
+ set_ready
340
202
  raise EOFError
341
203
  end
342
204
 
@@ -386,6 +248,92 @@ module Puma
386
248
  true
387
249
  end
388
250
 
251
+ def write_error(status_code)
252
+ begin
253
+ @io << ERROR_RESPONSE[status_code]
254
+ rescue StandardError
255
+ end
256
+ end
257
+
258
+ def peerip
259
+ return @peerip if @peerip
260
+
261
+ if @remote_addr_header
262
+ hdr = (@env[@remote_addr_header] || LOCALHOST_ADDR).split(/[\s,]/).first
263
+ @peerip = hdr
264
+ return hdr
265
+ end
266
+
267
+ @peerip ||= @io.peeraddr.last
268
+ end
269
+
270
+ private
271
+
272
+ def setup_body
273
+ @body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
274
+
275
+ if @env[HTTP_EXPECT] == CONTINUE
276
+ # TODO allow a hook here to check the headers before
277
+ # going forward
278
+ @io << HTTP_11_100
279
+ @io.flush
280
+ end
281
+
282
+ @read_header = false
283
+
284
+ body = @parser.body
285
+
286
+ te = @env[TRANSFER_ENCODING2]
287
+
288
+ if te
289
+ if te.include?(",")
290
+ te.split(",").each do |part|
291
+ if CHUNKED.casecmp(part.strip) == 0
292
+ return setup_chunked_body(body)
293
+ end
294
+ end
295
+ elsif CHUNKED.casecmp(te) == 0
296
+ return setup_chunked_body(body)
297
+ end
298
+ end
299
+
300
+ @chunked_body = false
301
+
302
+ cl = @env[CONTENT_LENGTH]
303
+
304
+ unless cl
305
+ @buffer = body.empty? ? nil : body
306
+ @body = EmptyBody
307
+ set_ready
308
+ return true
309
+ end
310
+
311
+ remain = cl.to_i - body.bytesize
312
+
313
+ if remain <= 0
314
+ @body = StringIO.new(body)
315
+ @buffer = nil
316
+ set_ready
317
+ return true
318
+ end
319
+
320
+ if remain > MAX_BODY
321
+ @body = Tempfile.new(Const::PUMA_TMP_BASE)
322
+ @body.binmode
323
+ @tempfile = @body
324
+ else
325
+ # The body[0,0] trick is to get an empty string in the same
326
+ # encoding as body.
327
+ @body = StringIO.new body[0,0]
328
+ end
329
+
330
+ @body.write body
331
+
332
+ @body_remain = remain
333
+
334
+ return false
335
+ end
336
+
389
337
  def read_body
390
338
  if @chunked_body
391
339
  return read_chunked_body
@@ -403,7 +351,7 @@ module Puma
403
351
 
404
352
  begin
405
353
  chunk = @io.read_nonblock(want)
406
- rescue Errno::EAGAIN
354
+ rescue IO::WaitReadable
407
355
  return false
408
356
  rescue SystemCallError, IOError
409
357
  raise ConnectionError, "Connection error detected during read"
@@ -413,8 +361,7 @@ module Puma
413
361
  unless chunk
414
362
  @body.close
415
363
  @buffer = nil
416
- @requests_served += 1
417
- @ready = true
364
+ set_ready
418
365
  raise EOFError
419
366
  end
420
367
 
@@ -423,8 +370,7 @@ module Puma
423
370
  if remain <= 0
424
371
  @body.rewind
425
372
  @buffer = nil
426
- @requests_served += 1
427
- @ready = true
373
+ set_ready
428
374
  return true
429
375
  end
430
376
 
@@ -433,37 +379,136 @@ module Puma
433
379
  false
434
380
  end
435
381
 
436
- def write_400
437
- begin
438
- @io << ERROR_400_RESPONSE
439
- rescue StandardError
382
+ def read_chunked_body
383
+ while true
384
+ begin
385
+ chunk = @io.read_nonblock(4096)
386
+ rescue IO::WaitReadable
387
+ return false
388
+ rescue SystemCallError, IOError
389
+ raise ConnectionError, "Connection error detected during read"
390
+ end
391
+
392
+ # No chunk means a closed socket
393
+ unless chunk
394
+ @body.close
395
+ @buffer = nil
396
+ set_ready
397
+ raise EOFError
398
+ end
399
+
400
+ if decode_chunk(chunk)
401
+ @env[CONTENT_LENGTH] = @chunked_content_length
402
+ return true
403
+ end
440
404
  end
441
405
  end
442
406
 
443
- def write_408
444
- begin
445
- @io << ERROR_408_RESPONSE
446
- rescue StandardError
407
+ def setup_chunked_body(body)
408
+ @chunked_body = true
409
+ @partial_part_left = 0
410
+ @prev_chunk = ""
411
+
412
+ @body = Tempfile.new(Const::PUMA_TMP_BASE)
413
+ @body.binmode
414
+ @tempfile = @body
415
+
416
+ @chunked_content_length = 0
417
+
418
+ if decode_chunk(body)
419
+ @env[CONTENT_LENGTH] = @chunked_content_length
420
+ return true
447
421
  end
448
422
  end
449
423
 
450
- def write_500
451
- begin
452
- @io << ERROR_500_RESPONSE
453
- rescue StandardError
454
- end
424
+ def write_chunk(str)
425
+ @chunked_content_length += @body.write(str)
455
426
  end
456
427
 
457
- def peerip
458
- return @peerip if @peerip
428
+ def decode_chunk(chunk)
429
+ if @partial_part_left > 0
430
+ if @partial_part_left <= chunk.size
431
+ if @partial_part_left > 2
432
+ write_chunk(chunk[0..(@partial_part_left-3)]) # skip the \r\n
433
+ end
434
+ chunk = chunk[@partial_part_left..-1]
435
+ @partial_part_left = 0
436
+ else
437
+ write_chunk(chunk) if @partial_part_left > 2 # don't include the last \r\n
438
+ @partial_part_left -= chunk.size
439
+ return false
440
+ end
441
+ end
459
442
 
460
- if @remote_addr_header
461
- hdr = (@env[@remote_addr_header] || LOCALHOST_ADDR).split(/[\s,]/).first
462
- @peerip = hdr
463
- return hdr
443
+ if @prev_chunk.empty?
444
+ io = StringIO.new(chunk)
445
+ else
446
+ io = StringIO.new(@prev_chunk+chunk)
447
+ @prev_chunk = ""
464
448
  end
465
449
 
466
- @peerip ||= @io.peeraddr.last
450
+ while !io.eof?
451
+ line = io.gets
452
+ if line.end_with?("\r\n")
453
+ len = line.strip.to_i(16)
454
+ if len == 0
455
+ @in_last_chunk = true
456
+ @body.rewind
457
+ rest = io.read
458
+ last_crlf_size = "\r\n".bytesize
459
+ if rest.bytesize < last_crlf_size
460
+ @buffer = nil
461
+ @partial_part_left = last_crlf_size - rest.bytesize
462
+ return false
463
+ else
464
+ @buffer = rest[last_crlf_size..-1]
465
+ @buffer = nil if @buffer.empty?
466
+ set_ready
467
+ return true
468
+ end
469
+ end
470
+
471
+ len += 2
472
+
473
+ part = io.read(len)
474
+
475
+ unless part
476
+ @partial_part_left = len
477
+ next
478
+ end
479
+
480
+ got = part.size
481
+
482
+ case
483
+ when got == len
484
+ write_chunk(part[0..-3]) # to skip the ending \r\n
485
+ when got <= len - 2
486
+ write_chunk(part)
487
+ @partial_part_left = len - part.size
488
+ when got == len - 1 # edge where we get just \r but not \n
489
+ write_chunk(part[0..-2])
490
+ @partial_part_left = len - part.size
491
+ end
492
+ else
493
+ @prev_chunk = line
494
+ return false
495
+ end
496
+ end
497
+
498
+ if @in_last_chunk
499
+ set_ready
500
+ true
501
+ else
502
+ false
503
+ end
504
+ end
505
+
506
+ def set_ready
507
+ if @body_read_start
508
+ @env['puma.request_body_wait'] = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) - @body_read_start
509
+ end
510
+ @requests_served += 1
511
+ @ready = true
467
512
  end
468
513
  end
469
514
  end