unicorn 0.8.4 → 0.9.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.
- data/.document +1 -0
- data/CHANGELOG +1 -3
- data/COPYING +339 -0
- data/GNUmakefile +12 -8
- data/LICENSE +3 -3
- data/Manifest +9 -0
- data/README +20 -8
- data/TODO +5 -13
- data/examples/echo.ru +32 -0
- data/examples/git.ru +13 -0
- data/ext/unicorn/http11/http11.c +9 -2
- data/lib/unicorn.rb +35 -17
- data/lib/unicorn/app/exec_cgi.rb +10 -7
- data/lib/unicorn/app/inetd.rb +108 -0
- data/lib/unicorn/chunked_reader.rb +94 -0
- data/lib/unicorn/configurator.rb +1 -1
- data/lib/unicorn/const.rb +5 -1
- data/lib/unicorn/http_request.rb +16 -60
- data/lib/unicorn/http_response.rb +2 -3
- data/lib/unicorn/tee_input.rb +135 -0
- data/lib/unicorn/trailer_parser.rb +52 -0
- data/lib/unicorn/util.rb +0 -17
- data/local.mk.sample +3 -3
- data/test/rails/test_rails.rb +18 -12
- data/test/test_helper.rb +26 -0
- data/test/unit/test_chunked_reader.rb +180 -0
- data/test/unit/test_configurator.rb +1 -1
- data/test/unit/test_http_parser.rb +30 -0
- data/test/unit/test_request.rb +6 -1
- data/test/unit/test_server.rb +12 -1
- data/test/unit/test_signals.rb +2 -0
- data/test/unit/test_trailer_parser.rb +52 -0
- data/test/unit/test_upload.rb +130 -104
- data/test/unit/test_util.rb +28 -30
- data/unicorn.gemspec +7 -6
- metadata +19 -3
data/README
CHANGED
@@ -131,16 +131,28 @@ regarding this.
|
|
131
131
|
|
132
132
|
== Known Issues
|
133
133
|
|
134
|
-
* WONTFIX: code reloading with Sinatra 0.3.
|
134
|
+
* WONTFIX: code reloading and restarts with Sinatra 0.3.x (and likely older
|
135
135
|
versions) apps is broken. The workaround is to force production
|
136
|
-
mode to disable code reloading in your
|
136
|
+
mode to disable code reloading as well as disabling "run" in your
|
137
|
+
Sinatra application:
|
137
138
|
set :env, :production
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
139
|
+
set :run, false
|
140
|
+
Since this is no longer an issue with Sinatra 0.9.x apps, this will not be
|
141
|
+
fixed on our end. Since Unicorn is itself the application launcher, the
|
142
|
+
at_exit handler used in old Sinatra always caused Mongrel to be launched
|
143
|
+
whenever a Unicorn worker was about to exit.
|
144
|
+
|
145
|
+
Also remember we're capable of replacing the running binary without dropping
|
146
|
+
any connections regardless of framework :)
|
142
147
|
|
143
148
|
== Contact
|
144
149
|
|
145
|
-
|
146
|
-
|
150
|
+
All feedback (bug reports, user/development dicussion, patches, pull
|
151
|
+
requests) go to the mailing list. Patches must be sent inline
|
152
|
+
(git format-patch -M + git send-email). No subscription is necessary
|
153
|
+
to post on the mailing list. No top posting. Address replies +To:+ (or
|
154
|
+
+Cc:+) the original sender and +Cc:+ the mailing list.
|
155
|
+
|
156
|
+
* email: mongrel-unicorn@rubyforge.org
|
157
|
+
* archives: http://rubyforge.org/pipermail/mongrel-unicorn/
|
158
|
+
* subscribe: http://rubyforge.org/mailman/listinfo/mongrel-unicorn/
|
data/TODO
CHANGED
@@ -1,17 +1,9 @@
|
|
1
|
-
|
1
|
+
* integration tests with nginx including bad client handling
|
2
2
|
|
3
|
-
|
3
|
+
* manpages (why do so few Ruby executables come with proper manpages?)
|
4
4
|
|
5
|
-
|
5
|
+
* code cleanups (launchers)
|
6
6
|
|
7
|
-
|
7
|
+
* Pure Ruby HTTP parser
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
curl -T- http://host:port/path < file_from_stdin
|
12
|
-
|
13
|
-
* code cleanups (launchers)
|
14
|
-
|
15
|
-
* Pure Ruby HTTP parser
|
16
|
-
|
17
|
-
* Rubinius support?
|
9
|
+
* Rubinius support
|
data/examples/echo.ru
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
#\-E none
|
2
|
+
#
|
3
|
+
# Example application that echoes read data back to the HTTP client.
|
4
|
+
# This emulates the old echo protocol people used to run.
|
5
|
+
#
|
6
|
+
# An example of using this in a client would be to run:
|
7
|
+
# curl --no-buffer -T- http://host:port/
|
8
|
+
#
|
9
|
+
# Then type random stuff in your terminal to watch it get echoed back!
|
10
|
+
|
11
|
+
class EchoBody
|
12
|
+
def initialize(input)
|
13
|
+
@input = input
|
14
|
+
end
|
15
|
+
|
16
|
+
def each(&block)
|
17
|
+
while buf = @input.read(4096)
|
18
|
+
yield buf
|
19
|
+
end
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
def close
|
24
|
+
@input = nil
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
use Rack::Chunked
|
29
|
+
run lambda { |env|
|
30
|
+
[ 200, { 'Content-Type' => 'application/octet-stream' },
|
31
|
+
EchoBody.new(env['rack.input']) ]
|
32
|
+
}
|
data/examples/git.ru
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
#\-E none
|
2
|
+
|
3
|
+
# See http://thread.gmane.org/gmane.comp.web.curl.general/10473/raw on
|
4
|
+
# how to setup git for this. A better version of the above patch was
|
5
|
+
# accepted and committed on June 15, 2009, so you can pull the latest
|
6
|
+
# curl CVS snapshot to try this out.
|
7
|
+
require 'unicorn/app/inetd'
|
8
|
+
|
9
|
+
use Rack::Lint
|
10
|
+
use Rack::Chunked # important!
|
11
|
+
run Unicorn::App::Inetd.new(
|
12
|
+
*%w(git daemon --verbose --inetd --export-all --base-path=/home/ew/unicorn)
|
13
|
+
)
|
data/ext/unicorn/http11/http11.c
CHANGED
@@ -324,10 +324,17 @@ static void header_done(void *data, const char *at, size_t length)
|
|
324
324
|
}
|
325
325
|
rb_hash_aset(req, global_server_name, server_name);
|
326
326
|
rb_hash_aset(req, global_server_port, server_port);
|
327
|
+
rb_hash_aset(req, global_server_protocol, global_server_protocol_value);
|
327
328
|
|
328
329
|
/* grab the initial body and stuff it into the hash */
|
329
|
-
|
330
|
-
|
330
|
+
temp = rb_hash_aref(req, global_request_method);
|
331
|
+
if (temp != Qnil) {
|
332
|
+
long len = RSTRING_LEN(temp);
|
333
|
+
char *ptr = RSTRING_PTR(temp);
|
334
|
+
|
335
|
+
if (memcmp(ptr, "HEAD", len) && memcmp(ptr, "GET", len))
|
336
|
+
rb_hash_aset(req, sym_http_body, rb_str_new(at, length));
|
337
|
+
}
|
331
338
|
}
|
332
339
|
|
333
340
|
static void HttpParser_free(void *data) {
|
data/lib/unicorn.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'fcntl'
|
2
|
+
require 'tempfile'
|
2
3
|
require 'unicorn/socket_helper'
|
3
4
|
autoload :Rack, 'rack'
|
4
5
|
|
@@ -10,8 +11,15 @@ module Unicorn
|
|
10
11
|
autoload :HttpRequest, 'unicorn/http_request'
|
11
12
|
autoload :HttpResponse, 'unicorn/http_response'
|
12
13
|
autoload :Configurator, 'unicorn/configurator'
|
14
|
+
autoload :TeeInput, 'unicorn/tee_input'
|
15
|
+
autoload :ChunkedReader, 'unicorn/chunked_reader'
|
16
|
+
autoload :TrailerParser, 'unicorn/trailer_parser'
|
13
17
|
autoload :Util, 'unicorn/util'
|
14
18
|
|
19
|
+
Z = '' # the stock empty string we use everywhere...
|
20
|
+
Z.force_encoding(Encoding::BINARY) if Z.respond_to?(:force_encoding)
|
21
|
+
Z.freeze
|
22
|
+
|
15
23
|
class << self
|
16
24
|
def run(app, options = {})
|
17
25
|
HttpServer.new(app, options).start.join
|
@@ -51,10 +59,11 @@ module Unicorn
|
|
51
59
|
# don't rely on Dir.pwd here since it's not symlink-aware, and
|
52
60
|
# symlink dirs are the default with Capistrano...
|
53
61
|
:cwd => `/bin/sh -c pwd`.chomp("\n"),
|
54
|
-
|
62
|
+
:zero => $0.dup,
|
55
63
|
}
|
56
64
|
|
57
|
-
|
65
|
+
Worker = Struct.new(:nr, :tempfile) unless defined?(Worker)
|
66
|
+
class Worker
|
58
67
|
# worker objects may be compared to just plain numbers
|
59
68
|
def ==(other_nr)
|
60
69
|
self.nr == other_nr
|
@@ -322,7 +331,7 @@ module Unicorn
|
|
322
331
|
self.pid = @pid.chomp('.oldbin') if @pid
|
323
332
|
proc_name 'master'
|
324
333
|
else
|
325
|
-
worker = WORKERS.delete(pid) and worker.
|
334
|
+
worker = WORKERS.delete(pid) and worker.tempfile.close rescue nil
|
326
335
|
logger.info "reaped #{status.inspect} " \
|
327
336
|
"worker=#{worker.nr rescue 'unknown'}"
|
328
337
|
end
|
@@ -362,7 +371,7 @@ module Unicorn
|
|
362
371
|
listener_fds = LISTENERS.map { |sock| sock.fileno }
|
363
372
|
ENV['UNICORN_FD'] = listener_fds.join(',')
|
364
373
|
Dir.chdir(START_CTX[:cwd])
|
365
|
-
cmd = [ START_CTX[
|
374
|
+
cmd = [ START_CTX[:zero] ] + START_CTX[:argv]
|
366
375
|
|
367
376
|
# avoid leaking FDs we don't know about, but let before_exec
|
368
377
|
# unset FD_CLOEXEC, if anything else in the app eventually
|
@@ -382,16 +391,16 @@ module Unicorn
|
|
382
391
|
end
|
383
392
|
|
384
393
|
# forcibly terminate all workers that haven't checked in in @timeout
|
385
|
-
# seconds. The timeout is implemented using an unlinked
|
394
|
+
# seconds. The timeout is implemented using an unlinked tempfile
|
386
395
|
# shared between the parent process and each worker. The worker
|
387
|
-
# runs File#chmod to modify the ctime of the
|
396
|
+
# runs File#chmod to modify the ctime of the tempfile. If the ctime
|
388
397
|
# is stale for >@timeout seconds, then we'll kill the corresponding
|
389
398
|
# worker.
|
390
399
|
def murder_lazy_workers
|
391
400
|
diff = stat = nil
|
392
401
|
WORKERS.dup.each_pair do |pid, worker|
|
393
402
|
stat = begin
|
394
|
-
worker.
|
403
|
+
worker.tempfile.stat
|
395
404
|
rescue => e
|
396
405
|
logger.warn "worker=#{worker.nr} PID:#{pid} stat error: #{e.inspect}"
|
397
406
|
kill_worker(:QUIT, pid)
|
@@ -415,7 +424,9 @@ module Unicorn
|
|
415
424
|
SIG_QUEUE << :QUIT # forcibly emulate SIGQUIT
|
416
425
|
return
|
417
426
|
end
|
418
|
-
|
427
|
+
tempfile = Tempfile.new(nil) # as short as possible to save dir space
|
428
|
+
tempfile.unlink # don't allow other processes to find or see it
|
429
|
+
worker = Worker.new(worker_nr, tempfile)
|
419
430
|
@before_fork.call(self, worker)
|
420
431
|
pid = fork { worker_loop(worker) }
|
421
432
|
WORKERS[pid] = worker
|
@@ -433,8 +444,15 @@ module Unicorn
|
|
433
444
|
# once a client is accepted, it is processed in its entirety here
|
434
445
|
# in 3 easy steps: read request, call app, write app response
|
435
446
|
def process_client(app, client)
|
436
|
-
|
437
|
-
|
447
|
+
response = app.call(env = REQUEST.read(client))
|
448
|
+
|
449
|
+
if 100 == response.first.to_i
|
450
|
+
client.write(Const::EXPECT_100_RESPONSE)
|
451
|
+
env.delete(Const::HTTP_EXPECT)
|
452
|
+
response = app.call(env)
|
453
|
+
end
|
454
|
+
|
455
|
+
HttpResponse.write(client, response)
|
438
456
|
# if we get any error, try to write something back to the client
|
439
457
|
# assuming we haven't closed the socket, but don't get hung up
|
440
458
|
# if the socket is already closed or broken. We'll always ensure
|
@@ -457,16 +475,16 @@ module Unicorn
|
|
457
475
|
# traps for USR1, USR2, and HUP may be set in the @after_fork Proc
|
458
476
|
# by the user.
|
459
477
|
def init_worker_process(worker)
|
460
|
-
QUEUE_SIGS.each { |sig| trap(sig,
|
478
|
+
QUEUE_SIGS.each { |sig| trap(sig, 'IGNORE') }
|
461
479
|
trap(:CHLD, 'DEFAULT')
|
462
480
|
SIG_QUEUE.clear
|
463
481
|
proc_name "worker[#{worker.nr}]"
|
464
482
|
START_CTX.clear
|
465
483
|
init_self_pipe!
|
466
|
-
WORKERS.values.each { |other| other.
|
484
|
+
WORKERS.values.each { |other| other.tempfile.close! rescue nil }
|
467
485
|
WORKERS.clear
|
468
486
|
LISTENERS.each { |sock| sock.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) }
|
469
|
-
worker.
|
487
|
+
worker.tempfile.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
|
470
488
|
@after_fork.call(self, worker) # can drop perms
|
471
489
|
@timeout /= 2.0 # halve it for select()
|
472
490
|
build_app! unless @preload_app
|
@@ -486,7 +504,7 @@ module Unicorn
|
|
486
504
|
master_pid = Process.ppid # slightly racy, but less memory usage
|
487
505
|
init_worker_process(worker)
|
488
506
|
nr = 0 # this becomes negative if we need to reopen logs
|
489
|
-
alive = worker.
|
507
|
+
alive = worker.tempfile # tempfile is our lifeline to the master process
|
490
508
|
ready = LISTENERS
|
491
509
|
t = ti = 0
|
492
510
|
|
@@ -552,7 +570,7 @@ module Unicorn
|
|
552
570
|
begin
|
553
571
|
Process.kill(signal, pid)
|
554
572
|
rescue Errno::ESRCH
|
555
|
-
worker = WORKERS.delete(pid) and worker.
|
573
|
+
worker = WORKERS.delete(pid) and worker.tempfile.close rescue nil
|
556
574
|
end
|
557
575
|
end
|
558
576
|
|
@@ -613,8 +631,8 @@ module Unicorn
|
|
613
631
|
end
|
614
632
|
|
615
633
|
def proc_name(tag)
|
616
|
-
$0 = ([ File.basename(START_CTX[
|
617
|
-
|
634
|
+
$0 = ([ File.basename(START_CTX[:zero]), tag ] +
|
635
|
+
START_CTX[:argv]).join(' ')
|
618
636
|
end
|
619
637
|
|
620
638
|
def redirect_io(io, path)
|
data/lib/unicorn/app/exec_cgi.rb
CHANGED
@@ -42,8 +42,11 @@ module Unicorn::App
|
|
42
42
|
|
43
43
|
# Calls the app
|
44
44
|
def call(env)
|
45
|
-
out, err =
|
45
|
+
out, err = Tempfile.new(nil), Tempfile.new(nil)
|
46
|
+
out.unlink
|
47
|
+
err.unlink
|
46
48
|
inp = force_file_input(env)
|
49
|
+
out.sync = err.sync = true
|
47
50
|
pid = fork { run_child(inp, out, err, env) }
|
48
51
|
inp.close
|
49
52
|
pid, status = Process.waitpid2(pid)
|
@@ -118,15 +121,15 @@ module Unicorn::App
|
|
118
121
|
# ensures rack.input is a file handle that we can redirect stdin to
|
119
122
|
def force_file_input(env)
|
120
123
|
inp = env['rack.input']
|
121
|
-
if inp.
|
122
|
-
inp
|
123
|
-
elsif inp.size == 0 # inp could be a StringIO or StringIO-like object
|
124
|
+
if inp.size == 0 # inp could be a StringIO or StringIO-like object
|
124
125
|
::File.open('/dev/null', 'rb')
|
125
126
|
else
|
126
|
-
tmp =
|
127
|
+
tmp = Tempfile.new(nil)
|
128
|
+
tmp.unlink
|
129
|
+
tmp.binmode
|
130
|
+
tmp.sync = true
|
127
131
|
|
128
|
-
|
129
|
-
buf = Unicorn::Z.dup
|
132
|
+
buf = Z.dup
|
130
133
|
while inp.read(CHUNK_SIZE, buf)
|
131
134
|
tmp.syswrite(buf)
|
132
135
|
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# Copyright (c) 2009 Eric Wong
|
2
|
+
# You can redistribute it and/or modify it under the same terms as Ruby.
|
3
|
+
|
4
|
+
# this class *must* be used with Rack::Chunked
|
5
|
+
|
6
|
+
module Unicorn::App
|
7
|
+
class Inetd
|
8
|
+
|
9
|
+
class CatBody
|
10
|
+
def initialize(env, cmd)
|
11
|
+
@cmd = cmd
|
12
|
+
@input, @errors = env['rack.input'], env['rack.errors']
|
13
|
+
in_rd, in_wr = IO.pipe
|
14
|
+
@err_rd, err_wr = IO.pipe
|
15
|
+
@out_rd, out_wr = IO.pipe
|
16
|
+
|
17
|
+
@cmd_pid = fork {
|
18
|
+
inp, out, err = (0..2).map { |i| IO.new(i) }
|
19
|
+
inp.reopen(in_rd)
|
20
|
+
out.reopen(out_wr)
|
21
|
+
err.reopen(err_wr)
|
22
|
+
[ in_rd, in_wr, @err_rd, err_wr, @out_rd, out_wr ].each { |io|
|
23
|
+
io.close
|
24
|
+
}
|
25
|
+
exec(*cmd)
|
26
|
+
}
|
27
|
+
[ in_rd, err_wr, out_wr ].each { |io| io.close }
|
28
|
+
[ in_wr, @err_rd, @out_rd ].each { |io| io.binmode }
|
29
|
+
in_wr.sync = true
|
30
|
+
|
31
|
+
# Unfortunately, input here must be processed inside a seperate
|
32
|
+
# thread/process using blocking I/O since env['rack.input'] is not
|
33
|
+
# IO.select-able and attempting to make it so would trip Rack::Lint
|
34
|
+
@inp_pid = fork {
|
35
|
+
[ @err_rd, @out_rd ].each { |io| io.close }
|
36
|
+
buf = Unicorn::Z.dup
|
37
|
+
|
38
|
+
# this is dependent on @input.read having readpartial semantics:
|
39
|
+
while @input.read(16384, buf)
|
40
|
+
in_wr.write(buf)
|
41
|
+
end
|
42
|
+
in_wr.close
|
43
|
+
}
|
44
|
+
in_wr.close
|
45
|
+
end
|
46
|
+
|
47
|
+
def each(&block)
|
48
|
+
buf = Unicorn::Z.dup
|
49
|
+
begin
|
50
|
+
rd, = IO.select([@err_rd, @out_rd])
|
51
|
+
rd && rd.first or next
|
52
|
+
|
53
|
+
if rd.include?(@err_rd)
|
54
|
+
begin
|
55
|
+
@errors.write(@err_rd.read_nonblock(16384, buf))
|
56
|
+
rescue Errno::EINTR
|
57
|
+
rescue Errno::EAGAIN
|
58
|
+
break
|
59
|
+
end while true
|
60
|
+
end
|
61
|
+
|
62
|
+
rd.include?(@out_rd) or next
|
63
|
+
|
64
|
+
begin
|
65
|
+
yield @out_rd.read_nonblock(16384, buf)
|
66
|
+
rescue Errno::EINTR
|
67
|
+
rescue Errno::EAGAIN
|
68
|
+
break
|
69
|
+
end while true
|
70
|
+
rescue EOFError,Errno::EPIPE,Errno::EBADF,Errno::EINVAL
|
71
|
+
break
|
72
|
+
end while true
|
73
|
+
|
74
|
+
self
|
75
|
+
end
|
76
|
+
|
77
|
+
def close
|
78
|
+
@input = nil
|
79
|
+
[ [ @cmd.inspect, @cmd_pid ], [ 'input streamer', @inp_pid ]
|
80
|
+
].each { |str, pid|
|
81
|
+
begin
|
82
|
+
pid, status = Process.waitpid2(pid)
|
83
|
+
status.success? or
|
84
|
+
@errors.write("#{str}: #{status.inspect} (PID:#{pid})\n")
|
85
|
+
rescue Errno::ECHILD
|
86
|
+
@errors.write("Failed to reap #{str} (PID:#{pid})\n")
|
87
|
+
end
|
88
|
+
}
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
def initialize(*cmd)
|
94
|
+
@cmd = cmd
|
95
|
+
end
|
96
|
+
|
97
|
+
def call(env)
|
98
|
+
expect = env[Unicorn::Const::HTTP_EXPECT] and
|
99
|
+
/\A100-continue\z/i =~ expect and
|
100
|
+
return [ 100, {} , [] ]
|
101
|
+
|
102
|
+
[ 200, { 'Content-Type' => 'application/octet-stream' },
|
103
|
+
CatBody.new(env, @cmd) ]
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# Copyright (c) 2009 Eric Wong
|
2
|
+
# You can redistribute it and/or modify it under the same terms as Ruby.
|
3
|
+
|
4
|
+
require 'unicorn'
|
5
|
+
require 'unicorn/http11'
|
6
|
+
|
7
|
+
module Unicorn
|
8
|
+
class ChunkedReader
|
9
|
+
|
10
|
+
def initialize(env, input, buf)
|
11
|
+
@env, @input, @buf = env, input, buf
|
12
|
+
@chunk_left = 0
|
13
|
+
parse_chunk_header
|
14
|
+
end
|
15
|
+
|
16
|
+
def readpartial(max, buf = Z.dup)
|
17
|
+
while @input && @chunk_left <= 0 && ! parse_chunk_header
|
18
|
+
@buf << @input.readpartial(Const::CHUNK_SIZE, buf)
|
19
|
+
end
|
20
|
+
|
21
|
+
if @input
|
22
|
+
begin
|
23
|
+
@buf << @input.read_nonblock(Const::CHUNK_SIZE, buf)
|
24
|
+
rescue Errno::EAGAIN, Errno::EINTR
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
max = @chunk_left if max > @chunk_left
|
29
|
+
buf.replace(last_block(max) || Z)
|
30
|
+
@chunk_left -= buf.size
|
31
|
+
(0 == buf.size && @input.nil?) and raise EOFError
|
32
|
+
buf
|
33
|
+
end
|
34
|
+
|
35
|
+
def gets
|
36
|
+
line = nil
|
37
|
+
begin
|
38
|
+
line = readpartial(Const::CHUNK_SIZE)
|
39
|
+
begin
|
40
|
+
if line.sub!(%r{\A(.*?#{$/})}, Z)
|
41
|
+
@chunk_left += line.size
|
42
|
+
@buf = @buf ? (line << @buf) : line
|
43
|
+
return $1.dup
|
44
|
+
end
|
45
|
+
line << readpartial(Const::CHUNK_SIZE)
|
46
|
+
end while true
|
47
|
+
rescue EOFError
|
48
|
+
return line
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def last_block(max = nil)
|
55
|
+
rv = @buf
|
56
|
+
if max && rv && max < rv.size
|
57
|
+
@buf = rv[max - rv.size, rv.size - max]
|
58
|
+
return rv[0, max]
|
59
|
+
end
|
60
|
+
@buf = Z.dup
|
61
|
+
rv
|
62
|
+
end
|
63
|
+
|
64
|
+
def parse_chunk_header
|
65
|
+
buf = @buf
|
66
|
+
# ignoring chunk-extension info for now, I haven't seen any use for it
|
67
|
+
# (or any users, and TE:chunked sent by clients is rare already)
|
68
|
+
# if there was not enough data in buffer to parse length of the chunk
|
69
|
+
# then just return
|
70
|
+
if buf.sub!(/\A(?:\r\n)?([a-fA-F0-9]{1,8})[^\r]*?\r\n/, Z)
|
71
|
+
@chunk_left = $1.to_i(16)
|
72
|
+
if 0 == @chunk_left # EOF
|
73
|
+
buf.sub!(/\A\r\n(?:\r\n)?/, Z) # cleanup for future requests
|
74
|
+
if trailer = @env[Const::HTTP_TRAILER]
|
75
|
+
tp = TrailerParser.new(trailer)
|
76
|
+
while ! tp.execute!(@env, buf)
|
77
|
+
buf << @input.readpartial(Const::CHUNK_SIZE)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
@input = nil
|
81
|
+
end
|
82
|
+
return @chunk_left
|
83
|
+
end
|
84
|
+
|
85
|
+
buf.size > 256 and
|
86
|
+
raise HttpParserError,
|
87
|
+
"malformed chunk, chunk-length not found in buffer: " \
|
88
|
+
"#{buf.inspect}"
|
89
|
+
nil
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|