unicorn 2.0.1 → 3.0.0pre1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|