yahns 1.12.5 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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