unicorn 2.0.1 → 3.0.0pre1
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.
- data/GIT-VERSION-GEN +1 -1
- data/TODO +0 -3
- data/ext/unicorn_http/global_variables.h +0 -4
- data/ext/unicorn_http/unicorn_http.rl +48 -38
- data/lib/unicorn.rb +1 -0
- data/lib/unicorn/configurator.rb +26 -6
- data/lib/unicorn/const.rb +2 -2
- data/lib/unicorn/http_request.rb +11 -2
- data/lib/unicorn/http_server.rb +10 -1
- data/lib/unicorn/preread_input.rb +1 -1
- data/lib/unicorn/stream_input.rb +156 -0
- data/lib/unicorn/tee_input.rb +19 -128
- data/t/t0013-rewindable-input-false.sh +24 -0
- data/t/t0014-rewindable-input-true.sh +24 -0
- data/test/unit/test_http_parser.rb +13 -3
- data/test/unit/test_http_parser_ng.rb +43 -12
- data/test/unit/test_stream_input.rb +143 -0
- data/test/unit/test_tee_input.rb +39 -13
- metadata +16 -10
- data/t/t0002-parser-error.sh +0 -31
data/GIT-VERSION-GEN
CHANGED
data/TODO
CHANGED
@@ -26,8 +26,6 @@ static VALUE g_http;
|
|
26
26
|
static VALUE g_http_09;
|
27
27
|
static VALUE g_http_10;
|
28
28
|
static VALUE g_http_11;
|
29
|
-
static VALUE g_GET;
|
30
|
-
static VALUE g_HEAD;
|
31
29
|
|
32
30
|
/** Defines common length and error messages for input length validation. */
|
33
31
|
#define DEF_MAX_LENGTH(N, length) \
|
@@ -82,8 +80,6 @@ static void init_globals(void)
|
|
82
80
|
DEF_GLOBAL(http_11, "HTTP/1.1");
|
83
81
|
DEF_GLOBAL(http_10, "HTTP/1.0");
|
84
82
|
DEF_GLOBAL(http_09, "HTTP/0.9");
|
85
|
-
DEF_GLOBAL(GET, "GET");
|
86
|
-
DEF_GLOBAL(HEAD, "HEAD");
|
87
83
|
}
|
88
84
|
|
89
85
|
#undef DEF_GLOBAL
|
@@ -18,12 +18,12 @@
|
|
18
18
|
#define UH_FL_HASTRAILER 0x8
|
19
19
|
#define UH_FL_INTRAILER 0x10
|
20
20
|
#define UH_FL_INCHUNK 0x20
|
21
|
-
#define
|
21
|
+
#define UH_FL_REQEOF 0x40
|
22
22
|
#define UH_FL_KAVERSION 0x80
|
23
23
|
#define UH_FL_HASHEADER 0x100
|
24
24
|
|
25
|
-
/*
|
26
|
-
#define UH_FL_KEEPALIVE (
|
25
|
+
/* all of these flags need to be set for keepalive to be supported */
|
26
|
+
#define UH_FL_KEEPALIVE (UH_FL_KAVERSION | UH_FL_REQEOF | UH_FL_HASHEADER)
|
27
27
|
|
28
28
|
/* keep this small for Rainbows! since every client has one */
|
29
29
|
struct http_parser {
|
@@ -79,46 +79,29 @@ static void parser_error(const char *msg)
|
|
79
79
|
*/
|
80
80
|
static void hp_keepalive_connection(struct http_parser *hp, VALUE val)
|
81
81
|
{
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
* Raising an exception might be too mean...
|
98
|
-
*/
|
99
|
-
}
|
82
|
+
if (STR_CSTR_CASE_EQ(val, "keep-alive")) {
|
83
|
+
/* basically have HTTP/1.0 masquerade as HTTP/1.1+ */
|
84
|
+
HP_FL_SET(hp, KAVERSION);
|
85
|
+
} else if (STR_CSTR_CASE_EQ(val, "close")) {
|
86
|
+
/*
|
87
|
+
* it doesn't matter what HTTP version or request method we have,
|
88
|
+
* if a client says "Connection: close", we disable keepalive
|
89
|
+
*/
|
90
|
+
HP_FL_UNSET(hp, KAVERSION);
|
91
|
+
} else {
|
92
|
+
/*
|
93
|
+
* client could've sent anything, ignore it for now. Maybe
|
94
|
+
* "HP_FL_UNSET(hp, KAVERSION);" just in case?
|
95
|
+
* Raising an exception might be too mean...
|
96
|
+
*/
|
100
97
|
}
|
101
98
|
}
|
102
99
|
|
103
100
|
static void
|
104
101
|
request_method(struct http_parser *hp, const char *ptr, size_t len)
|
105
102
|
{
|
106
|
-
VALUE v;
|
103
|
+
VALUE v = rb_str_new(ptr, len);
|
107
104
|
|
108
|
-
/*
|
109
|
-
* we only support keepalive for GET and HEAD requests for now other
|
110
|
-
* methods are too rarely seen to be worth optimizing. POST is unsafe
|
111
|
-
* since some clients send extra bytes after POST bodies.
|
112
|
-
*/
|
113
|
-
if (CONST_MEM_EQ("GET", ptr, len)) {
|
114
|
-
HP_FL_SET(hp, KAMETHOD);
|
115
|
-
v = g_GET;
|
116
|
-
} else if (CONST_MEM_EQ("HEAD", ptr, len)) {
|
117
|
-
HP_FL_SET(hp, KAMETHOD);
|
118
|
-
v = g_HEAD;
|
119
|
-
} else {
|
120
|
-
v = rb_str_new(ptr, len);
|
121
|
-
}
|
122
105
|
rb_hash_aset(hp->env, g_request_method, v);
|
123
106
|
}
|
124
107
|
|
@@ -206,7 +189,8 @@ static void write_value(struct http_parser *hp,
|
|
206
189
|
hp->len.content = parse_length(RSTRING_PTR(v), RSTRING_LEN(v));
|
207
190
|
if (hp->len.content < 0)
|
208
191
|
parser_error("invalid Content-Length");
|
209
|
-
|
192
|
+
if (hp->len.content != 0)
|
193
|
+
HP_FL_SET(hp, HASBODY);
|
210
194
|
hp_invalid_if_trailer(hp);
|
211
195
|
} else if (f == g_http_transfer_encoding) {
|
212
196
|
if (STR_CSTR_CASE_EQ(v, "chunked")) {
|
@@ -305,6 +289,7 @@ static void write_value(struct http_parser *hp,
|
|
305
289
|
if (HP_FL_TEST(hp, CHUNKED))
|
306
290
|
cs = http_parser_en_ChunkedBody;
|
307
291
|
} else {
|
292
|
+
HP_FL_SET(hp, REQEOF);
|
308
293
|
assert(!HP_FL_TEST(hp, CHUNKED) && "chunked encoding without body!");
|
309
294
|
}
|
310
295
|
/*
|
@@ -559,6 +544,8 @@ static VALUE HttpParser_parse(VALUE self)
|
|
559
544
|
hp->cs == http_parser_en_ChunkedBody) {
|
560
545
|
advance_str(data, hp->offset + 1);
|
561
546
|
hp->offset = 0;
|
547
|
+
if (HP_FL_TEST(hp, INTRAILER))
|
548
|
+
HP_FL_SET(hp, REQEOF);
|
562
549
|
|
563
550
|
return hp->env;
|
564
551
|
}
|
@@ -630,6 +617,25 @@ static VALUE HttpParser_keepalive(VALUE self)
|
|
630
617
|
return HP_FL_ALL(hp, KEEPALIVE) ? Qtrue : Qfalse;
|
631
618
|
}
|
632
619
|
|
620
|
+
/**
|
621
|
+
* call-seq:
|
622
|
+
* parser.next? => true or false
|
623
|
+
*
|
624
|
+
* Exactly like HttpParser#keepalive?, except it will reset the internal
|
625
|
+
* parser state if it returns true.
|
626
|
+
*/
|
627
|
+
static VALUE HttpParser_next(VALUE self)
|
628
|
+
{
|
629
|
+
struct http_parser *hp = data_get(self);
|
630
|
+
|
631
|
+
if (HP_FL_ALL(hp, KEEPALIVE)) {
|
632
|
+
http_parser_init(hp);
|
633
|
+
rb_funcall(hp->env, id_clear, 0);
|
634
|
+
return Qtrue;
|
635
|
+
}
|
636
|
+
return Qfalse;
|
637
|
+
}
|
638
|
+
|
633
639
|
/**
|
634
640
|
* call-seq:
|
635
641
|
* parser.headers? => true or false
|
@@ -708,10 +714,13 @@ static VALUE HttpParser_filter_body(VALUE self, VALUE buf, VALUE data)
|
|
708
714
|
if (hp->len.content > 0) {
|
709
715
|
long nr = MIN(dlen, hp->len.content);
|
710
716
|
|
717
|
+
hp->buf = data;
|
711
718
|
memcpy(RSTRING_PTR(buf), dptr, nr);
|
712
719
|
hp->len.content -= nr;
|
713
|
-
if (hp->len.content == 0)
|
720
|
+
if (hp->len.content == 0) {
|
721
|
+
HP_FL_SET(hp, REQEOF);
|
714
722
|
hp->cs = http_parser_first_final;
|
723
|
+
}
|
715
724
|
advance_str(data, nr);
|
716
725
|
rb_str_set_len(buf, nr);
|
717
726
|
data = Qnil;
|
@@ -747,6 +756,7 @@ void Init_unicorn_http(void)
|
|
747
756
|
rb_define_method(cHttpParser, "body_eof?", HttpParser_body_eof, 0);
|
748
757
|
rb_define_method(cHttpParser, "keepalive?", HttpParser_keepalive, 0);
|
749
758
|
rb_define_method(cHttpParser, "headers?", HttpParser_has_headers, 0);
|
759
|
+
rb_define_method(cHttpParser, "next?", HttpParser_next, 0);
|
750
760
|
rb_define_method(cHttpParser, "buf", HttpParser_buf, 0);
|
751
761
|
rb_define_method(cHttpParser, "env", HttpParser_env, 0);
|
752
762
|
|
data/lib/unicorn.rb
CHANGED
data/lib/unicorn/configurator.rb
CHANGED
@@ -39,6 +39,7 @@ class Unicorn::Configurator
|
|
39
39
|
},
|
40
40
|
:pid => nil,
|
41
41
|
:preload_app => false,
|
42
|
+
:rewindable_input => true, # for Rack 2.x: (Rack::VERSION[0] <= 1),
|
42
43
|
}
|
43
44
|
#:startdoc:
|
44
45
|
|
@@ -373,12 +374,22 @@ class Unicorn::Configurator
|
|
373
374
|
# cause the master process to exit with an error.
|
374
375
|
|
375
376
|
def preload_app(bool)
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
377
|
+
set_bool(:preload_app, bool)
|
378
|
+
end
|
379
|
+
|
380
|
+
# Toggles making <code>env["rack.input"]</code> rewindable.
|
381
|
+
# Disabling rewindability can improve performance by lowering
|
382
|
+
# I/O and memory usage for applications that accept uploads.
|
383
|
+
# Keep in mind that the Rack 1.x spec requires
|
384
|
+
# <code>env["rack.input"]</code> to be rewindable, so this allows
|
385
|
+
# intentionally violating the current Rack 1.x spec.
|
386
|
+
#
|
387
|
+
# +rewindable_input+ defaults to +true+ when used with Rack 1.x for
|
388
|
+
# Rack conformance. When Rack 2.x is finalized, this will most
|
389
|
+
# likely default to +false+ while still conforming to the newer
|
390
|
+
# (less demanding) spec.
|
391
|
+
def rewindable_input(bool)
|
392
|
+
set_bool(:rewindable_input, bool)
|
382
393
|
end
|
383
394
|
|
384
395
|
# Allow redirecting $stderr to a given path. Unlike doing this from
|
@@ -469,6 +480,15 @@ private
|
|
469
480
|
end
|
470
481
|
end
|
471
482
|
|
483
|
+
def set_bool(var, bool) #:nodoc:
|
484
|
+
case bool
|
485
|
+
when true, false
|
486
|
+
set[var] = bool
|
487
|
+
else
|
488
|
+
raise ArgumentError, "#{var}=#{bool.inspect} not a boolean"
|
489
|
+
end
|
490
|
+
end
|
491
|
+
|
472
492
|
def set_hook(var, my_proc, req_arity = 2) #:nodoc:
|
473
493
|
case my_proc
|
474
494
|
when Proc
|
data/lib/unicorn/const.rb
CHANGED
@@ -7,8 +7,8 @@
|
|
7
7
|
# improve things much compared to constants.
|
8
8
|
module Unicorn::Const
|
9
9
|
|
10
|
-
# The current version of Unicorn, currently
|
11
|
-
UNICORN_VERSION = "
|
10
|
+
# The current version of Unicorn, currently 3.0.0pre1
|
11
|
+
UNICORN_VERSION = "3.0.0pre1"
|
12
12
|
|
13
13
|
# default TCP listen host address (0.0.0.0, all interfaces)
|
14
14
|
DEFAULT_HOST = "0.0.0.0"
|
data/lib/unicorn/http_request.rb
CHANGED
@@ -25,7 +25,15 @@ class Unicorn::HttpParser
|
|
25
25
|
# A frozen format for this is about 15% faster
|
26
26
|
REMOTE_ADDR = 'REMOTE_ADDR'.freeze
|
27
27
|
RACK_INPUT = 'rack.input'.freeze
|
28
|
-
|
28
|
+
@@input_class = Unicorn::TeeInput
|
29
|
+
|
30
|
+
def self.input_class
|
31
|
+
@@input_class
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.input_class=(klass)
|
35
|
+
@@input_class = klass
|
36
|
+
end
|
29
37
|
# :startdoc:
|
30
38
|
|
31
39
|
# Does the majority of the IO processing. It has been written in
|
@@ -63,7 +71,8 @@ class Unicorn::HttpParser
|
|
63
71
|
buf << socket.kgio_read!(16384)
|
64
72
|
end while parse.nil?
|
65
73
|
end
|
66
|
-
e[RACK_INPUT] = 0 == content_length ?
|
74
|
+
e[RACK_INPUT] = 0 == content_length ?
|
75
|
+
NULL_IO : @@input_class.new(socket, self)
|
67
76
|
e.merge!(DEFAULTS)
|
68
77
|
end
|
69
78
|
end
|
data/lib/unicorn/http_server.rb
CHANGED
@@ -355,6 +355,15 @@ class Unicorn::HttpServer
|
|
355
355
|
kill_each_worker(:KILL)
|
356
356
|
end
|
357
357
|
|
358
|
+
def rewindable_input
|
359
|
+
Unicorn::HttpRequest.input_class.method_defined?(:rewind)
|
360
|
+
end
|
361
|
+
|
362
|
+
def rewindable_input=(bool)
|
363
|
+
Unicorn::HttpRequest.input_class = bool ?
|
364
|
+
Unicorn::TeeInput : Unicorn::StreamInput
|
365
|
+
end
|
366
|
+
|
358
367
|
private
|
359
368
|
|
360
369
|
# wait for a signal hander to wake us up and then consume the pipe
|
@@ -491,7 +500,7 @@ class Unicorn::HttpServer
|
|
491
500
|
msg = case e
|
492
501
|
when EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::EINVAL,Errno::EBADF
|
493
502
|
Unicorn::Const::ERROR_500_RESPONSE
|
494
|
-
when
|
503
|
+
when HttpParserError # try to tell the client they're bad
|
495
504
|
Unicorn::Const::ERROR_400_RESPONSE
|
496
505
|
else
|
497
506
|
logger.error "Read error: #{e.inspect}"
|
@@ -0,0 +1,156 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
|
3
|
+
# When processing uploads, Unicorn may expose a StreamInput object under
|
4
|
+
# "rack.input" of the Rack (2.x) environment.
|
5
|
+
class Unicorn::StreamInput
|
6
|
+
# The I/O chunk size (in +bytes+) for I/O operations where
|
7
|
+
# the size cannot be user-specified when a method is called.
|
8
|
+
# The default is 16 kilobytes.
|
9
|
+
@@io_chunk_size = Unicorn::Const::CHUNK_SIZE
|
10
|
+
|
11
|
+
# Initializes a new StreamInput object. You normally do not have to call
|
12
|
+
# this unless you are writing an HTTP server.
|
13
|
+
def initialize(socket, request)
|
14
|
+
@chunked = request.content_length.nil?
|
15
|
+
@socket = socket
|
16
|
+
@parser = request
|
17
|
+
@buf = request.buf
|
18
|
+
@rbuf = ''
|
19
|
+
@bytes_read = 0
|
20
|
+
filter_body(@rbuf, @buf) unless @buf.empty?
|
21
|
+
end
|
22
|
+
|
23
|
+
# :call-seq:
|
24
|
+
# ios.read([length [, buffer ]]) => string, buffer, or nil
|
25
|
+
#
|
26
|
+
# Reads at most length bytes from the I/O stream, or to the end of
|
27
|
+
# file if length is omitted or is nil. length must be a non-negative
|
28
|
+
# integer or nil. If the optional buffer argument is present, it
|
29
|
+
# must reference a String, which will receive the data.
|
30
|
+
#
|
31
|
+
# At end of file, it returns nil or '' depend on length.
|
32
|
+
# ios.read() and ios.read(nil) returns ''.
|
33
|
+
# ios.read(length [, buffer]) returns nil.
|
34
|
+
#
|
35
|
+
# If the Content-Length of the HTTP request is known (as is the common
|
36
|
+
# case for POST requests), then ios.read(length [, buffer]) will block
|
37
|
+
# until the specified length is read (or it is the last chunk).
|
38
|
+
# Otherwise, for uncommon "Transfer-Encoding: chunked" requests,
|
39
|
+
# ios.read(length [, buffer]) will return immediately if there is
|
40
|
+
# any data and only block when nothing is available (providing
|
41
|
+
# IO#readpartial semantics).
|
42
|
+
def read(*args)
|
43
|
+
length = args.shift
|
44
|
+
rv = args.shift || ''
|
45
|
+
if length.nil?
|
46
|
+
read_all(rv)
|
47
|
+
else
|
48
|
+
if length <= @rbuf.size
|
49
|
+
rv.replace(@rbuf.slice(0, length))
|
50
|
+
@rbuf.replace(@rbuf.slice(length, @rbuf.size) || '')
|
51
|
+
else
|
52
|
+
rv.replace(@rbuf)
|
53
|
+
length -= @rbuf.size
|
54
|
+
@rbuf.replace('')
|
55
|
+
until length == 0 || eof? || (rv.size > 0 && @chunked)
|
56
|
+
@socket.kgio_read(length, @buf) or eof!
|
57
|
+
filter_body(@rbuf, @buf)
|
58
|
+
rv << @rbuf
|
59
|
+
length -= @rbuf.size
|
60
|
+
@rbuf.replace('')
|
61
|
+
end
|
62
|
+
end
|
63
|
+
rv = nil if rv.empty?
|
64
|
+
end
|
65
|
+
rv
|
66
|
+
end
|
67
|
+
|
68
|
+
# :call-seq:
|
69
|
+
# ios.gets => string or nil
|
70
|
+
#
|
71
|
+
# Reads the next ``line'' from the I/O stream; lines are separated
|
72
|
+
# by the global record separator ($/, typically "\n"). A global
|
73
|
+
# record separator of nil reads the entire unread contents of ios.
|
74
|
+
# Returns nil if called at the end of file.
|
75
|
+
# This takes zero arguments for strict Rack::Lint compatibility,
|
76
|
+
# unlike IO#gets.
|
77
|
+
def gets
|
78
|
+
sep = $/
|
79
|
+
if sep.nil?
|
80
|
+
read_all(rv = '')
|
81
|
+
return rv.empty? ? nil : rv
|
82
|
+
end
|
83
|
+
re = /\A(.*?#{Regexp.escape(sep)})/
|
84
|
+
|
85
|
+
begin
|
86
|
+
@rbuf.gsub!(re, '') and return $1
|
87
|
+
if eof?
|
88
|
+
if @rbuf.empty?
|
89
|
+
return nil
|
90
|
+
else
|
91
|
+
rv = @rbuf.dup
|
92
|
+
@rbuf.replace('')
|
93
|
+
return rv
|
94
|
+
end
|
95
|
+
end
|
96
|
+
@socket.kgio_read(@@io_chunk_size, @buf) or eof!
|
97
|
+
filter_body(once = '', @buf)
|
98
|
+
@rbuf << once
|
99
|
+
end while true
|
100
|
+
end
|
101
|
+
|
102
|
+
# :call-seq:
|
103
|
+
# ios.each { |line| block } => ios
|
104
|
+
#
|
105
|
+
# Executes the block for every ``line'' in *ios*, where lines are
|
106
|
+
# separated by the global record separator ($/, typically "\n").
|
107
|
+
def each(&block)
|
108
|
+
while line = gets
|
109
|
+
yield line
|
110
|
+
end
|
111
|
+
|
112
|
+
self # Rack does not specify what the return value is here
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
def eof?
|
118
|
+
if @parser.body_eof?
|
119
|
+
until @parser.parse
|
120
|
+
once = @socket.kgio_read(@@io_chunk_size) or eof!
|
121
|
+
@buf << once
|
122
|
+
end
|
123
|
+
@socket = nil
|
124
|
+
true
|
125
|
+
else
|
126
|
+
false
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def filter_body(dst, src)
|
131
|
+
rv = @parser.filter_body(dst, src)
|
132
|
+
@bytes_read += dst.size
|
133
|
+
rv
|
134
|
+
end
|
135
|
+
|
136
|
+
def read_all(dst)
|
137
|
+
dst.replace(@rbuf)
|
138
|
+
@socket or return
|
139
|
+
until eof?
|
140
|
+
@socket.kgio_read(@@io_chunk_size, @buf) or eof!
|
141
|
+
filter_body(@rbuf, @buf)
|
142
|
+
dst << @rbuf
|
143
|
+
end
|
144
|
+
ensure
|
145
|
+
@rbuf.replace('')
|
146
|
+
end
|
147
|
+
|
148
|
+
def eof!
|
149
|
+
# in case client only did a premature shutdown(SHUT_WR)
|
150
|
+
# we do support clients that shutdown(SHUT_WR) after the
|
151
|
+
# _entire_ request has been sent, and those will not have
|
152
|
+
# raised EOFError on us.
|
153
|
+
@socket.close if @socket
|
154
|
+
raise Unicorn::ClientShutdown, "bytes_read=#{@bytes_read}", []
|
155
|
+
end
|
156
|
+
end
|
data/lib/unicorn/tee_input.rb
CHANGED
@@ -11,34 +11,18 @@
|
|
11
11
|
#
|
12
12
|
# When processing uploads, Unicorn exposes a TeeInput object under
|
13
13
|
# "rack.input" of the Rack environment.
|
14
|
-
class Unicorn::TeeInput
|
15
|
-
attr_accessor :tmp, :socket, :parser, :env, :buf, :len, :buf2
|
16
|
-
|
14
|
+
class Unicorn::TeeInput < Unicorn::StreamInput
|
17
15
|
# The maximum size (in +bytes+) to buffer in memory before
|
18
16
|
# resorting to a temporary file. Default is 112 kilobytes.
|
19
17
|
@@client_body_buffer_size = Unicorn::Const::MAX_BODY
|
20
18
|
|
21
|
-
# The I/O chunk size (in +bytes+) for I/O operations where
|
22
|
-
# the size cannot be user-specified when a method is called.
|
23
|
-
# The default is 16 kilobytes.
|
24
|
-
@@io_chunk_size = Unicorn::Const::CHUNK_SIZE
|
25
|
-
|
26
19
|
# Initializes a new TeeInput object. You normally do not have to call
|
27
20
|
# this unless you are writing an HTTP server.
|
28
21
|
def initialize(socket, request)
|
29
|
-
@socket = socket
|
30
|
-
@parser = request
|
31
|
-
@buf = request.buf
|
32
|
-
@env = request.env
|
33
22
|
@len = request.content_length
|
23
|
+
super
|
34
24
|
@tmp = @len && @len < @@client_body_buffer_size ?
|
35
25
|
StringIO.new("") : Unicorn::TmpIO.new
|
36
|
-
@buf2 = ""
|
37
|
-
if @buf.size > 0
|
38
|
-
@parser.filter_body(@buf2, @buf) and finalize_input
|
39
|
-
@tmp.write(@buf2)
|
40
|
-
@tmp.rewind
|
41
|
-
end
|
42
26
|
end
|
43
27
|
|
44
28
|
# :call-seq:
|
@@ -59,15 +43,10 @@ class Unicorn::TeeInput
|
|
59
43
|
# specified +length+ in a loop until it returns +nil+.
|
60
44
|
def size
|
61
45
|
@len and return @len
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
end
|
67
|
-
@tmp.seek(pos)
|
68
|
-
end
|
69
|
-
|
70
|
-
@len = @tmp.size
|
46
|
+
pos = @bytes_read
|
47
|
+
consume!
|
48
|
+
@tmp.pos = pos
|
49
|
+
@len = @bytes_read
|
71
50
|
end
|
72
51
|
|
73
52
|
# :call-seq:
|
@@ -90,24 +69,7 @@ class Unicorn::TeeInput
|
|
90
69
|
# any data and only block when nothing is available (providing
|
91
70
|
# IO#readpartial semantics).
|
92
71
|
def read(*args)
|
93
|
-
@socket
|
94
|
-
|
95
|
-
length = args.shift
|
96
|
-
if nil == length
|
97
|
-
rv = @tmp.read || ""
|
98
|
-
while tee(@@io_chunk_size, @buf2)
|
99
|
-
rv << @buf2
|
100
|
-
end
|
101
|
-
rv
|
102
|
-
else
|
103
|
-
rv = args.shift || ""
|
104
|
-
diff = @tmp.size - @tmp.pos
|
105
|
-
if 0 == diff
|
106
|
-
ensure_length(tee(length, rv), length)
|
107
|
-
else
|
108
|
-
ensure_length(@tmp.read(diff > length ? length : diff, rv), length)
|
109
|
-
end
|
110
|
-
end
|
72
|
+
@socket ? tee(super) : @tmp.read(*args)
|
111
73
|
end
|
112
74
|
|
113
75
|
# :call-seq:
|
@@ -120,43 +82,7 @@ class Unicorn::TeeInput
|
|
120
82
|
# This takes zero arguments for strict Rack::Lint compatibility,
|
121
83
|
# unlike IO#gets.
|
122
84
|
def gets
|
123
|
-
@socket
|
124
|
-
sep = $/ or return read
|
125
|
-
|
126
|
-
orig_size = @tmp.size
|
127
|
-
if @tmp.pos == orig_size
|
128
|
-
tee(@@io_chunk_size, @buf2) or return nil
|
129
|
-
@tmp.seek(orig_size)
|
130
|
-
end
|
131
|
-
|
132
|
-
sep_size = Rack::Utils.bytesize(sep)
|
133
|
-
line = @tmp.gets # cannot be nil here since size > pos
|
134
|
-
sep == line[-sep_size, sep_size] and return line
|
135
|
-
|
136
|
-
# unlikely, if we got here, then @tmp is at EOF
|
137
|
-
begin
|
138
|
-
orig_size = @tmp.pos
|
139
|
-
tee(@@io_chunk_size, @buf2) or break
|
140
|
-
@tmp.seek(orig_size)
|
141
|
-
line << @tmp.gets
|
142
|
-
sep == line[-sep_size, sep_size] and return line
|
143
|
-
# @tmp is at EOF again here, retry the loop
|
144
|
-
end while true
|
145
|
-
|
146
|
-
line
|
147
|
-
end
|
148
|
-
|
149
|
-
# :call-seq:
|
150
|
-
# ios.each { |line| block } => ios
|
151
|
-
#
|
152
|
-
# Executes the block for every ``line'' in *ios*, where lines are
|
153
|
-
# separated by the global record separator ($/, typically "\n").
|
154
|
-
def each(&block)
|
155
|
-
while line = gets
|
156
|
-
yield line
|
157
|
-
end
|
158
|
-
|
159
|
-
self # Rack does not specify what the return value is here
|
85
|
+
@socket ? tee(super) : @tmp.gets
|
160
86
|
end
|
161
87
|
|
162
88
|
# :call-seq:
|
@@ -166,59 +92,24 @@ class Unicorn::TeeInput
|
|
166
92
|
# the offset (zero) of the +ios+ pointer. Subsequent reads will
|
167
93
|
# start from the beginning of the previously-buffered input.
|
168
94
|
def rewind
|
95
|
+
return 0 if @bytes_read == 0
|
96
|
+
consume! if @socket
|
169
97
|
@tmp.rewind # Rack does not specify what the return value is here
|
170
98
|
end
|
171
99
|
|
172
100
|
private
|
173
101
|
|
174
|
-
#
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
unless @parser.body_eof?
|
179
|
-
r = @socket.kgio_read(length, @buf) or eof!
|
180
|
-
unless @parser.filter_body(dst, @buf)
|
181
|
-
@tmp.write(dst)
|
182
|
-
@tmp.seek(0, IO::SEEK_END) # workaround FreeBSD/OSX + MRI 1.8.x bug
|
183
|
-
return dst
|
184
|
-
end
|
185
|
-
end
|
186
|
-
finalize_input
|
102
|
+
# consumes the stream of the socket
|
103
|
+
def consume!
|
104
|
+
junk = ""
|
105
|
+
nil while read(@@io_chunk_size, junk)
|
187
106
|
end
|
188
107
|
|
189
|
-
def
|
190
|
-
|
191
|
-
|
192
|
-
@
|
108
|
+
def tee(buffer)
|
109
|
+
if buffer && (n = buffer.size) > 0
|
110
|
+
@tmp.write(buffer)
|
111
|
+
@tmp.seek(0, IO::SEEK_END) # workaround FreeBSD/OSX + MRI 1.8.x bug
|
193
112
|
end
|
194
|
-
|
195
|
-
end
|
196
|
-
|
197
|
-
# tee()s into +dst+ until it is of +length+ bytes (or until
|
198
|
-
# we've reached the Content-Length of the request body).
|
199
|
-
# Returns +dst+ (the exact object, not a duplicate)
|
200
|
-
# To continue supporting applications that need near-real-time
|
201
|
-
# streaming input bodies, this is a no-op for
|
202
|
-
# "Transfer-Encoding: chunked" requests.
|
203
|
-
def ensure_length(dst, length)
|
204
|
-
# len is nil for chunked bodies, so we can't ensure length for those
|
205
|
-
# since they could be streaming bidirectionally and we don't want to
|
206
|
-
# block the caller in that case.
|
207
|
-
return dst if dst.nil? || @len.nil?
|
208
|
-
|
209
|
-
while dst.size < length && tee(length - dst.size, @buf2)
|
210
|
-
dst << @buf2
|
211
|
-
end
|
212
|
-
|
213
|
-
dst
|
214
|
-
end
|
215
|
-
|
216
|
-
def eof!
|
217
|
-
# in case client only did a premature shutdown(SHUT_WR)
|
218
|
-
# we do support clients that shutdown(SHUT_WR) after the
|
219
|
-
# _entire_ request has been sent, and those will not have
|
220
|
-
# raised EOFError on us.
|
221
|
-
@socket.close if @socket
|
222
|
-
raise Unicorn::ClientShutdown, "bytes_read=#{@tmp.size}", []
|
113
|
+
buffer
|
223
114
|
end
|
224
115
|
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
#!/bin/sh
|
2
|
+
. ./test-lib.sh
|
3
|
+
t_plan 4 "rewindable_input toggled to false"
|
4
|
+
|
5
|
+
t_begin "setup and start" && {
|
6
|
+
unicorn_setup
|
7
|
+
echo rewindable_input false >> $unicorn_config
|
8
|
+
unicorn -D -c $unicorn_config t0013.ru
|
9
|
+
unicorn_wait_start
|
10
|
+
}
|
11
|
+
|
12
|
+
t_begin "ensure worker is started" && {
|
13
|
+
test xOK = x$(curl -T t0013.ru -H Expect: -vsSf http://$listen/)
|
14
|
+
}
|
15
|
+
|
16
|
+
t_begin "killing succeeds" && {
|
17
|
+
kill $unicorn_pid
|
18
|
+
}
|
19
|
+
|
20
|
+
t_begin "check stderr" && {
|
21
|
+
check_stderr
|
22
|
+
}
|
23
|
+
|
24
|
+
t_done
|
@@ -0,0 +1,24 @@
|
|
1
|
+
#!/bin/sh
|
2
|
+
. ./test-lib.sh
|
3
|
+
t_plan 4 "rewindable_input toggled to true"
|
4
|
+
|
5
|
+
t_begin "setup and start" && {
|
6
|
+
unicorn_setup
|
7
|
+
echo rewindable_input true >> $unicorn_config
|
8
|
+
unicorn -D -c $unicorn_config t0014.ru
|
9
|
+
unicorn_wait_start
|
10
|
+
}
|
11
|
+
|
12
|
+
t_begin "ensure worker is started" && {
|
13
|
+
test xOK = x$(curl -T t0014.ru -sSf http://$listen/)
|
14
|
+
}
|
15
|
+
|
16
|
+
t_begin "killing succeeds" && {
|
17
|
+
kill $unicorn_pid
|
18
|
+
}
|
19
|
+
|
20
|
+
t_begin "check stderr" && {
|
21
|
+
check_stderr
|
22
|
+
}
|
23
|
+
|
24
|
+
t_done
|
@@ -76,12 +76,22 @@ class HttpParserTest < Test::Unit::TestCase
|
|
76
76
|
assert parser.keepalive?
|
77
77
|
end
|
78
78
|
|
79
|
-
def
|
79
|
+
def test_connection_keep_alive_no_body
|
80
80
|
parser = HttpParser.new
|
81
81
|
req = {}
|
82
82
|
tmp = "POST / HTTP/1.1\r\nConnection: keep-alive\r\n\r\n"
|
83
83
|
assert_equal req.object_id, parser.headers(req, tmp).object_id
|
84
|
-
assert
|
84
|
+
assert parser.keepalive?
|
85
|
+
end
|
86
|
+
|
87
|
+
def test_connection_keep_alive_no_body_empty
|
88
|
+
parser = HttpParser.new
|
89
|
+
req = {}
|
90
|
+
tmp = "POST / HTTP/1.1\r\n" \
|
91
|
+
"Content-Length: 0\r\n" \
|
92
|
+
"Connection: keep-alive\r\n\r\n"
|
93
|
+
assert_equal req.object_id, parser.headers(req, tmp).object_id
|
94
|
+
assert parser.keepalive?
|
85
95
|
end
|
86
96
|
|
87
97
|
def test_connection_keep_alive_ka_bad_version
|
@@ -461,7 +471,7 @@ class HttpParserTest < Test::Unit::TestCase
|
|
461
471
|
assert_equal 'page=1', req['QUERY_STRING']
|
462
472
|
assert_equal "", s
|
463
473
|
assert_equal m, req['REQUEST_METHOD']
|
464
|
-
assert
|
474
|
+
assert parser.keepalive? # TODO: read HTTP/1.2 when it's final
|
465
475
|
}
|
466
476
|
end
|
467
477
|
|
@@ -11,6 +11,19 @@ class HttpParserNgTest < Test::Unit::TestCase
|
|
11
11
|
@parser = HttpParser.new
|
12
12
|
end
|
13
13
|
|
14
|
+
def test_default_keepalive_is_off
|
15
|
+
assert ! @parser.keepalive?
|
16
|
+
assert ! @parser.next?
|
17
|
+
assert_nothing_raised do
|
18
|
+
@parser.buf << "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"
|
19
|
+
@parser.parse
|
20
|
+
end
|
21
|
+
assert @parser.keepalive?
|
22
|
+
@parser.reset
|
23
|
+
assert ! @parser.keepalive?
|
24
|
+
assert ! @parser.next?
|
25
|
+
end
|
26
|
+
|
14
27
|
def test_identity_byte_headers
|
15
28
|
req = {}
|
16
29
|
str = "PUT / HTTP/1.1\r\n"
|
@@ -27,6 +40,12 @@ class HttpParserNgTest < Test::Unit::TestCase
|
|
27
40
|
assert ! @parser.keepalive?
|
28
41
|
assert @parser.headers?
|
29
42
|
assert_equal 123, @parser.content_length
|
43
|
+
dst = ""
|
44
|
+
buf = '.' * 123
|
45
|
+
@parser.filter_body(dst, buf)
|
46
|
+
assert_equal '.' * 123, dst
|
47
|
+
assert_equal "", buf
|
48
|
+
assert @parser.keepalive?
|
30
49
|
end
|
31
50
|
|
32
51
|
def test_identity_step_headers
|
@@ -41,6 +60,12 @@ class HttpParserNgTest < Test::Unit::TestCase
|
|
41
60
|
assert_equal 0, str.size
|
42
61
|
assert ! @parser.keepalive?
|
43
62
|
assert @parser.headers?
|
63
|
+
dst = ""
|
64
|
+
buf = '.' * 123
|
65
|
+
@parser.filter_body(dst, buf)
|
66
|
+
assert_equal '.' * 123, dst
|
67
|
+
assert_equal "", buf
|
68
|
+
assert @parser.keepalive?
|
44
69
|
end
|
45
70
|
|
46
71
|
def test_identity_oneshot_header
|
@@ -50,6 +75,12 @@ class HttpParserNgTest < Test::Unit::TestCase
|
|
50
75
|
assert_equal '123', req['CONTENT_LENGTH']
|
51
76
|
assert_equal 0, str.size
|
52
77
|
assert ! @parser.keepalive?
|
78
|
+
assert @parser.headers?
|
79
|
+
dst = ""
|
80
|
+
buf = '.' * 123
|
81
|
+
@parser.filter_body(dst, buf)
|
82
|
+
assert_equal '.' * 123, dst
|
83
|
+
assert_equal "", buf
|
53
84
|
end
|
54
85
|
|
55
86
|
def test_identity_oneshot_header_with_body
|
@@ -67,7 +98,7 @@ class HttpParserNgTest < Test::Unit::TestCase
|
|
67
98
|
assert_equal 0, str.size
|
68
99
|
assert_equal tmp, body
|
69
100
|
assert_equal "", @parser.filter_body(tmp, str)
|
70
|
-
assert
|
101
|
+
assert @parser.keepalive?
|
71
102
|
end
|
72
103
|
|
73
104
|
def test_identity_oneshot_header_with_body_partial
|
@@ -85,7 +116,7 @@ class HttpParserNgTest < Test::Unit::TestCase
|
|
85
116
|
assert_nil rv
|
86
117
|
assert_equal "", str
|
87
118
|
assert_equal str.object_id, @parser.filter_body(tmp, str).object_id
|
88
|
-
assert
|
119
|
+
assert @parser.keepalive?
|
89
120
|
end
|
90
121
|
|
91
122
|
def test_identity_oneshot_header_with_body_slop
|
@@ -99,7 +130,7 @@ class HttpParserNgTest < Test::Unit::TestCase
|
|
99
130
|
assert_equal "G", @parser.filter_body(tmp, str)
|
100
131
|
assert_equal 1, tmp.size
|
101
132
|
assert_equal "a", tmp
|
102
|
-
assert
|
133
|
+
assert @parser.keepalive?
|
103
134
|
end
|
104
135
|
|
105
136
|
def test_chunked
|
@@ -122,6 +153,10 @@ class HttpParserNgTest < Test::Unit::TestCase
|
|
122
153
|
assert_equal rv.object_id, @parser.filter_body(tmp, rv).object_id
|
123
154
|
assert_equal "PUT", rv
|
124
155
|
assert ! @parser.keepalive?
|
156
|
+
rv << "TY: FOO\r\n\r\n"
|
157
|
+
assert_equal req, @parser.trailers(req, rv)
|
158
|
+
assert_equal "FOO", req["HTTP_PUTTY"]
|
159
|
+
assert @parser.keepalive?
|
125
160
|
end
|
126
161
|
|
127
162
|
def test_two_chunks
|
@@ -177,7 +212,7 @@ class HttpParserNgTest < Test::Unit::TestCase
|
|
177
212
|
assert_equal req, @parser.trailers(req, moo = "\r\n")
|
178
213
|
assert_equal "", moo
|
179
214
|
assert @parser.body_eof?
|
180
|
-
assert
|
215
|
+
assert @parser.keepalive?
|
181
216
|
end
|
182
217
|
|
183
218
|
def test_two_chunks_oneshot
|
@@ -237,7 +272,7 @@ class HttpParserNgTest < Test::Unit::TestCase
|
|
237
272
|
assert_nil @parser.trailers(req, str << "\r")
|
238
273
|
assert_equal req, @parser.trailers(req, str << "\nGET / ")
|
239
274
|
assert_equal "GET / ", str
|
240
|
-
assert
|
275
|
+
assert @parser.keepalive?
|
241
276
|
end
|
242
277
|
|
243
278
|
def test_trailers_slowly
|
@@ -297,14 +332,12 @@ class HttpParserNgTest < Test::Unit::TestCase
|
|
297
332
|
assert_equal req, @parser.headers(req, str)
|
298
333
|
assert_nil @parser.content_length
|
299
334
|
assert_raise(HttpParserError) { @parser.filter_body('', str) }
|
300
|
-
assert ! @parser.keepalive?
|
301
335
|
end
|
302
336
|
|
303
337
|
def test_overflow_content_length
|
304
338
|
n = HttpParser::LENGTH_MAX + 1
|
305
339
|
str = "PUT / HTTP/1.1\r\nContent-Length: #{n}\r\n\r\n"
|
306
340
|
assert_raise(HttpParserError) { @parser.headers({}, str) }
|
307
|
-
assert ! @parser.keepalive?
|
308
341
|
end
|
309
342
|
|
310
343
|
def test_bad_chunk
|
@@ -315,13 +348,11 @@ class HttpParserNgTest < Test::Unit::TestCase
|
|
315
348
|
assert_equal req, @parser.headers(req, str)
|
316
349
|
assert_nil @parser.content_length
|
317
350
|
assert_raise(HttpParserError) { @parser.filter_body('', str) }
|
318
|
-
assert ! @parser.keepalive?
|
319
351
|
end
|
320
352
|
|
321
353
|
def test_bad_content_length
|
322
354
|
str = "PUT / HTTP/1.1\r\nContent-Length: 7ff\r\n\r\n"
|
323
355
|
assert_raise(HttpParserError) { @parser.headers({}, str) }
|
324
|
-
assert ! @parser.keepalive?
|
325
356
|
end
|
326
357
|
|
327
358
|
def test_bad_trailers
|
@@ -338,7 +369,6 @@ class HttpParserNgTest < Test::Unit::TestCase
|
|
338
369
|
assert_equal '', str
|
339
370
|
str << "Transfer-Encoding: identity\r\n\r\n"
|
340
371
|
assert_raise(HttpParserError) { @parser.trailers(req, str) }
|
341
|
-
assert ! @parser.keepalive?
|
342
372
|
end
|
343
373
|
|
344
374
|
def test_repeat_headers
|
@@ -492,8 +522,9 @@ class HttpParserNgTest < Test::Unit::TestCase
|
|
492
522
|
env1 = @parser.parse.dup
|
493
523
|
assert_equal expect, env1
|
494
524
|
assert_equal str, @parser.buf
|
495
|
-
assert @parser.
|
496
|
-
@parser.
|
525
|
+
assert ! @parser.env.empty?
|
526
|
+
assert @parser.next?
|
527
|
+
assert @parser.env.empty?
|
497
528
|
env2 = @parser.parse.dup
|
498
529
|
assert_equal expect, env2
|
499
530
|
assert_equal "", @parser.buf
|
@@ -0,0 +1,143 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
|
3
|
+
require 'test/unit'
|
4
|
+
require 'digest/sha1'
|
5
|
+
require 'unicorn'
|
6
|
+
|
7
|
+
class TestStreamInput < Test::Unit::TestCase
|
8
|
+
def setup
|
9
|
+
@rs = $/
|
10
|
+
@env = {}
|
11
|
+
@rd, @wr = Kgio::UNIXSocket.pair
|
12
|
+
@rd.sync = @wr.sync = true
|
13
|
+
@start_pid = $$
|
14
|
+
end
|
15
|
+
|
16
|
+
def teardown
|
17
|
+
return if $$ != @start_pid
|
18
|
+
$/ = @rs
|
19
|
+
@rd.close rescue nil
|
20
|
+
@wr.close rescue nil
|
21
|
+
Process.waitall
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_read_small
|
25
|
+
r = init_request('hello')
|
26
|
+
si = Unicorn::StreamInput.new(@rd, r)
|
27
|
+
assert_equal 'hello', si.read
|
28
|
+
assert_equal '', si.read
|
29
|
+
assert_nil si.read(5)
|
30
|
+
assert_nil si.gets
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_gets_oneliner
|
34
|
+
r = init_request('hello')
|
35
|
+
si = Unicorn::StreamInput.new(@rd, r)
|
36
|
+
assert_equal 'hello', si.gets
|
37
|
+
assert_nil si.gets
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_gets_multiline
|
41
|
+
r = init_request("a\nb\n\n")
|
42
|
+
si = Unicorn::StreamInput.new(@rd, r)
|
43
|
+
assert_equal "a\n", si.gets
|
44
|
+
assert_equal "b\n", si.gets
|
45
|
+
assert_equal "\n", si.gets
|
46
|
+
assert_nil si.gets
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_gets_empty_rs
|
50
|
+
$/ = nil
|
51
|
+
r = init_request("a\nb\n\n")
|
52
|
+
si = Unicorn::StreamInput.new(@rd, r)
|
53
|
+
assert_equal "a\nb\n\n", si.gets
|
54
|
+
assert_nil si.gets
|
55
|
+
end
|
56
|
+
|
57
|
+
def test_read_with_equal_len
|
58
|
+
r = init_request("abcde")
|
59
|
+
si = Unicorn::StreamInput.new(@rd, r)
|
60
|
+
assert_equal "abcde", si.read(5)
|
61
|
+
assert_nil si.read(5)
|
62
|
+
end
|
63
|
+
|
64
|
+
def test_big_body_multi
|
65
|
+
r = init_request('.', Unicorn::Const::MAX_BODY + 1)
|
66
|
+
si = Unicorn::StreamInput.new(@rd, r)
|
67
|
+
assert_equal Unicorn::Const::MAX_BODY, @parser.content_length
|
68
|
+
assert ! @parser.body_eof?
|
69
|
+
nr = Unicorn::Const::MAX_BODY / 4
|
70
|
+
pid = fork {
|
71
|
+
@rd.close
|
72
|
+
nr.times { @wr.write('....') }
|
73
|
+
@wr.close
|
74
|
+
}
|
75
|
+
@wr.close
|
76
|
+
assert_equal '.', si.read(1)
|
77
|
+
nr.times { |x|
|
78
|
+
assert_equal '....', si.read(4), "nr=#{x}"
|
79
|
+
}
|
80
|
+
assert_nil si.read(1)
|
81
|
+
status = nil
|
82
|
+
assert_nothing_raised { pid, status = Process.waitpid2(pid) }
|
83
|
+
assert status.success?
|
84
|
+
end
|
85
|
+
|
86
|
+
def test_gets_long
|
87
|
+
r = init_request("hello", 5 + (4096 * 4 * 3) + "#$/foo#$/".size)
|
88
|
+
si = Unicorn::StreamInput.new(@rd, r)
|
89
|
+
status = line = nil
|
90
|
+
pid = fork {
|
91
|
+
@rd.close
|
92
|
+
3.times { @wr.write("ffff" * 4096) }
|
93
|
+
@wr.write "#$/foo#$/"
|
94
|
+
@wr.close
|
95
|
+
}
|
96
|
+
@wr.close
|
97
|
+
assert_nothing_raised { line = si.gets }
|
98
|
+
assert_equal(4096 * 4 * 3 + 5 + $/.size, line.size)
|
99
|
+
assert_equal("hello" << ("ffff" * 4096 * 3) << "#$/", line)
|
100
|
+
assert_nothing_raised { line = si.gets }
|
101
|
+
assert_equal "foo#$/", line
|
102
|
+
assert_nil si.gets
|
103
|
+
assert_nothing_raised { pid, status = Process.waitpid2(pid) }
|
104
|
+
assert status.success?
|
105
|
+
end
|
106
|
+
|
107
|
+
def test_read_with_buffer
|
108
|
+
r = init_request('hello')
|
109
|
+
si = Unicorn::StreamInput.new(@rd, r)
|
110
|
+
buf = ''
|
111
|
+
rv = si.read(4, buf)
|
112
|
+
assert_equal 'hell', rv
|
113
|
+
assert_equal 'hell', buf
|
114
|
+
assert_equal rv.object_id, buf.object_id
|
115
|
+
assert_equal 'o', si.read
|
116
|
+
assert_equal nil, si.read(5, buf)
|
117
|
+
end
|
118
|
+
|
119
|
+
def test_read_with_buffer_clobbers
|
120
|
+
r = init_request('hello')
|
121
|
+
si = Unicorn::StreamInput.new(@rd, r)
|
122
|
+
buf = 'foo'
|
123
|
+
assert_equal 'hello', si.read(nil, buf)
|
124
|
+
assert_equal 'hello', buf
|
125
|
+
assert_equal '', si.read(nil, buf)
|
126
|
+
assert_equal '', buf
|
127
|
+
buf = 'asdf'
|
128
|
+
assert_nil si.read(5, buf)
|
129
|
+
assert_equal '', buf
|
130
|
+
end
|
131
|
+
|
132
|
+
def init_request(body, size = nil)
|
133
|
+
@parser = Unicorn::HttpParser.new
|
134
|
+
body = body.to_s.freeze
|
135
|
+
@buf = "POST / HTTP/1.1\r\n" \
|
136
|
+
"Host: localhost\r\n" \
|
137
|
+
"Content-Length: #{size || body.size}\r\n" \
|
138
|
+
"\r\n#{body}"
|
139
|
+
assert_equal @env, @parser.headers(@env, @buf)
|
140
|
+
assert_equal body, @buf
|
141
|
+
@parser
|
142
|
+
end
|
143
|
+
end
|
data/test/unit/test_tee_input.rb
CHANGED
@@ -4,6 +4,10 @@ require 'test/unit'
|
|
4
4
|
require 'digest/sha1'
|
5
5
|
require 'unicorn'
|
6
6
|
|
7
|
+
class TeeInput < Unicorn::TeeInput
|
8
|
+
attr_accessor :tmp, :len
|
9
|
+
end
|
10
|
+
|
7
11
|
class TestTeeInput < Test::Unit::TestCase
|
8
12
|
|
9
13
|
def setup
|
@@ -28,7 +32,7 @@ class TestTeeInput < Test::Unit::TestCase
|
|
28
32
|
|
29
33
|
def test_gets_long
|
30
34
|
r = init_request("hello", 5 + (4096 * 4 * 3) + "#$/foo#$/".size)
|
31
|
-
ti =
|
35
|
+
ti = TeeInput.new(@rd, r)
|
32
36
|
status = line = nil
|
33
37
|
pid = fork {
|
34
38
|
@rd.close
|
@@ -49,7 +53,7 @@ class TestTeeInput < Test::Unit::TestCase
|
|
49
53
|
|
50
54
|
def test_gets_short
|
51
55
|
r = init_request("hello", 5 + "#$/foo".size)
|
52
|
-
ti =
|
56
|
+
ti = TeeInput.new(@rd, r)
|
53
57
|
status = line = nil
|
54
58
|
pid = fork {
|
55
59
|
@rd.close
|
@@ -68,7 +72,7 @@ class TestTeeInput < Test::Unit::TestCase
|
|
68
72
|
|
69
73
|
def test_small_body
|
70
74
|
r = init_request('hello')
|
71
|
-
ti =
|
75
|
+
ti = TeeInput.new(@rd, r)
|
72
76
|
assert_equal 0, @parser.content_length
|
73
77
|
assert @parser.body_eof?
|
74
78
|
assert_equal StringIO, ti.tmp.class
|
@@ -77,11 +81,12 @@ class TestTeeInput < Test::Unit::TestCase
|
|
77
81
|
assert_equal 'hello', ti.read
|
78
82
|
assert_equal '', ti.read
|
79
83
|
assert_nil ti.read(4096)
|
84
|
+
assert_equal 5, ti.size
|
80
85
|
end
|
81
86
|
|
82
87
|
def test_read_with_buffer
|
83
88
|
r = init_request('hello')
|
84
|
-
ti =
|
89
|
+
ti = TeeInput.new(@rd, r)
|
85
90
|
buf = ''
|
86
91
|
rv = ti.read(4, buf)
|
87
92
|
assert_equal 'hell', rv
|
@@ -96,7 +101,7 @@ class TestTeeInput < Test::Unit::TestCase
|
|
96
101
|
|
97
102
|
def test_big_body
|
98
103
|
r = init_request('.' * Unicorn::Const::MAX_BODY << 'a')
|
99
|
-
ti =
|
104
|
+
ti = TeeInput.new(@rd, r)
|
100
105
|
assert_equal 0, @parser.content_length
|
101
106
|
assert @parser.body_eof?
|
102
107
|
assert_kind_of File, ti.tmp
|
@@ -108,7 +113,7 @@ class TestTeeInput < Test::Unit::TestCase
|
|
108
113
|
a, b = 300, 3
|
109
114
|
r = init_request('.' * b, 300)
|
110
115
|
assert_equal 300, @parser.content_length
|
111
|
-
ti =
|
116
|
+
ti = TeeInput.new(@rd, r)
|
112
117
|
pid = fork {
|
113
118
|
@wr.write('.' * 197)
|
114
119
|
sleep 1 # still a *potential* race here that would make the test moot...
|
@@ -122,12 +127,11 @@ class TestTeeInput < Test::Unit::TestCase
|
|
122
127
|
|
123
128
|
def test_big_body_multi
|
124
129
|
r = init_request('.', Unicorn::Const::MAX_BODY + 1)
|
125
|
-
ti =
|
130
|
+
ti = TeeInput.new(@rd, r)
|
126
131
|
assert_equal Unicorn::Const::MAX_BODY, @parser.content_length
|
127
132
|
assert ! @parser.body_eof?
|
128
133
|
assert_kind_of File, ti.tmp
|
129
134
|
assert_equal 0, ti.tmp.pos
|
130
|
-
assert_equal 1, ti.tmp.size
|
131
135
|
assert_equal Unicorn::Const::MAX_BODY + 1, ti.size
|
132
136
|
nr = Unicorn::Const::MAX_BODY / 4
|
133
137
|
pid = fork {
|
@@ -138,8 +142,8 @@ class TestTeeInput < Test::Unit::TestCase
|
|
138
142
|
@wr.close
|
139
143
|
assert_equal '.', ti.read(1)
|
140
144
|
assert_equal Unicorn::Const::MAX_BODY + 1, ti.size
|
141
|
-
nr.times {
|
142
|
-
assert_equal '....', ti.read(4)
|
145
|
+
nr.times { |x|
|
146
|
+
assert_equal '....', ti.read(4), "nr=#{x}"
|
143
147
|
assert_equal Unicorn::Const::MAX_BODY + 1, ti.size
|
144
148
|
}
|
145
149
|
assert_nil ti.read(1)
|
@@ -163,7 +167,7 @@ class TestTeeInput < Test::Unit::TestCase
|
|
163
167
|
@wr.write("0\r\n\r\n")
|
164
168
|
}
|
165
169
|
@wr.close
|
166
|
-
ti =
|
170
|
+
ti = TeeInput.new(@rd, @parser)
|
167
171
|
assert_nil @parser.content_length
|
168
172
|
assert_nil ti.len
|
169
173
|
assert ! @parser.body_eof?
|
@@ -201,7 +205,7 @@ class TestTeeInput < Test::Unit::TestCase
|
|
201
205
|
end
|
202
206
|
@wr.write("0\r\n\r\n")
|
203
207
|
}
|
204
|
-
ti =
|
208
|
+
ti = TeeInput.new(@rd, @parser)
|
205
209
|
assert_nil @parser.content_length
|
206
210
|
assert_nil ti.len
|
207
211
|
assert ! @parser.body_eof?
|
@@ -230,7 +234,7 @@ class TestTeeInput < Test::Unit::TestCase
|
|
230
234
|
@wr.write("Hello: World\r\n\r\n")
|
231
235
|
}
|
232
236
|
@wr.close
|
233
|
-
ti =
|
237
|
+
ti = TeeInput.new(@rd, @parser)
|
234
238
|
assert_nil @parser.content_length
|
235
239
|
assert_nil ti.len
|
236
240
|
assert ! @parser.body_eof?
|
@@ -241,6 +245,28 @@ class TestTeeInput < Test::Unit::TestCase
|
|
241
245
|
assert status.success?
|
242
246
|
end
|
243
247
|
|
248
|
+
def test_chunked_and_size_slow
|
249
|
+
@parser = Unicorn::HttpParser.new
|
250
|
+
@buf = "POST / HTTP/1.1\r\n" \
|
251
|
+
"Host: localhost\r\n" \
|
252
|
+
"Trailer: Hello\r\n" \
|
253
|
+
"Transfer-Encoding: chunked\r\n" \
|
254
|
+
"\r\n"
|
255
|
+
assert_equal @env, @parser.headers(@env, @buf)
|
256
|
+
assert_equal "", @buf
|
257
|
+
|
258
|
+
@wr.write("9\r\nabcde")
|
259
|
+
ti = TeeInput.new(@rd, @parser)
|
260
|
+
assert_nil @parser.content_length
|
261
|
+
assert_equal "abcde", ti.read(9)
|
262
|
+
assert ! @parser.body_eof?
|
263
|
+
@wr.write("fghi\r\n0\r\nHello: World\r\n\r\n")
|
264
|
+
assert_equal 9, ti.size
|
265
|
+
assert_equal "fghi", ti.read(9)
|
266
|
+
assert_equal nil, ti.read(9)
|
267
|
+
assert_equal "World", @env['HTTP_HELLO']
|
268
|
+
end
|
269
|
+
|
244
270
|
private
|
245
271
|
|
246
272
|
def init_request(body, size = nil)
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: unicorn
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
5
|
-
prerelease:
|
4
|
+
hash: -766259879
|
5
|
+
prerelease: true
|
6
6
|
segments:
|
7
|
-
-
|
7
|
+
- 3
|
8
8
|
- 0
|
9
|
-
-
|
10
|
-
version:
|
9
|
+
- 0pre1
|
10
|
+
version: 3.0.0pre1
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Unicorn hackers
|
@@ -105,6 +105,7 @@ extra_rdoc_files:
|
|
105
105
|
- lib/unicorn/oob_gc.rb
|
106
106
|
- lib/unicorn/preread_input.rb
|
107
107
|
- lib/unicorn/socket_helper.rb
|
108
|
+
- lib/unicorn/stream_input.rb
|
108
109
|
- lib/unicorn/tee_input.rb
|
109
110
|
- lib/unicorn/tmpio.rb
|
110
111
|
- lib/unicorn/util.rb
|
@@ -176,6 +177,7 @@ files:
|
|
176
177
|
- lib/unicorn/oob_gc.rb
|
177
178
|
- lib/unicorn/preread_input.rb
|
178
179
|
- lib/unicorn/socket_helper.rb
|
180
|
+
- lib/unicorn/stream_input.rb
|
179
181
|
- lib/unicorn/tee_input.rb
|
180
182
|
- lib/unicorn/tmpio.rb
|
181
183
|
- lib/unicorn/util.rb
|
@@ -230,7 +232,6 @@ files:
|
|
230
232
|
- t/t0000-http-basic.sh
|
231
233
|
- t/t0001-reload-bad-config.sh
|
232
234
|
- t/t0002-config-conflict.sh
|
233
|
-
- t/t0002-parser-error.sh
|
234
235
|
- t/t0003-working_directory.sh
|
235
236
|
- t/t0004-working_directory_broken.sh
|
236
237
|
- t/t0005-working_directory_app.rb.sh
|
@@ -242,6 +243,8 @@ files:
|
|
242
243
|
- t/t0010-reap-logging.sh
|
243
244
|
- t/t0011-active-unix-socket.sh
|
244
245
|
- t/t0012-reload-empty-config.sh
|
246
|
+
- t/t0013-rewindable-input-false.sh
|
247
|
+
- t/t0014-rewindable-input-true.sh
|
245
248
|
- t/t0300-rails3-basic.sh
|
246
249
|
- t/t0301-rails3-missing-config-ru.sh
|
247
250
|
- t/t0302-rails3-alt-working_directory.sh
|
@@ -341,6 +344,7 @@ files:
|
|
341
344
|
- test/unit/test_server.rb
|
342
345
|
- test/unit/test_signals.rb
|
343
346
|
- test/unit/test_socket_helper.rb
|
347
|
+
- test/unit/test_stream_input.rb
|
344
348
|
- test/unit/test_tee_input.rb
|
345
349
|
- test/unit/test_upload.rb
|
346
350
|
- test/unit/test_util.rb
|
@@ -368,12 +372,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
368
372
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
369
373
|
none: false
|
370
374
|
requirements:
|
371
|
-
- - "
|
375
|
+
- - ">"
|
372
376
|
- !ruby/object:Gem::Version
|
373
|
-
hash:
|
377
|
+
hash: 25
|
374
378
|
segments:
|
375
|
-
-
|
376
|
-
|
379
|
+
- 1
|
380
|
+
- 3
|
381
|
+
- 1
|
382
|
+
version: 1.3.1
|
377
383
|
requirements: []
|
378
384
|
|
379
385
|
rubyforge_project: mongrel
|
data/t/t0002-parser-error.sh
DELETED
@@ -1,31 +0,0 @@
|
|
1
|
-
#!/bin/sh
|
2
|
-
. ./test-lib.sh
|
3
|
-
t_plan 5 "parser error test"
|
4
|
-
|
5
|
-
t_begin "setup and startup" && {
|
6
|
-
unicorn_setup
|
7
|
-
unicorn -D env.ru -c $unicorn_config
|
8
|
-
unicorn_wait_start
|
9
|
-
}
|
10
|
-
|
11
|
-
t_begin "send a bad request" && {
|
12
|
-
(
|
13
|
-
printf 'GET / HTTP/1/1\r\nHost: example.com\r\n\r\n'
|
14
|
-
cat $fifo > $tmp &
|
15
|
-
wait
|
16
|
-
echo ok > $ok
|
17
|
-
) | socat - TCP:$listen > $fifo
|
18
|
-
test xok = x$(cat $ok)
|
19
|
-
}
|
20
|
-
|
21
|
-
dbgcat tmp
|
22
|
-
|
23
|
-
t_begin "response should be a 400" && {
|
24
|
-
grep -F 'HTTP/1.1 400 Bad Request' $tmp
|
25
|
-
}
|
26
|
-
|
27
|
-
t_begin "server stderr should be clean" && check_stderr
|
28
|
-
|
29
|
-
t_begin "term signal sent" && kill $unicorn_pid
|
30
|
-
|
31
|
-
t_done
|