yahns 1.12.5 → 1.13.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/Documentation/yahns-rackup.pod +0 -10
  3. data/Documentation/yahns_config.pod +3 -0
  4. data/GIT-VERSION-FILE +1 -1
  5. data/GIT-VERSION-GEN +1 -1
  6. data/NEWS +80 -0
  7. data/examples/init.sh +34 -9
  8. data/examples/logrotate.conf +5 -0
  9. data/examples/yahns.socket +17 -0
  10. data/examples/yahns@.service +50 -0
  11. data/examples/yahns_rack_basic.conf.rb +0 -6
  12. data/extras/autoindex.rb +3 -2
  13. data/extras/exec_cgi.rb +1 -0
  14. data/extras/try_gzip_static.rb +19 -5
  15. data/lib/yahns/chunk_body.rb +27 -0
  16. data/lib/yahns/fdmap.rb +7 -4
  17. data/lib/yahns/http_client.rb +39 -10
  18. data/lib/yahns/http_response.rb +41 -22
  19. data/lib/yahns/openssl_client.rb +7 -3
  20. data/lib/yahns/proxy_http_response.rb +132 -159
  21. data/lib/yahns/proxy_pass.rb +6 -170
  22. data/lib/yahns/queue_epoll.rb +1 -0
  23. data/lib/yahns/queue_kqueue.rb +1 -0
  24. data/lib/yahns/req_res.rb +164 -0
  25. data/lib/yahns/server.rb +2 -1
  26. data/lib/yahns/server_mp.rb +1 -1
  27. data/lib/yahns/version.rb +1 -1
  28. data/lib/yahns/wbuf.rb +5 -6
  29. data/lib/yahns/wbuf_common.rb +5 -10
  30. data/lib/yahns/wbuf_lite.rb +111 -0
  31. data/man/yahns-rackup.1 +29 -29
  32. data/man/yahns_config.5 +47 -35
  33. data/test/helper.rb +12 -0
  34. data/test/test_auto_chunk.rb +56 -0
  35. data/test/test_extras_exec_cgi.rb +1 -3
  36. data/test/test_extras_try_gzip_static.rb +30 -16
  37. data/test/test_output_buffering.rb +5 -1
  38. data/test/test_proxy_pass.rb +2 -2
  39. data/test/test_proxy_pass_no_buffering.rb +170 -0
  40. data/test/test_reopen_logs.rb +5 -1
  41. data/test/test_response.rb +42 -0
  42. data/test/test_server.rb +35 -0
  43. data/test/test_ssl.rb +0 -6
  44. data/test/test_tmpio.rb +4 -0
  45. data/test/test_wbuf.rb +11 -4
  46. metadata +10 -4
  47. data/lib/yahns/sendfile_compat.rb +0 -24
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b9e19ad9295dcdd4fb3423900a4061e9a4495597
4
- data.tar.gz: e98ed154740a92a0378b0ddc255561bcda1ea391
3
+ metadata.gz: '0885938d8049ca0b240e7727bcf47c9558945997'
4
+ data.tar.gz: 0b2a81c5a297241967f3f184a755c80d51f90a34
5
5
  SHA512:
6
- metadata.gz: 5e80a5f1d8ec22f04c7e80a1d128e18a6c05923ad2c850cafdae763c50e03c29242279160d73de267593789ab09b5e31beff797d177f5ff57aba7939de174480
7
- data.tar.gz: 44fec12611e1e0f8f83868879b53c8fb9186e61c8be1bc5b020c4879ed25fdbbe5c1eb3aabe650e7b2270d7c4dcb6b77f1f21949926755987f1fe1bd05a37f0f
6
+ metadata.gz: a5fa096ca664e7f224f933a5c37b437a15617decfca04415aa8928d33cd733fe700b0544966392929568c5dcacb25016971d7aab294c5d33a9e4907c66b5d60a
7
+ data.tar.gz: 7dea50f747b2f7214c1de823fcb584b6da6738cb2c34ba8b5676a86636dddb322af50b22119a7a38b075fae692fb507feef3294e34538bd0ba0a88e6fb7a372a
@@ -159,16 +159,6 @@ The RACK_ENV variable is set by the aforementioned -E switch.
159
159
  If RACK_ENV is already set, it will be used unless -E is used.
160
160
  See rackup documentation for more details.
161
161
 
162
- =head1 CAVEATS
163
-
164
- yahns is strict about buggy, non-compliant Rack applications.
165
- Some existing servers work fine without "Content-Length" or
166
- "Transfer-Encoding: chunked" response headers enforced by Rack::Lint.
167
- Forgetting these headers with yahns causes clients to stall as they
168
- assume more data is coming. Loading the Rack::ContentLength and/or
169
- Rack::Chunked middlewares will set the necessary response headers
170
- and fix your app.
171
-
172
162
  =head1 CONTACT
173
163
 
174
164
  All feedback welcome via plain-text mail to L<mailto:yahns-public@yhbt.net>
@@ -451,6 +451,9 @@ An example which seems to work is:
451
451
  # but disable client certificate verification as it is rare:
452
452
  ssl_ctx.set_params(verify_mode: OpenSSL::SSL::VERIFY_NONE)
453
453
 
454
+ # Built-in session cache (only works if worker_processes is nil or 1)
455
+ ssl_ctx.session_cache_mode = OpenSSL::SSL::SSLContext::SESSION_CACHE_SERVER
456
+
454
457
  app(:rack, "/path/to/my/app/config.ru") do
455
458
  listen 443, ssl_ctx: ssl_ctx
456
459
  end
data/GIT-VERSION-FILE CHANGED
@@ -1 +1 @@
1
- VERSION = 1.12.5
1
+ VERSION = 1.13.0
data/GIT-VERSION-GEN CHANGED
@@ -5,7 +5,7 @@
5
5
  CONSTANT = "Yahns::VERSION"
6
6
  RVF = "lib/yahns/version.rb"
7
7
  GVF = "GIT-VERSION-FILE"
8
- DEF_VER = "v1.12.5"
8
+ DEF_VER = "v1.13.0"
9
9
  vn = DEF_VER.dup
10
10
 
11
11
  # First see if there is a version file (included in release tarballs),
data/NEWS CHANGED
@@ -1,3 +1,83 @@
1
+ === yahns 1.13.0 - some user-visible improvements... / 2016-08-05 07:26 UTC
2
+
3
+ And probably a billion new regressions!
4
+
5
+ yahns now allows users to skip the Rack::Head, Rack::Chunked and
6
+ Rack::ContentLength middlewares to ease migrating from/to other
7
+ real-world Rack HTTP servers. Most notably, our chunked
8
+ encoding implementation is a bit faster than Rack::Chunked by
9
+ taking advantage of the writev(2) syscall:
10
+
11
+ https://yhbt.net/yahns-public/20160803031906.14553-4-e@80x24.org/
12
+
13
+ There's also rack 2.x fixes in the test case and extras/ section
14
+ (these incompatibilities did not affect existing users unless
15
+ they use the wonky extras/ section).
16
+
17
+ There's also some graceful shutdown fixes, the process title is
18
+ now changed to display the number of live FDs.
19
+
20
+ Of course, there's the usual round of documentation improvements
21
+ which are systemd and OpenSSL setup-related this time around.
22
+
23
+ However, the majority of changes (proxy_*, wbuf_lite), affect
24
+ currently-unadvertised functionality which is subject to removal
25
+ or incompatible config changes. However, they are used to serve
26
+ our mailing list archives at:
27
+
28
+ https://yhbt.net/yahns-public/
29
+
30
+ 49 changes since yahns 1.12.5:
31
+ proxy_pass: simplify writing request bodies upstream
32
+ proxy_pass: hoist out proxy_res_headers method
33
+ proxy_pass: simplify proxy_http_response
34
+ proxy_pass: split out body and trailer reading in response
35
+ proxy_pass: trim down proxy_response_finish, too
36
+ proxy_pass: split out req_res into a separate file
37
+ proxy_pass: fix resumes after complete buffering is unblocked
38
+ proxy_pass: X-Forwarded-For appends to existing list
39
+ proxy_pass: pass entire object to proxy_http_response
40
+ proxy_pass: support "proxy_buffering: false"
41
+ proxy_pass: remove unnecessary rescue
42
+ req_res: store proxy_pass object here, instead
43
+ proxy_pass: redo "proxy_buffering: false"
44
+ wbuf: remove needless "busy" parameter
45
+ Merge branch 'maint'
46
+ extras/try_gzip_static: do not show backtrace on syscall errors
47
+ wbuf: remove tmpdir parameter
48
+ wbuf_lite: fix write retries for OpenSSL sockets
49
+ test_proxy_pass_no_buffering: fix racy test
50
+ queue_*: check for closed IO objects
51
+ cleanup graceful shutdown handling
52
+ proxy_pass: more descriptive error messages
53
+ proxy_pass: fix HTTP/1.0 backends on EOF w/o buffering
54
+ wbuf_common: reset offset counter when done
55
+ extras/try_gzip_static: resolve symlinks
56
+ test_ssl: remove unnecessary priv_key DH parameter
57
+ openssl_client: wrap shutdown for graceful termination
58
+ proxy_pass: keep trailer buffer on blocked client writes
59
+ proxy_pass: avoid TOCTTOU race when unbuffering, too
60
+ proxy_pass: avoid accessing logger in env after hijacking
61
+ proxy_pass: avoid stuck responses in "proxy_buffering: false"
62
+ extras: include status messages in responses
63
+ update init and add systemd examples
64
+ test_proxy_pass_no_buffering: exclude rb/ru files, too
65
+ wbuf_lite: use StringIO instead of TmpIO
66
+ wbuf_lite: truncate StringIO when done
67
+ wbuf_lite: prevent clobbering responses
68
+ wbuf_lite: unify EOF error handling
69
+ wbuf_lite: reset sf_offset/sf_count consistently
70
+ wbuf_lite: clear @busy flag when re-arming
71
+ http_response: drop bodies for non-compliant responses
72
+ fix rack 2.x compatibility bugs
73
+ doc: add session cache usage to OpenSSL example
74
+ test: skip some buffering tests on non-default values
75
+ response: drop clients after HTTP responses of unknown length
76
+ response: reduce stack overhead for parameter passing
77
+ response: support auto-chunking for HTTP/1.1
78
+ Revert "document Rack::Chunked/ContentLength semi-requirements"
79
+ extras/exec_cgi: fix for HTTPoxy vulnerability
80
+
1
81
  === yahns 1.12.5 - proxy_pass + rack.hijack fixes / 2016-06-05 23:09 UTC
2
82
 
3
83
  Hopefully the last of the 1.12.x series, this release
data/examples/init.sh CHANGED
@@ -2,8 +2,14 @@
2
2
  # To the extent possible under law, Eric Wong has waived all copyright and
3
3
  # related or neighboring rights to this examples
4
4
  set -e
5
- # Example init script, this can be used with nginx, too,
6
- # since nginx and yahns accept the same signals
5
+ ### BEGIN INIT INFO
6
+ # Provides: yahns
7
+ # Required-Start: $local_fs $network
8
+ # Required-Stop: $local_fs $network
9
+ # Default-Start: 2 3 4 5
10
+ # Default-Stop: 0 1 6
11
+ # Short-Description: Start/stop yahns Ruby app server
12
+ ### END INIT INFO
7
13
 
8
14
  # Feel free to change any of the following variables for your app:
9
15
  TIMEOUT=${TIMEOUT-60}
@@ -11,21 +17,22 @@ APP_ROOT=/home/x/my_app/current
11
17
  PID=$APP_ROOT/tmp/pids/yahns.pid
12
18
  CMD="/usr/bin/yahns -D -c $APP_ROOT/config/yahns.rb"
13
19
  INIT_CONF=$APP_ROOT/config/init.conf
20
+ UPGRADE_DELAY=${UPGRADE_DELAY-2}
14
21
  action="$1"
15
22
  set -u
16
23
 
17
24
  test -f "$INIT_CONF" && . $INIT_CONF
18
25
 
19
- old_pid="$PID.oldbin"
26
+ OLD="$PID.oldbin"
20
27
 
21
28
  cd $APP_ROOT || exit 1
22
29
 
23
30
  sig () {
24
- test -s "$PID" && kill -$1 `cat $PID`
31
+ test -s "$PID" && kill -$1 $(cat $PID)
25
32
  }
26
33
 
27
34
  oldsig () {
28
- test -s $old_pid && kill -$1 `cat $old_pid`
35
+ test -s "$OLD" && kill -$1 $(cat $OLD)
29
36
  }
30
37
 
31
38
  case $action in
@@ -47,18 +54,36 @@ restart|reload)
47
54
  $CMD
48
55
  ;;
49
56
  upgrade)
50
- if sig USR2 && sleep 2 && sig 0 && oldsig QUIT
57
+ if oldsig 0
58
+ then
59
+ echo >&2 "Old upgraded process still running with $OLD"
60
+ exit 1
61
+ fi
62
+
63
+ cur_pid=
64
+ if test -s "$PID"
65
+ then
66
+ cur_pid=$(cat $PID)
67
+ fi
68
+
69
+ if test -n "$cur_pid" &&
70
+ kill -USR2 "$cur_pid" &&
71
+ sleep $UPGRADE_DELAY &&
72
+ new_pid=$(cat $PID) &&
73
+ test x"$new_pid" != x"$cur_pid" &&
74
+ kill -0 "$new_pid" &&
75
+ kill -QUIT "$cur_pid"
51
76
  then
52
77
  n=$TIMEOUT
53
- while test -s $old_pid && test $n -ge 0
78
+ while kill -0 "$cur_pid" 2>/dev/null && test $n -ge 0
54
79
  do
55
80
  printf '.' && sleep 1 && n=$(( $n - 1 ))
56
81
  done
57
82
  echo
58
83
 
59
- if test $n -lt 0 && test -s $old_pid
84
+ if test $n -lt 0 && kill -0 "$cur_pid" 2>/dev/null
60
85
  then
61
- echo >&2 "$old_pid still exists after $TIMEOUT seconds"
86
+ echo >&2 "$cur_pid still running after $TIMEOUT seconds"
62
87
  exit 1
63
88
  fi
64
89
  exit 0
@@ -25,6 +25,11 @@
25
25
  # config. yahns supports the USR1 signal and we send it
26
26
  # as our "lastaction" action:
27
27
  lastaction
28
+ # systemd users do not have PID files,
29
+ # only signal the @1 process since the @2 is short-lived
30
+ # and only runs while @1 is restarting.
31
+ systemctl kill -s SIGUSR1 yahns@1.service
32
+
28
33
  # assuming your pid file is in /var/run/yahns_app/pid
29
34
  pid=/var/run/yahns_app/pid
30
35
  test -s $pid && kill -USR1 "$(cat $pid)"
@@ -0,0 +1,17 @@
1
+ # ==> /etc/systemd/system/yahns.socket <==
2
+ [Unit]
3
+ Description = yahns sockets
4
+
5
+ [Socket]
6
+
7
+ # yahns can handle an arbitrary number of listen sockets,
8
+ # so I prefer to keep listeners for IPv4 and IPv6 separate
9
+ # to avoid ugly IPv4-mapped-IPv6 addresses for IPv4 clients:
10
+ # (e.g ":ffff:10.0.0.1" instead of just "10.0.0.1").
11
+ ListenStream = 0.0.0.0:443
12
+ BindIPv6Only = ipv6-only
13
+ ListenStream = [::]:443
14
+ Service = yahns@1.service
15
+
16
+ [Install]
17
+ WantedBy = sockets.target
@@ -0,0 +1,50 @@
1
+ # ==> /etc/systemd/system/yahns@.service <==
2
+ # Since SIGUSR2 upgrades do not work under systemd, this service
3
+ # file allows starting two (or more) simultaneous services
4
+ # during upgrade (e.g. yahns@1 and yahns@2) with the intention
5
+ # that they are both running during the upgrade process.
6
+ #
7
+ # This allows upgrading without downtime, using yahns@2 as a
8
+ # temporary hot spare:
9
+ #
10
+ # systemctl start yahns@2
11
+ # sleep 2 # wait for yahns@2 to boot, increase as necessary for big apps
12
+ # systemctl restart yahns@1
13
+ # sleep 2 # wait for yahns@1 to warmup
14
+ # systemctl stop yahns@2
15
+
16
+ [Unit]
17
+ Description = yahns Ruby server %i
18
+ Wants = yahns.socket
19
+ After = yahns.socket
20
+
21
+ [Service]
22
+ # yahns can handle lots of open files:
23
+ LimitNOFILE = 32768
24
+ LimitCORE = infinity
25
+
26
+ # The listen socket we give yahns should be blocking for optimal
27
+ # load distribution between processes under the Linux kernel.
28
+ # NonBlocking is false by default in systemd, but we specify it
29
+ # here anyways to discourage users from blindly changing it.
30
+ Sockets = yahns.socket
31
+ NonBlocking = false
32
+
33
+ # bundler users must use the "--keep-file-descriptors" switch, here:
34
+ # ExecStart = /path/to/bin/bundle exec --keep-file-descriptors yahns -c ...
35
+ ExecStart = /path/to/bin/yahns -c /path/to/yahns.conf.rb
36
+ KillSignal = SIGQUIT
37
+ User = www-data
38
+ Group = www-data
39
+ ExecReload = /bin/kill -HUP $MAINPID
40
+
41
+ # this should match the shutdown_timeout value in yahns_config(5)
42
+ TimeoutStopSec = 600
43
+
44
+ # Only kill the master process, it may be harmful to signal
45
+ # workers via default "control-group" setting since some
46
+ # Ruby extensions and applications misbehave on interrupts
47
+ KillMode = process
48
+
49
+ [Install]
50
+ WantedBy = multi-user.target
@@ -30,12 +30,6 @@ queue do
30
30
  worker_threads 50
31
31
  end
32
32
 
33
- # note: Rack requires responses set "Content-Length" or use
34
- # "Transfer-Encoding: chunked". Some Rack servers tolerate
35
- # the lack of these, yahns does not. Thus you should load
36
- # Rack::Chunked and/or Rack::ContentLength middleware in your
37
- # config.ru to ensure clients know when your application
38
- # responses terminate.
39
33
  app(:rack, "config.ru", preload: false) do
40
34
  listen 80
41
35
 
data/extras/autoindex.rb CHANGED
@@ -168,8 +168,9 @@ class Autoindex
168
168
  if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include?(code)
169
169
  [ code, {}, [] ]
170
170
  else
171
- h = { 'Content-Type' => 'text/plain', 'Content-Length' => '0' }
172
- [ code, h, [] ]
171
+ msg = "#{code} #{Rack::Utils::HTTP_STATUS_CODES[code.to_i]}\n"
172
+ h = { 'Content-Type' => 'text/plain', 'Content-Length' => msg.size.to_s }
173
+ [ code, h, [ msg ] ]
173
174
  end
174
175
  end
175
176
 
data/extras/exec_cgi.rb CHANGED
@@ -86,6 +86,7 @@ class ExecCgi
86
86
 
87
87
  # Calls the app
88
88
  def call(env)
89
+ env.delete('HTTP_PROXY') # ref: https://httpoxy.org/
89
90
  cgi_env = { "GATEWAY_INTERFACE" => "CGI/1.1" }
90
91
  PASS_VARS.each { |key| val = env[key] and cgi_env[key] = val }
91
92
  env.each { |key,val| cgi_env[key] = val if key =~ /\AHTTP_/ }
@@ -49,7 +49,7 @@ class TryGzipStatic
49
49
  end
50
50
 
51
51
  size = st.size
52
- ranges = Rack::Utils.byte_ranges(env, size)
52
+ ranges = byte_ranges(env, size)
53
53
  if ranges.nil? || ranges.length > 1
54
54
  [ 200 ] # serve the whole thing, possibly with static gzip \o/
55
55
  elsif ranges.empty?
@@ -92,7 +92,12 @@ class TryGzipStatic
92
92
  def stat_path(env)
93
93
  path = fspath(env) or return r(403)
94
94
  begin
95
- st = File.stat(path)
95
+ st = File.lstat(path)
96
+ if st.symlink?
97
+ path = File.readlink(path)
98
+ path[0] == '/'.freeze or path = "#@root/#{path}"
99
+ st = File.stat(path)
100
+ end
96
101
  return r(404) unless st.file?
97
102
  return r(403) unless st.readable?
98
103
  [ path, st ]
@@ -203,7 +208,7 @@ class TryGzipStatic
203
208
  msg = msg.dump if /[[:cntrl:]]/ =~ msg # prevent code injection
204
209
  logger.warn("#{env['REQUEST_METHOD']} #{env['PATH_INFO']} " \
205
210
  "#{code} #{msg}")
206
- if exc.respond_to?(:backtrace)
211
+ if exc.respond_to?(:backtrace) && !(SystemCallError === exc)
207
212
  exc.backtrace.each { |line| logger.warn(line) }
208
213
  end
209
214
  end
@@ -211,8 +216,17 @@ class TryGzipStatic
211
216
  if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include?(code)
212
217
  [ code, {}, [] ]
213
218
  else
214
- h = { 'Content-Type' => 'text/plain', 'Content-Length' => '0' }
215
- [ code, h, [] ]
219
+ msg = "#{code} #{Rack::Utils::HTTP_STATUS_CODES[code.to_i]}\n"
220
+ h = { 'Content-Type' => 'text/plain', 'Content-Length' => msg.size.to_s }
221
+ [ code, h, [ msg ] ]
222
+ end
223
+ end
224
+
225
+ if Rack::Utils.respond_to?(:get_byte_ranges) # rack 2.0+
226
+ def byte_ranges(env, size)
227
+ Rack::Utils.get_byte_ranges(env['HTTP_RANGE'], size)
216
228
  end
229
+ else # rack 1.x
230
+ def byte_ranges(env, size); Rack::Utils.byte_ranges(env, size); end
217
231
  end
218
232
  end
@@ -0,0 +1,27 @@
1
+ # -*- encoding: binary -*-
2
+ # Copyright (C) 2016 all contributors <yahns-public@yhbt.net>
3
+ # License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
4
+ # frozen_string_literal: true
5
+ class Yahns::ChunkBody
6
+ def initialize(body, vec)
7
+ @body = body
8
+ @vec = vec
9
+ end
10
+
11
+ def each
12
+ vec = @vec
13
+ vec[2] = "\r\n".freeze
14
+ @body.each do |chunk|
15
+ vec[0] = "#{chunk.bytesize.to_s(16)}\r\n"
16
+ vec[1] = chunk
17
+ # vec[2] never changes: "\r\n" above
18
+ yield vec
19
+ end
20
+ vec.clear
21
+ yield "0\r\n\r\n".freeze
22
+ end
23
+
24
+ def close
25
+ @body.close if @body.respond_to?(:close)
26
+ end
27
+ end
data/lib/yahns/fdmap.rb CHANGED
@@ -89,10 +89,10 @@ class Yahns::Fdmap # :nodoc:
89
89
  # We should not be calling this too frequently, it is expensive
90
90
  # This is called while @fdmap_mtx is held
91
91
  def __expire(timeout)
92
- return if @count == 0
92
+ return 0 if @count == 0
93
93
  nr = 0
94
94
  now = Yahns.now
95
- (now - @last_expire) >= 1.0 or return # don't expire too frequently
95
+ (now - @last_expire) >= 1.0 or return @count # don't expire too frequently
96
96
 
97
97
  # @fdmap_ary may be huge, so always expire a bunch at once to
98
98
  # avoid getting to this method too frequently
@@ -104,8 +104,11 @@ class Yahns::Fdmap # :nodoc:
104
104
  end
105
105
 
106
106
  @last_expire = Yahns.now
107
- msg = timeout ? "timeout=#{timeout}" : "client_timeout"
108
- @logger.info("dropping #{nr} of #@count clients for #{msg}")
107
+ if nr != 0
108
+ msg = timeout ? "timeout=#{timeout}" : "client_timeout"
109
+ @logger.info("dropping #{nr} of #@count clients for #{msg}")
110
+ end
111
+ @count
109
112
  end
110
113
 
111
114
  # used for graceful shutdown
@@ -2,6 +2,12 @@
2
2
  # Copyright (C) 2013-2016 all contributors <yahns-public@yhbt.net>
3
3
  # License: GPL-3.0+ (https://www.gnu.org/licenses/gpl-3.0.txt)
4
4
  # frozen_string_literal: true
5
+ begin
6
+ raise LoadError, 'SENDFILE_BROKEN env set' if ENV['SENDFILE_BROKEN']
7
+ require 'sendfile'
8
+ rescue LoadError
9
+ end
10
+
5
11
  class Yahns::HttpClient < Kgio::Socket # :nodoc:
6
12
  NULL_IO = StringIO.new(''.dup) # :nodoc:
7
13
 
@@ -183,9 +189,11 @@ class Yahns::HttpClient < Kgio::Socket # :nodoc:
183
189
  mkinput_preread # keep looping (@state == :body)
184
190
  true
185
191
  else # :lazy, false
186
- status, headers, body = k.app.call(env = @hs.env)
187
- return :ignore if app_hijacked?(env, body)
188
- http_response_write(status, headers, body)
192
+ env = @hs.env
193
+ opt = http_response_prep(env)
194
+ res = k.app.call(env)
195
+ return :ignore if app_hijacked?(env, res)
196
+ http_response_write(res, opt)
189
197
  end
190
198
  end
191
199
 
@@ -214,16 +222,17 @@ class Yahns::HttpClient < Kgio::Socket # :nodoc:
214
222
  env['SERVER_PORT'] = '443'.freeze
215
223
  end
216
224
 
225
+ opt = http_response_prep(env)
217
226
  # run the rack app
218
- status, headers, body = k.app.call(env)
219
- return :ignore if app_hijacked?(env, body)
220
- if status.to_i == 100
227
+ res = k.app.call(env)
228
+ return :ignore if app_hijacked?(env, res)
229
+ if res[0].to_i == 100
221
230
  rv = http_100_response(env) and return rv
222
- status, headers, body = k.app.call(env)
231
+ res = k.app.call(env)
223
232
  end
224
233
 
225
234
  # this returns :wait_readable, :wait_writable, :ignore, or nil:
226
- http_response_write(status, headers, body)
235
+ http_response_write(res, opt)
227
236
  end
228
237
 
229
238
  # called automatically by kgio_write
@@ -299,9 +308,29 @@ class Yahns::HttpClient < Kgio::Socket # :nodoc:
299
308
  return # always drop the connection on uncaught errors
300
309
  end
301
310
 
302
- def app_hijacked?(env, body)
311
+ def app_hijacked?(env, res)
303
312
  return false unless env.include?('rack.hijack_io'.freeze)
304
- body.close if body.respond_to?(:close)
313
+ res[2].close if res && res[2].respond_to?(:close)
305
314
  true
306
315
  end
316
+
317
+ def trysendio(io, offset, count)
318
+ return 0 if count == 0
319
+ count = 0x4000 if count > 0x4000
320
+ buf = Thread.current[:yahns_sfbuf] ||= ''.dup
321
+ io.pos = offset
322
+ str = io.read(count, buf) or return # nil for EOF
323
+ n = 0
324
+ case rv = kgio_trywrite(str)
325
+ when String # partial write, keep trying
326
+ n += (str.size - rv.size)
327
+ str = rv
328
+ when :wait_writable, :wait_readable
329
+ return n > 0 ? n : rv
330
+ when nil
331
+ return n + str.size # yay!
332
+ end while true
333
+ end
334
+
335
+ alias trysendfile trysendio unless IO.instance_methods.include?(:trysendfile)
307
336
  end
@@ -4,12 +4,14 @@
4
4
  # frozen_string_literal: true
5
5
  require_relative 'stream_file'
6
6
  require_relative 'wbuf_str'
7
+ require_relative 'chunk_body'
7
8
 
8
9
  # Writes a Rack response to your client using the HTTP/1.1 specification.
9
10
  # You use it by simply doing:
10
11
  #
11
- # status, headers, body = rack_app.call(env)
12
- # http_response_write(status, headers, body)
12
+ # opt = http_response_prep(env)
13
+ # res = rack_app.call(env)
14
+ # http_response_write(res, opt)
13
15
  #
14
16
  # Most header correctness (including Content-Length and Content-Type)
15
17
  # is the job of Rack, with the exception of the "Date" header.
@@ -57,12 +59,12 @@ module Yahns::HttpResponse # :nodoc:
57
59
  "#{response_start}#{code} #{Rack::Utils::HTTP_STATUS_CODES[code]}\r\n\r\n"
58
60
  end
59
61
 
60
- def response_header_blocked(ret, header, body, alive, offset, count)
62
+ def response_header_blocked(header, body, alive, offset, count)
61
63
  if body.respond_to?(:to_path)
62
64
  alive = Yahns::StreamFile.new(body, alive, offset, count)
63
65
  body = nil
64
66
  end
65
- wbuf = Yahns::Wbuf.new(body, alive, self.class.output_buffer_tmpdir, ret)
67
+ wbuf = Yahns::Wbuf.new(body, alive)
66
68
  rv = wbuf.wbuf_write(self, header)
67
69
  if body && ! alive.respond_to?(:call) # skip body.each if hijacked
68
70
  body.each { |chunk| rv = wbuf.wbuf_write(self, chunk) }
@@ -118,21 +120,20 @@ module Yahns::HttpResponse # :nodoc:
118
120
  end
119
121
  end
120
122
 
121
- def have_more?(value)
122
- value.to_i > 0 && @hs.env['REQUEST_METHOD'] != 'HEAD'.freeze
123
- end
124
-
125
123
  # writes the rack_response to socket as an HTTP response
126
124
  # returns :wait_readable, :wait_writable, :forget, or nil
127
- def http_response_write(status, headers, body)
125
+ def http_response_write(res, opt)
126
+ status, headers, body = res
128
127
  offset = 0
129
128
  count = hijack = nil
130
- k = self.class
131
- alive = @hs.next? && k.persistent_connections
129
+ alive = @hs.next? && self.class.persistent_connections
132
130
  flags = MSG_DONTWAIT
131
+ term = false
132
+ hdr_only, chunk_ok = opt
133
133
 
134
134
  if @hs.headers?
135
135
  code = status.to_i
136
+ hdr_only ||= Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include?(code)
136
137
  msg = Rack::Utils::HTTP_STATUS_CODES[code]
137
138
  buf = "#{response_start}#{msg ? %Q(#{code} #{msg}) : status}\r\n" \
138
139
  "Date: #{httpdate}\r\n".dup
@@ -150,7 +151,11 @@ module Yahns::HttpResponse # :nodoc:
150
151
  # allow Rack apps to tell us they want to drop the client
151
152
  alive = false if value =~ /\bclose\b/i
152
153
  when %r{\AContent-Length\z}i
153
- flags |= MSG_MORE if have_more?(value)
154
+ term = true
155
+ flags |= MSG_MORE if value.to_i > 0 && !hdr_only
156
+ kv_str(buf, key, value)
157
+ when %r{\ATransfer-Encoding\z}i
158
+ term = true if value =~ /\bchunked\b/i
154
159
  kv_str(buf, key, value)
155
160
  when "rack.hijack"
156
161
  hijack = value
@@ -158,6 +163,12 @@ module Yahns::HttpResponse # :nodoc:
158
163
  kv_str(buf, key, value)
159
164
  end
160
165
  end
166
+ if !term && chunk_ok
167
+ term = true
168
+ body = Yahns::ChunkBody.new(body, opt)
169
+ buf << "Transfer-Encoding: chunked\r\n".freeze
170
+ end
171
+ alive &&= term
161
172
  buf << (alive ? "Connection: keep-alive\r\n\r\n".freeze
162
173
  : "Connection: close\r\n\r\n".freeze)
163
174
  case rv = kgio_syssend(buf, flags)
@@ -169,9 +180,9 @@ module Yahns::HttpResponse # :nodoc:
169
180
  flags = MSG_DONTWAIT
170
181
  buf = rv # unlikely, hope the skb grows
171
182
  when :wait_writable, :wait_readable # unlikely
172
- if k.output_buffering
183
+ if self.class.output_buffering
173
184
  alive = hijack ? hijack : alive
174
- rv = response_header_blocked(rv, buf, body, alive, offset, count)
185
+ rv = response_header_blocked(buf, body, alive, offset, count)
175
186
  body = nil # ensure we do not close body in ensure
176
187
  return rv
177
188
  else
@@ -181,6 +192,7 @@ module Yahns::HttpResponse # :nodoc:
181
192
  end
182
193
 
183
194
  return response_hijacked(hijack) if hijack
195
+ return http_response_done(alive) if hdr_only
184
196
 
185
197
  if body.respond_to?(:to_path)
186
198
  @state = body = Yahns::StreamFile.new(body, alive, offset, count)
@@ -188,19 +200,19 @@ module Yahns::HttpResponse # :nodoc:
188
200
  end
189
201
 
190
202
  wbuf = rv = nil
191
- body.each do |chunk|
203
+ body.each do |x|
192
204
  if wbuf
193
- rv = wbuf.wbuf_write(self, chunk)
205
+ rv = wbuf.wbuf_write(self, x)
194
206
  else
195
- case rv = kgio_trywrite(chunk)
207
+ case rv = String === x ? kgio_trywrite(x) : kgio_trywritev(x)
196
208
  when nil # all done, likely and good!
197
209
  break
198
- when String
199
- chunk = rv # hope the skb grows when we loop into the trywrite
210
+ when String, Array
211
+ x = rv # hope the skb grows when we loop into the trywrite
200
212
  when :wait_writable, :wait_readable
201
- if k.output_buffering
202
- wbuf = Yahns::Wbuf.new(body, alive, k.output_buffer_tmpdir, rv)
203
- rv = wbuf.wbuf_write(self, chunk)
213
+ if self.class.output_buffering
214
+ wbuf = Yahns::Wbuf.new(body, alive)
215
+ rv = wbuf.wbuf_write(self, x)
204
216
  break
205
217
  else
206
218
  response_wait_write(rv) or return :close
@@ -273,4 +285,11 @@ module Yahns::HttpResponse # :nodoc:
273
285
  return rv
274
286
  end while true
275
287
  end
288
+
289
+ # must be called before app dispatch, since the app can
290
+ # do all sorts of nasty things to env
291
+ def http_response_prep(env)
292
+ [ env['REQUEST_METHOD'] == 'HEAD'.freeze, # hdr_only
293
+ env['HTTP_VERSION'] == 'HTTP/1.1'.freeze ] # chunk_ok
294
+ end
276
295
  end