unicorn 5.5.3 → 5.8.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: def232de6614b9d1e5394c6a8ebde552a8c5d063ddc04e946478d7dbc3220526
4
- data.tar.gz: f1dc8153f477d7c81b9be38c0e6b184863d8eb5d6d35779f637f9259e76a1272
3
+ metadata.gz: d3166eb1eac760d39cac35a6fc46332d6a6a2ee75bdf594118fddb3c6a959f6d
4
+ data.tar.gz: 3e18599fef201efdd79c67eb126aab35787c196054313a727d6bb184f830587e
5
5
  SHA512:
6
- metadata.gz: 330cd4ce75af35523f2e6bbdee24794f42222d1c14ffabeccc37bfb5d8cd5e4774d940e6528e6fa72b2890c07acc02c4f917458459718794c1709d6cf0aa0493
7
- data.tar.gz: ecbc2df84176fa9a99522b06dc127fd5d19e78d782f8f027f8f74f25dca841295299ef310698fb7c42f2cee7bb9b3531dbd2331c328dd8e1d358666df6b2a706
6
+ metadata.gz: 356f0c951cee2e69de8e49169f2c9560e6fead9fe759bd0af5a51097888a23794eb17175ff5393f07d1fadec5678215b494b688cdeeadb8a95c37201f3808a92
7
+ data.tar.gz: ffa1f8781231ac559157310d2d235340522c6c1cccf69be3d87d432659d2a2a7a39d744a84306c1f4292a3f6b36141cc1bb80497f1a9047902ec0d592f77871b
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
- DEF_VER = "v5.5.3"
2
+ DEF_VER = "v5.8.0"
3
3
  CONSTANT = "Unicorn::Const::UNICORN_VERSION"
4
4
  RVF = "lib/unicorn/version.rb"
5
5
  GVF = "GIT-VERSION-FILE"
@@ -10,6 +10,7 @@ RAGEL = ragel
10
10
  RSYNC = rsync
11
11
  OLDDOC = olddoc
12
12
  RDOC = rdoc
13
+ INSTALL = install
13
14
 
14
15
  GIT-VERSION-FILE: .FORCE-GIT-VERSION-FILE
15
16
  @./GIT-VERSION-GEN
@@ -25,7 +26,38 @@ endif
25
26
 
26
27
  RUBY_ENGINE := $(shell $(RUBY) -e 'puts((RUBY_ENGINE rescue "ruby"))')
27
28
 
28
- MYLIBS = $(RUBYLIB)
29
+ # we should never package more than one ext to avoid DSO proliferation:
30
+ # https://udrepper.livejournal.com/8790.html
31
+ ext := $(firstword $(wildcard ext/*))
32
+
33
+ ragel: $(ext)/unicorn_http.c
34
+
35
+ rl_files := $(wildcard $(ext)/*.rl)
36
+ ragel: $(ext)/unicorn_http.c
37
+ $(ext)/unicorn_http.c: $(rl_files)
38
+ cd $(@D) && $(RAGEL) unicorn_http.rl -C $(RLFLAGS) -o $(@F)
39
+ ext_pfx := test/$(RUBY_ENGINE)-$(RUBY_VERSION)
40
+ tmp_bin := $(ext_pfx)/bin
41
+ ext_h := $(wildcard $(ext)/*/*.h $(ext)/*.h)
42
+ ext_src := $(sort $(wildcard $(ext)/*.c) $(ext_h) $(ext)/unicorn_http.c)
43
+ ext_pfx_src := $(addprefix $(ext_pfx)/,$(ext_src))
44
+ ext_dir := $(ext_pfx)/$(ext)
45
+ $(ext)/extconf.rb: $(wildcard $(ext)/*.h)
46
+ @>>$@
47
+ $(ext_dir) $(tmp_bin) man/man1 doc/man1 pkg t/trash:
48
+ @mkdir -p $@
49
+ $(ext_pfx)/$(ext)/%: $(ext)/% | $(ext_dir)
50
+ $(INSTALL) -m 644 $< $@
51
+ $(ext_pfx)/$(ext)/Makefile: $(ext)/extconf.rb $(ext_h) | $(ext_dir)
52
+ $(RM) -f $(@D)/*.o
53
+ cd $(@D) && $(RUBY) $(CURDIR)/$(ext)/extconf.rb $(EXTCONF_ARGS)
54
+ ext_sfx := _ext.$(DLEXT)
55
+ ext_dl := $(ext_pfx)/$(ext)/$(notdir $(ext)_ext.$(DLEXT))
56
+ $(ext_dl): $(ext_src) $(ext_pfx_src) $(ext_pfx)/$(ext)/Makefile
57
+ $(MAKE) -C $(@D)
58
+ lib := $(CURDIR)/lib:$(CURDIR)/$(ext_pfx)/$(ext)
59
+ http build: $(ext_dl)
60
+ $(ext_pfx)/$(ext)/unicorn_http.c: ext/unicorn_http/unicorn_http.c
29
61
 
30
62
  # dunno how to implement this as concisely in Ruby, and hell, I love awk
31
63
  awk_slow := awk '/def test_/{print FILENAME"--"$$2".n"}' 2>/dev/null
@@ -37,44 +69,21 @@ T := $(filter-out $(slow_tests), $(wildcard test/*/test*.rb))
37
69
  T_n := $(shell $(awk_slow) $(slow_tests))
38
70
  T_log := $(subst .rb,$(log_suffix),$(T))
39
71
  T_n_log := $(subst .n,$(log_suffix),$(T_n))
40
- test_prefix = $(CURDIR)/test/$(RUBY_ENGINE)-$(RUBY_VERSION)
41
72
 
42
- ext := ext/unicorn_http
43
- c_files := $(ext)/unicorn_http.c $(ext)/httpdate.c $(wildcard $(ext)/*.h)
44
- rl_files := $(wildcard $(ext)/*.rl)
45
73
  base_bins := unicorn unicorn_rails
46
74
  bins := $(addprefix bin/, $(base_bins))
47
75
  man1_rdoc := $(addsuffix _1, $(base_bins))
48
76
  man1_bins := $(addsuffix .1, $(base_bins))
49
77
  man1_paths := $(addprefix man/man1/, $(man1_bins))
50
- rb_files := $(bins) $(shell find lib ext -type f -name '*.rb')
51
- inst_deps := $(c_files) $(rb_files) GNUmakefile test/test_helper.rb
52
-
53
- ragel: $(ext)/unicorn_http.c
54
- $(ext)/unicorn_http.c: $(rl_files)
55
- cd $(@D) && $(RAGEL) unicorn_http.rl -C $(RLFLAGS) -o $(@F)
56
- $(ext)/Makefile: $(ext)/extconf.rb $(c_files)
57
- cd $(@D) && $(RUBY) extconf.rb
58
- $(ext)/unicorn_http.$(DLEXT): $(ext)/Makefile
59
- $(MAKE) -C $(@D)
60
- http: $(ext)/unicorn_http.$(DLEXT)
78
+ tmp_bins = $(addprefix $(tmp_bin)/, unicorn unicorn_rails)
79
+ pid := $(shell echo $$PPID)
61
80
 
62
- # only used for tests
63
- http-install: $(ext)/unicorn_http.$(DLEXT)
64
- install -m644 $< lib/
81
+ $(tmp_bin)/%: bin/% | $(tmp_bin)
82
+ $(INSTALL) -m 755 $< $@.$(pid)
83
+ $(MRI) -i -p -e '$$_.gsub!(%r{^#!.*$$},"#!$(ruby_bin)")' $@.$(pid)
84
+ mv $@.$(pid) $@
65
85
 
66
- test-install: $(test_prefix)/.stamp
67
- $(test_prefix)/.stamp: $(inst_deps)
68
- mkdir -p $(test_prefix)/.ccache
69
- tar cf - $(inst_deps) GIT-VERSION-GEN | \
70
- (cd $(test_prefix) && tar xf -)
71
- $(MAKE) -C $(test_prefix) clean
72
- $(MAKE) -C $(test_prefix) http-install shebang RUBY="$(RUBY)"
73
- > $@
74
-
75
- # this is only intended to be run within $(test_prefix)
76
- shebang: $(bins)
77
- $(MRI) -i -p -e '$$_.gsub!(%r{^#!.*$$},"#!$(ruby_bin)")' $^
86
+ bins: $(tmp_bins)
78
87
 
79
88
  t_log := $(T_log) $(T_n_log)
80
89
  test: $(T) $(T_n)
@@ -83,15 +92,54 @@ test: $(T) $(T_n)
83
92
 
84
93
  test-exec: $(wildcard test/exec/test_*.rb)
85
94
  test-unit: $(wildcard test/unit/test_*.rb)
86
- $(slow_tests): $(test_prefix)/.stamp
95
+ $(slow_tests): $(ext_dl)
87
96
  @$(MAKE) $(shell $(awk_slow) $@)
88
97
 
89
98
  # ensure we can require just the HTTP parser without the rest of unicorn
90
- test-require: $(ext)/unicorn_http.$(DLEXT)
91
- $(RUBY) --disable-gems -I$(ext) -runicorn_http -e Unicorn
99
+ test-require: $(ext_dl)
100
+ $(RUBY) --disable-gems -I$(ext_pfx)/$(ext) -runicorn_http -e Unicorn
101
+
102
+ test_prereq := $(tmp_bins) $(ext_dl)
103
+
104
+ SH_TEST_OPTS =
105
+ ifdef V
106
+ ifeq ($(V),2)
107
+ SH_TEST_OPTS += --trace
108
+ else
109
+ SH_TEST_OPTS += --verbose
110
+ endif
111
+ endif
92
112
 
93
- test-integration: $(test_prefix)/.stamp
94
- $(MAKE) -C t
113
+ # do we trust Ruby behavior to be stable? some tests are
114
+ # (mostly) POSIX sh (not bash or ksh93, so no "set -o pipefail"
115
+ # TRACER = strace -f -o $(t_pfx).strace -s 100000
116
+ # TRACER = /usr/bin/time -o $(t_pfx).time
117
+ t_pfx = trash/$@-$(RUBY_ENGINE)-$(RUBY_VERSION)
118
+ T_sh = $(wildcard t/t[0-9][0-9][0-9][0-9]-*.sh)
119
+ $(T_sh): export RUBY := $(RUBY)
120
+ $(T_sh): export PATH := $(CURDIR)/$(tmp_bin):$(PATH)
121
+ $(T_sh): export RUBYLIB := $(lib):$(RUBYLIB)
122
+ $(T_sh): dep $(test_prereq) t/random_blob t/trash/.gitignore
123
+ cd t && $(TRACER) $(SHELL) $(SH_TEST_OPTS) $(@F) $(TEST_OPTS)
124
+
125
+ t/trash/.gitignore : | t/trash
126
+ echo '*' >$@
127
+
128
+ dependencies := socat curl
129
+ deps := $(addprefix t/.dep+,$(dependencies))
130
+ $(deps): dep_bin = $(lastword $(subst +, ,$@))
131
+ $(deps):
132
+ @which $(dep_bin) > $@.$(pid) 2>/dev/null || :
133
+ @test -s $@.$(pid) || \
134
+ { echo >&2 "E '$(dep_bin)' not found in PATH=$(PATH)"; exit 1; }
135
+ @mv $@.$(pid) $@
136
+ dep: $(deps)
137
+
138
+ t/random_blob:
139
+ dd if=/dev/urandom bs=1M count=30 of=$@.$(pid)
140
+ mv $@.$(pid) $@
141
+
142
+ test-integration: $(T_sh)
95
143
 
96
144
  check: test-require test test-integration
97
145
  test-all: check
@@ -122,16 +170,16 @@ run_test = $(quiet_pre) \
122
170
 
123
171
  %.n: arg = $(subst .n,,$(subst --, -n ,$@))
124
172
  %.n: t = $(subst .n,$(log_suffix),$@)
125
- %.n: export PATH := $(test_prefix)/bin:$(PATH)
126
- %.n: export RUBYLIB := $(test_prefix)/lib:$(MYLIBS)
127
- %.n: $(test_prefix)/.stamp
173
+ %.n: export PATH := $(CURDIR)/$(tmp_bin):$(PATH)
174
+ %.n: export RUBYLIB := $(lib):$(RUBYLIB)
175
+ %.n: $(test_prereq)
128
176
  $(run_test)
129
177
 
130
178
  $(T): arg = $@
131
179
  $(T): t = $(subst .rb,$(log_suffix),$@)
132
- $(T): export PATH := $(test_prefix)/bin:$(PATH)
133
- $(T): export RUBYLIB := $(test_prefix)/lib:$(MYLIBS)
134
- $(T): $(test_prefix)/.stamp
180
+ $(T): export PATH := $(CURDIR)/$(tmp_bin):$(PATH)
181
+ $(T): export RUBYLIB := $(lib):$(RUBYLIB)
182
+ $(T): $(test_prereq)
135
183
  $(run_test)
136
184
 
137
185
  install: $(bins) $(ext)/unicorn_http.c
@@ -152,18 +200,16 @@ clean:
152
200
  -$(MAKE) -C $(ext) clean
153
201
  $(RM) $(ext)/Makefile
154
202
  $(RM) $(setup_rb_files) $(t_log)
155
- $(RM) -r $(test_prefix) man
156
- $(RM) $(man1) $(html1)
203
+ $(RM) -r $(ext_pfx) man t/trash
204
+ $(RM) $(html1)
157
205
 
158
206
  man1 := $(addprefix Documentation/, unicorn.1 unicorn_rails.1)
159
207
  html1 := $(addsuffix .html, $(man1))
160
- man :
161
- mkdir -p man/man1
162
- install -m 644 $(man1) man/man1
208
+ man : $(man1) | man/man1
209
+ $(INSTALL) -m 644 $(man1) man/man1
163
210
 
164
- html : $(html1)
165
- mkdir -p doc/man1
166
- install -m 644 $(html1) doc/man1
211
+ html : $(html1) | doc/man1
212
+ $(INSTALL) -m 644 $(html1) doc/man1
167
213
 
168
214
  %.1.html: %.1
169
215
  $(OLDDOC) man2html -o $@ ./$<
@@ -187,10 +233,10 @@ doc: .document $(ext)/unicorn_http.c man html .olddoc.yml $(PLACEHOLDERS)
187
233
  $(OLDDOC) prepare
188
234
  $(RDOC) -f dark216
189
235
  $(OLDDOC) merge
190
- install -m644 COPYING doc/COPYING
191
- install -m644 NEWS.atom.xml doc/NEWS.atom.xml
192
- install -m644 $(shell LC_ALL=C grep '^[A-Z]' .document) doc/
193
- install -m644 $(man1_paths) doc/
236
+ $(INSTALL) -m 644 COPYING doc/COPYING
237
+ $(INSTALL) -m 644 NEWS.atom.xml doc/NEWS.atom.xml
238
+ $(INSTALL) -m 644 $(shell LC_ALL=C grep '^[A-Z]' .document) doc/
239
+ $(INSTALL) -m 644 $(man1_paths) doc/
194
240
  tar cf - $$(git ls-files examples/) | (cd doc && tar xf -)
195
241
 
196
242
  # publishes docs to https://yhbt.net/unicorn/
@@ -199,7 +245,8 @@ publish_doc:
199
245
  $(MAKE) doc
200
246
  $(MAKE) doc_gz
201
247
  chmod 644 $$(find doc -type f)
202
- $(RSYNC) -av doc/ yhbt.net:/srv/yhbt/unicorn/
248
+ $(RSYNC) -av doc/ yhbt.net:/srv/yhbt/unicorn/ \
249
+ --exclude index.html* --exclude created.rid*
203
250
  git ls-files | xargs touch
204
251
 
205
252
  # Create gzip variants of the same timestamp as the original so nginx
@@ -231,9 +278,8 @@ gem: $(pkggem)
231
278
  install-gem: $(pkggem)
232
279
  gem install --local $(CURDIR)/$<
233
280
 
234
- $(pkggem): .manifest fix-perms
281
+ $(pkggem): .manifest fix-perms | pkg
235
282
  gem build $(rfpackage).gemspec
236
- mkdir -p pkg
237
283
  mv $(@F) $@
238
284
 
239
285
  $(pkgtgz): distdir = $(basename $@)
@@ -264,5 +310,4 @@ check-warnings:
264
310
  do $(RUBY) --disable-gems -d -W2 -c \
265
311
  $$i; done) | grep -v '^Syntax OK$$' || :
266
312
 
267
- .PHONY: .FORCE-GIT-VERSION-FILE doc $(T) $(slow_tests) man
268
- .PHONY: test-install
313
+ .PHONY: .FORCE-GIT-VERSION-FILE doc $(T) $(slow_tests) man $(T_sh) clean
data/ISSUES CHANGED
@@ -76,15 +76,18 @@ document distributed with git) on guidelines for patch submission.
76
76
  * public: mailto:unicorn-public@yhbt.net
77
77
  * nntp://news.gmane.io/gmane.comp.lang.ruby.unicorn.general
78
78
  * nntp://news.public-inbox.org/inbox.comp.lang.ruby.unicorn
79
+ * imaps://news.public-inbox.org/inbox.comp.lang.ruby.unicorn.0
79
80
  * https://yhbt.net/unicorn-public/
80
81
  * http://ou63pmih66umazou.onion/unicorn-public/
81
82
 
82
83
  Mailing list subscription is optional, so Cc: all participants.
83
84
 
84
- You can follow along via NNTP (read-only):
85
+ You can follow along via NNTP or IMAP (read-only):
85
86
 
86
87
  nntp://news.public-inbox.org/inbox.comp.lang.ruby.unicorn
87
88
  nntp://news.gmane.io/gmane.comp.lang.ruby.unicorn.general
89
+ imaps://news.public-inbox.org/inbox.comp.lang.ruby.unicorn.0
90
+ imap://ou63pmih66umazou.onion/inbox.comp.lang.ruby.unicorn.0
88
91
 
89
92
  Or Atom feeds:
90
93
 
data/README CHANGED
@@ -136,10 +136,17 @@ requests) go to the mailing list/newsgroup. See the ISSUES document for
136
136
  information on the {mailing list}[mailto:unicorn-public@yhbt.net].
137
137
 
138
138
  The mailing list is archived at https://yhbt.net/unicorn-public/
139
+
139
140
  Read-only NNTP access is available at:
140
141
  nntp://news.public-inbox.org/inbox.comp.lang.ruby.unicorn and
141
142
  nntp://news.gmane.io/gmane.comp.lang.ruby.unicorn.general
142
143
 
144
+ Read-only IMAP access is also avaialble at:
145
+ imaps://news.public-inbox.org/inbox.comp.lang.ruby.unicorn.0 and
146
+ imap://ou63pmih66umazou.onion/inbox.comp.lang.ruby.unicorn.0
147
+ AUTH=ANONYMOUS mechanism is supported, as is any username+password
148
+ combination.
149
+
143
150
  For the latest on unicorn releases, you may also finger us at
144
151
  unicorn@yhbt.net or check our NEWS page (and subscribe to our Atom
145
152
  feed).
@@ -1,6 +1,11 @@
1
1
  # -*- encoding: binary -*-
2
2
  require 'mkmf'
3
3
 
4
+ unless RUBY_VERSION < '3.1'
5
+ warn "Unicorn was only tested against MRI up to 3.0.\n" \
6
+ "It might not properly work with #{RUBY_VERSION}"
7
+ end
8
+
4
9
  have_macro("SIZEOF_OFF_T", "ruby.h") or check_sizeof("off_t", "sys/types.h")
5
10
  have_macro("SIZEOF_SIZE_T", "ruby.h") or check_sizeof("size_t", "sys/types.h")
6
11
  have_macro("SIZEOF_LONG", "ruby.h") or check_sizeof("long", "sys/types.h")
@@ -62,7 +62,8 @@ struct http_parser {
62
62
  } len;
63
63
  };
64
64
 
65
- static ID id_set_backtrace;
65
+ static ID id_set_backtrace, id_is_chunked_p;
66
+ static VALUE cHttpParser;
66
67
 
67
68
  #ifdef HAVE_RB_HASH_CLEAR /* Ruby >= 2.0 */
68
69
  # define my_hash_clear(h) (void)rb_hash_clear(h)
@@ -220,6 +221,19 @@ static void write_cont_value(struct http_parser *hp,
220
221
  rb_str_buf_cat(hp->cont, vptr, end + 1);
221
222
  }
222
223
 
224
+ static int is_chunked(VALUE v)
225
+ {
226
+ /* common case first */
227
+ if (STR_CSTR_CASE_EQ(v, "chunked"))
228
+ return 1;
229
+
230
+ /*
231
+ * call Ruby function in unicorn/http_request.rb to deal with unlikely
232
+ * comma-delimited case
233
+ */
234
+ return rb_funcall(cHttpParser, id_is_chunked_p, 1, v) != Qfalse;
235
+ }
236
+
223
237
  static void write_value(struct http_parser *hp,
224
238
  const char *buffer, const char *p)
225
239
  {
@@ -246,7 +260,9 @@ static void write_value(struct http_parser *hp,
246
260
  f = uncommon_field(field, flen);
247
261
  } else if (f == g_http_connection) {
248
262
  hp_keepalive_connection(hp, v);
249
- } else if (f == g_content_length) {
263
+ } else if (f == g_content_length && !HP_FL_TEST(hp, CHUNKED)) {
264
+ if (hp->len.content)
265
+ parser_raise(eHttpParserError, "Content-Length already set");
250
266
  hp->len.content = parse_length(RSTRING_PTR(v), RSTRING_LEN(v));
251
267
  if (hp->len.content < 0)
252
268
  parser_raise(eHttpParserError, "invalid Content-Length");
@@ -254,9 +270,30 @@ static void write_value(struct http_parser *hp,
254
270
  HP_FL_SET(hp, HASBODY);
255
271
  hp_invalid_if_trailer(hp);
256
272
  } else if (f == g_http_transfer_encoding) {
257
- if (STR_CSTR_CASE_EQ(v, "chunked")) {
273
+ if (is_chunked(v)) {
274
+ if (HP_FL_TEST(hp, CHUNKED))
275
+ /*
276
+ * RFC 7230 3.3.1:
277
+ * A sender MUST NOT apply chunked more than once to a message body
278
+ * (i.e., chunking an already chunked message is not allowed).
279
+ */
280
+ parser_raise(eHttpParserError, "Transfer-Encoding double chunked");
281
+
258
282
  HP_FL_SET(hp, CHUNKED);
259
283
  HP_FL_SET(hp, HASBODY);
284
+
285
+ /* RFC 7230 3.3.3, 3: favor chunked if Content-Length exists */
286
+ hp->len.content = 0;
287
+ } else if (HP_FL_TEST(hp, CHUNKED)) {
288
+ /*
289
+ * RFC 7230 3.3.3, point 3 states:
290
+ * If a Transfer-Encoding header field is present in a request and
291
+ * the chunked transfer coding is not the final encoding, the
292
+ * message body length cannot be determined reliably; the server
293
+ * MUST respond with the 400 (Bad Request) status code and then
294
+ * close the connection.
295
+ */
296
+ parser_raise(eHttpParserError, "invalid Transfer-Encoding");
260
297
  }
261
298
  hp_invalid_if_trailer(hp);
262
299
  } else if (f == g_http_trailer) {
@@ -931,7 +968,7 @@ static VALUE HttpParser_rssget(VALUE self)
931
968
 
932
969
  void Init_unicorn_http(void)
933
970
  {
934
- VALUE mUnicorn, cHttpParser;
971
+ VALUE mUnicorn;
935
972
 
936
973
  mUnicorn = rb_define_module("Unicorn");
937
974
  cHttpParser = rb_define_class_under(mUnicorn, "HttpParser", rb_cObject);
@@ -991,5 +1028,6 @@ void Init_unicorn_http(void)
991
1028
  #ifndef HAVE_RB_HASH_CLEAR
992
1029
  id_clear = rb_intern("clear");
993
1030
  #endif
1031
+ id_is_chunked_p = rb_intern("is_chunked?");
994
1032
  }
995
1033
  #undef SET_GLOBAL
@@ -53,6 +53,7 @@ class Unicorn::Configurator
53
53
  server.logger.info("worker=#{worker.nr} ready")
54
54
  },
55
55
  :pid => nil,
56
+ :early_hints => false,
56
57
  :worker_exec => false,
57
58
  :preload_app => false,
58
59
  :check_client_connection => false,
@@ -276,6 +277,15 @@ def default_middleware(bool)
276
277
  set_bool(:default_middleware, bool)
277
278
  end
278
279
 
280
+ # sets whether to enable the proposed early hints Rack API.
281
+ # If enabled, Rails 5.2+ will automatically send a 103 Early Hint
282
+ # for all the `javascript_include_tag` and `stylesheet_link_tag`
283
+ # in your response. See: https://api.rubyonrails.org/v5.2/classes/ActionDispatch/Request.html#method-i-send_early_hints
284
+ # See also https://tools.ietf.org/html/rfc8297
285
+ def early_hints(bool)
286
+ set_bool(:early_hints, bool)
287
+ end
288
+
279
289
  # sets listeners to the given +addresses+, replacing or augmenting the
280
290
  # current set. This is for the global listener pool shared by all
281
291
  # worker processes. For per-worker listeners, see the after_fork example
@@ -188,4 +188,15 @@ def write_http_header(socket) # :nodoc:
188
188
  HTTP_RESPONSE_START.each { |c| socket.write(c) }
189
189
  end
190
190
  end
191
+
192
+ # called by ext/unicorn_http/unicorn_http.rl via rb_funcall
193
+ def self.is_chunked?(v) # :nodoc:
194
+ vals = v.split(/[ \t]*,[ \t]*/).map!(&:downcase)
195
+ if vals.pop == 'chunked'.freeze
196
+ return true unless vals.include?('chunked'.freeze)
197
+ raise Unicorn::HttpParserError, 'double chunked', []
198
+ end
199
+ return false unless vals.include?('chunked'.freeze)
200
+ raise Unicorn::HttpParserError, 'chunked not last', []
201
+ end
191
202
  end
@@ -15,7 +15,7 @@ class Unicorn::HttpServer
15
15
  :before_fork, :after_fork, :before_exec,
16
16
  :listener_opts, :preload_app,
17
17
  :orig_app, :config, :ready_pipe, :user,
18
- :default_middleware
18
+ :default_middleware, :early_hints
19
19
  attr_writer :after_worker_exit, :after_worker_ready, :worker_exec
20
20
 
21
21
  attr_reader :pid, :logger
@@ -588,6 +588,25 @@ def handle_error(client, e)
588
588
  rescue
589
589
  end
590
590
 
591
+ def e103_response_write(client, headers)
592
+ response = if @request.response_start_sent
593
+ "103 Early Hints\r\n"
594
+ else
595
+ "HTTP/1.1 103 Early Hints\r\n"
596
+ end
597
+
598
+ headers.each_pair do |k, vs|
599
+ next if !vs || vs.empty?
600
+ values = vs.to_s.split("\n".freeze)
601
+ values.each do |v|
602
+ response << "#{k}: #{v}\r\n"
603
+ end
604
+ end
605
+ response << "\r\n".freeze
606
+ response << "HTTP/1.1 ".freeze if @request.response_start_sent
607
+ client.write(response)
608
+ end
609
+
591
610
  def e100_response_write(client, env)
592
611
  # We use String#freeze to avoid allocations under Ruby 2.1+
593
612
  # Not many users hit this code path, so it's better to reduce the
@@ -602,7 +621,17 @@ def e100_response_write(client, env)
602
621
  # once a client is accepted, it is processed in its entirety here
603
622
  # in 3 easy steps: read request, call app, write app response
604
623
  def process_client(client)
605
- status, headers, body = @app.call(env = @request.read(client))
624
+ env = @request.read(client)
625
+
626
+ if early_hints
627
+ env["rack.early_hints"] = lambda do |headers|
628
+ e103_response_write(client, headers)
629
+ end
630
+ end
631
+
632
+ env["rack.after_reply"] = []
633
+
634
+ status, headers, body = @app.call(env)
606
635
 
607
636
  begin
608
637
  return if @request.hijacked?
@@ -624,6 +653,8 @@ def process_client(client)
624
653
  end
625
654
  rescue => e
626
655
  handle_error(client, e)
656
+ ensure
657
+ env["rack.after_reply"].each(&:call) if env
627
658
  end
628
659
 
629
660
  def nuke_listeners!(readers)
@@ -686,6 +717,7 @@ def worker_loop(worker)
686
717
  trap(:USR1) { nr = -65536 }
687
718
 
688
719
  ready = readers.dup
720
+ nr_listeners = readers.size
689
721
  @after_worker_ready.call(self, worker)
690
722
 
691
723
  begin
@@ -708,7 +740,7 @@ def worker_loop(worker)
708
740
  # we're probably reasonably busy, so avoid calling select()
709
741
  # and do a speculative non-blocking accept() on ready listeners
710
742
  # before we sleep again in select().
711
- unless nr == 0
743
+ if nr == nr_listeners
712
744
  tmp = ready.dup
713
745
  redo
714
746
  end
@@ -1,74 +1,5 @@
1
- # we can run tests in parallel with GNU make
1
+ # there used to be more, here, but we stopped relying on recursive make
2
2
  all::
3
+ $(MAKE) -C .. test-integration
3
4
 
4
- pid := $(shell echo $$PPID)
5
-
6
- RUBY = ruby
7
- RAKE = rake
8
- -include ../local.mk
9
- ifeq ($(RUBY_VERSION),)
10
- RUBY_VERSION := $(shell $(RUBY) -e 'puts RUBY_VERSION')
11
- endif
12
-
13
- ifeq ($(RUBY_VERSION),)
14
- $(error unable to detect RUBY_VERSION)
15
- endif
16
-
17
- RUBY_ENGINE := $(shell $(RUBY) -e 'puts((RUBY_ENGINE rescue "ruby"))')
18
- export RUBY_ENGINE
19
-
20
- MYLIBS := $(RUBYLIB)
21
-
22
- T = $(wildcard t[0-9][0-9][0-9][0-9]-*.sh)
23
-
24
- all:: $(T)
25
-
26
- # can't rely on "set -o pipefail" since we don't require bash or ksh93 :<
27
- t_pfx = trash/$@-$(RUBY_ENGINE)-$(RUBY_VERSION)
28
- TEST_OPTS =
29
- # TRACER = strace -f -o $(t_pfx).strace -s 100000
30
- # TRACER = /usr/bin/time -o $(t_pfx).time
31
-
32
- ifdef V
33
- ifeq ($(V),2)
34
- TEST_OPTS += --trace
35
- else
36
- TEST_OPTS += --verbose
37
- endif
38
- endif
39
-
40
- random_blob:
41
- dd if=/dev/urandom bs=1M count=30 of=$@.$(pid)
42
- mv $@.$(pid) $@
43
-
44
- $(T): random_blob
45
-
46
- dependencies := socat curl
47
- deps := $(addprefix .dep+,$(dependencies))
48
- $(deps): dep_bin = $(lastword $(subst +, ,$@))
49
- $(deps):
50
- @which $(dep_bin) > $@.$(pid) 2>/dev/null || :
51
- @test -s $@.$(pid) || \
52
- { echo >&2 "E '$(dep_bin)' not found in PATH=$(PATH)"; exit 1; }
53
- @mv $@.$(pid) $@
54
- dep: $(deps)
55
-
56
- test_prefix := $(CURDIR)/../test/$(RUBY_ENGINE)-$(RUBY_VERSION)
57
- $(test_prefix)/.stamp:
58
- $(MAKE) -C .. test-install
59
-
60
- $(T): export RUBY := $(RUBY)
61
- $(T): export RAKE := $(RAKE)
62
- $(T): export PATH := $(test_prefix)/bin:$(PATH)
63
- $(T): export RUBYLIB := $(test_prefix)/lib:$(MYLIBS)
64
- $(T): dep $(test_prefix)/.stamp trash/.gitignore
65
- $(TRACER) $(SHELL) $(SH_TEST_OPTS) $@ $(TEST_OPTS)
66
-
67
- trash/.gitignore:
68
- mkdir -p $(@D)
69
- echo '*' > $@
70
-
71
- clean:
72
- $(RM) -r trash/*
73
-
74
- .PHONY: $(T) clean
5
+ .PHONY: all
@@ -34,16 +34,33 @@
34
34
  Debugger.start
35
35
  end
36
36
 
37
+ unless RUBY_VERSION < '3.1'
38
+ warn "Unicorn was only tested against MRI up to 3.0.\n" \
39
+ "It might not properly work with #{RUBY_VERSION}"
40
+ end
41
+
37
42
  def redirect_test_io
38
43
  orig_err = STDERR.dup
39
44
  orig_out = STDOUT.dup
40
- STDERR.reopen("test_stderr.#{$$}.log", "a")
41
- STDOUT.reopen("test_stdout.#{$$}.log", "a")
45
+ new_out = File.open("test_stdout.#$$.log", "a")
46
+ new_err = File.open("test_stderr.#$$.log", "a")
47
+ new_out.sync = new_err.sync = true
48
+
49
+ if tail = ENV['TAIL'] # "tail -F" if GNU, "tail -f" otherwise
50
+ require 'shellwords'
51
+ cmd = tail.shellsplit
52
+ cmd << new_out.path
53
+ cmd << new_err.path
54
+ pid = Process.spawn(*cmd, { 1 => 2, :pgroup => true })
55
+ sleep 0.1 # wait for tail(1) to startup
56
+ end
57
+ STDERR.reopen(new_err)
58
+ STDOUT.reopen(new_out)
42
59
  STDERR.sync = STDOUT.sync = true
43
60
 
44
61
  at_exit do
45
- File.unlink("test_stderr.#{$$}.log") rescue nil
46
- File.unlink("test_stdout.#{$$}.log") rescue nil
62
+ File.unlink(new_out.path) rescue nil
63
+ File.unlink(new_err.path) rescue nil
47
64
  end
48
65
 
49
66
  begin
@@ -51,6 +68,7 @@ def redirect_test_io
51
68
  ensure
52
69
  STDERR.reopen(orig_err)
53
70
  STDOUT.reopen(orig_out)
71
+ Process.kill(:TERM, pid) if pid
54
72
  end
55
73
  end
56
74
 
@@ -11,6 +11,20 @@ def setup
11
11
  @parser = HttpParser.new
12
12
  end
13
13
 
14
+ # RFC 7230 allows gzip/deflate/compress Transfer-Encoding,
15
+ # but "chunked" must be last if used
16
+ def test_is_chunked
17
+ [ 'chunked,chunked', 'chunked,gzip', 'chunked,gzip,chunked' ].each do |x|
18
+ assert_raise(HttpParserError) { HttpParser.is_chunked?(x) }
19
+ end
20
+ [ 'gzip, chunked', 'gzip,chunked', 'gzip ,chunked' ].each do |x|
21
+ assert HttpParser.is_chunked?(x)
22
+ end
23
+ [ 'gzip', 'xhunked', 'xchunked' ].each do |x|
24
+ assert !HttpParser.is_chunked?(x)
25
+ end
26
+ end
27
+
14
28
  def test_parser_max_len
15
29
  assert_raises(RangeError) do
16
30
  HttpParser.max_header_len = 0xffffffff + 1
@@ -566,6 +580,73 @@ def test_invalid_content_length
566
580
  end
567
581
  end
568
582
 
583
+ def test_duplicate_content_length
584
+ str = "PUT / HTTP/1.1\r\n" \
585
+ "Content-Length: 1\r\n" \
586
+ "Content-Length: 9\r\n" \
587
+ "\r\n"
588
+ assert_raises(HttpParserError) { @parser.headers({}, str) }
589
+ end
590
+
591
+ def test_chunked_overrides_content_length
592
+ order = [ 'Transfer-Encoding: chunked', 'Content-Length: 666' ]
593
+ %w(a b).each do |x|
594
+ str = "PUT /#{x} HTTP/1.1\r\n" \
595
+ "#{order.join("\r\n")}" \
596
+ "\r\n\r\na\r\nhelloworld\r\n0\r\n\r\n"
597
+ order.reverse!
598
+ env = @parser.headers({}, str)
599
+ assert_nil @parser.content_length
600
+ assert_equal 'chunked', env['HTTP_TRANSFER_ENCODING']
601
+ assert_equal '666', env['CONTENT_LENGTH'],
602
+ 'Content-Length logged so the app can log a possible client bug/attack'
603
+ @parser.filter_body(dst = '', str)
604
+ assert_equal 'helloworld', dst
605
+ @parser.parse # handle the non-existent trailer
606
+ assert @parser.next?
607
+ end
608
+ end
609
+
610
+ def test_chunked_order_good
611
+ str = "PUT /x HTTP/1.1\r\n" \
612
+ "Transfer-Encoding: gzip\r\n" \
613
+ "Transfer-Encoding: chunked\r\n" \
614
+ "\r\n"
615
+ env = @parser.headers({}, str)
616
+ assert_equal 'gzip,chunked', env['HTTP_TRANSFER_ENCODING']
617
+ assert_nil @parser.content_length
618
+
619
+ @parser.clear
620
+ str = "PUT /x HTTP/1.1\r\n" \
621
+ "Transfer-Encoding: gzip, chunked\r\n" \
622
+ "\r\n"
623
+ env = @parser.headers({}, str)
624
+ assert_equal 'gzip, chunked', env['HTTP_TRANSFER_ENCODING']
625
+ assert_nil @parser.content_length
626
+ end
627
+
628
+ def test_chunked_order_bad
629
+ str = "PUT /x HTTP/1.1\r\n" \
630
+ "Transfer-Encoding: chunked\r\n" \
631
+ "Transfer-Encoding: gzip\r\n" \
632
+ "\r\n"
633
+ assert_raise(HttpParserError) { @parser.headers({}, str) }
634
+ end
635
+
636
+ def test_double_chunked
637
+ str = "PUT /x HTTP/1.1\r\n" \
638
+ "Transfer-Encoding: chunked\r\n" \
639
+ "Transfer-Encoding: chunked\r\n" \
640
+ "\r\n"
641
+ assert_raise(HttpParserError) { @parser.headers({}, str) }
642
+
643
+ @parser.clear
644
+ str = "PUT /x HTTP/1.1\r\n" \
645
+ "Transfer-Encoding: chunked,chunked\r\n" \
646
+ "\r\n"
647
+ assert_raise(HttpParserError) { @parser.headers({}, str) }
648
+ end
649
+
569
650
  def test_backtrace_is_empty
570
651
  begin
571
652
  @parser.headers({}, "AAADFSFDSFD\r\n\r\n")
@@ -23,6 +23,34 @@ def call(env)
23
23
  end
24
24
  end
25
25
 
26
+ class TestEarlyHintsHandler
27
+ def call(env)
28
+ while env['rack.input'].read(4096)
29
+ end
30
+ env['rack.early_hints'].call(
31
+ "Link" => "</style.css>; rel=preload; as=style\n</script.js>; rel=preload"
32
+ )
33
+ [200, { 'Content-Type' => 'text/plain' }, ['hello!\n']]
34
+ end
35
+ end
36
+
37
+ class TestRackAfterReply
38
+ def initialize
39
+ @called = false
40
+ end
41
+
42
+ def call(env)
43
+ while env['rack.input'].read(4096)
44
+ end
45
+
46
+ env["rack.after_reply"] << -> { @called = true }
47
+
48
+ [200, { 'Content-Type' => 'text/plain' }, ["after_reply_called: #{@called}"]]
49
+ rescue Unicorn::ClientShutdown, Unicorn::HttpParserError => e
50
+ $stderr.syswrite("#{e.class}: #{e.message} #{e.backtrace.empty?}\n")
51
+ raise e
52
+ end
53
+ end
26
54
 
27
55
  class WebServerTest < Test::Unit::TestCase
28
56
 
@@ -84,6 +112,52 @@ def test_preload_app_config
84
112
  tmp.close!
85
113
  end
86
114
 
115
+ def test_early_hints
116
+ teardown
117
+ redirect_test_io do
118
+ @server = HttpServer.new(TestEarlyHintsHandler.new,
119
+ :listeners => [ "127.0.0.1:#@port"],
120
+ :early_hints => true)
121
+ @server.start
122
+ end
123
+
124
+ sock = TCPSocket.new('127.0.0.1', @port)
125
+ sock.syswrite("GET / HTTP/1.0\r\n\r\n")
126
+
127
+ responses = sock.read(4096)
128
+ assert_match %r{\AHTTP/1.[01] 103\b}, responses
129
+ assert_match %r{^Link: </style\.css>}, responses
130
+ assert_match %r{^Link: </script\.js>}, responses
131
+
132
+ assert_match %r{^HTTP/1.[01] 200\b}, responses
133
+ end
134
+
135
+ def test_after_reply
136
+ teardown
137
+
138
+ redirect_test_io do
139
+ @server = HttpServer.new(TestRackAfterReply.new,
140
+ :listeners => [ "127.0.0.1:#@port"])
141
+ @server.start
142
+ end
143
+
144
+ sock = TCPSocket.new('127.0.0.1', @port)
145
+ sock.syswrite("GET / HTTP/1.0\r\n\r\n")
146
+
147
+ responses = sock.read(4096)
148
+ assert_match %r{\AHTTP/1.[01] 200\b}, responses
149
+ assert_match %r{^after_reply_called: false}, responses
150
+
151
+ sock = TCPSocket.new('127.0.0.1', @port)
152
+ sock.syswrite("GET / HTTP/1.0\r\n\r\n")
153
+
154
+ responses = sock.read(4096)
155
+ assert_match %r{\AHTTP/1.[01] 200\b}, responses
156
+ assert_match %r{^after_reply_called: true}, responses
157
+
158
+ sock.close
159
+ end
160
+
87
161
  def test_broken_app
88
162
  teardown
89
163
  app = lambda { |env| raise RuntimeError, "hello" }
@@ -11,7 +11,7 @@
11
11
 
12
12
  Gem::Specification.new do |s|
13
13
  s.name = %q{unicorn}
14
- s.version = (ENV['VERSION'] || '5.5.3').dup
14
+ s.version = (ENV['VERSION'] || '5.8.0').dup
15
15
  s.authors = ['unicorn hackers']
16
16
  s.summary = 'Rack HTTP server for fast clients and Unix'
17
17
  s.description = File.read('README').split("\n\n")[1]
@@ -25,10 +25,11 @@
25
25
  s.homepage = 'https://yhbt.net/unicorn/'
26
26
  s.test_files = test_files
27
27
 
28
- # technically we need ">= 1.9.3", too, but avoid the array here since
29
- # old rubygems versions (1.8.23.2 at least) do not support multiple
30
- # version requirements here.
31
- s.required_ruby_version = '< 3.0'
28
+ # 1.9.3 is the minumum supported version. We don't specify
29
+ # a maximum version to make it easier to test pre-releases,
30
+ # but we do warn users if they install unicorn on an untested
31
+ # version in extconf.rb
32
+ s.required_ruby_version = ">= 1.9.3"
32
33
 
33
34
  # We do not have a hard dependency on rack, it's possible to load
34
35
  # things which respond to #call. HTTP status lines in responses
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: unicorn
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.5.3
4
+ version: 5.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - unicorn hackers
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-01-31 00:00:00.000000000 Z
11
+ date: 2020-12-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -282,9 +282,9 @@ require_paths:
282
282
  - lib
283
283
  required_ruby_version: !ruby/object:Gem::Requirement
284
284
  requirements:
285
- - - "<"
285
+ - - ">="
286
286
  - !ruby/object:Gem::Version
287
- version: '3.0'
287
+ version: 1.9.3
288
288
  required_rubygems_version: !ruby/object:Gem::Requirement
289
289
  requirements:
290
290
  - - ">="