unicorn 0.5.4 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,4 @@
1
- require 'unicorn/socket'
2
- require 'unicorn/const'
1
+ require 'socket'
3
2
  require 'logger'
4
3
 
5
4
  module Unicorn
@@ -42,6 +42,9 @@ module Unicorn
42
42
  503 => 'Service Unavailable',
43
43
  504 => 'Gateway Time-out',
44
44
  505 => 'HTTP Version not supported'
45
+ }.inject({}) { |hash,(code,msg)|
46
+ hash[code] = "#{code} #{msg}"
47
+ hash
45
48
  }
46
49
 
47
50
  # Frequently used constants when constructing requests or responses. Many times
@@ -56,9 +59,7 @@ module Unicorn
56
59
  REQUEST_URI='REQUEST_URI'.freeze
57
60
  REQUEST_PATH='REQUEST_PATH'.freeze
58
61
 
59
- UNICORN_VERSION="0.5.3".freeze
60
-
61
- UNICORN_TMP_BASE="unicorn".freeze
62
+ UNICORN_VERSION="0.6.0".freeze
62
63
 
63
64
  DEFAULT_HOST = "0.0.0.0".freeze # default TCP listen host address
64
65
  DEFAULT_PORT = "8080".freeze # default TCP listen port
@@ -82,7 +83,6 @@ module Unicorn
82
83
  CONTENT_LENGTH="CONTENT_LENGTH".freeze
83
84
  REMOTE_ADDR="REMOTE_ADDR".freeze
84
85
  HTTP_X_FORWARDED_FOR="HTTP_X_FORWARDED_FOR".freeze
85
- QUERY_STRING="QUERY_STRING".freeze
86
86
  RACK_INPUT="rack.input".freeze
87
87
  end
88
88
 
@@ -1,5 +1,4 @@
1
1
  require 'tempfile'
2
- require 'uri'
3
2
  require 'stringio'
4
3
 
5
4
  # compiled extension
@@ -19,27 +18,31 @@ module Unicorn
19
18
  "rack.multiprocess" => true,
20
19
  "rack.multithread" => false,
21
20
  "rack.run_once" => false,
22
- "rack.version" => [0, 1],
23
- "SCRIPT_NAME" => "",
21
+ "rack.version" => [0, 1].freeze,
22
+ "SCRIPT_NAME" => "".freeze,
24
23
 
25
24
  # this is not in the Rack spec, but some apps may rely on it
26
- "SERVER_SOFTWARE" => "Unicorn #{Const::UNICORN_VERSION}"
25
+ "SERVER_SOFTWARE" => "Unicorn #{Const::UNICORN_VERSION}".freeze
27
26
  }.freeze
28
27
 
28
+ LOCALHOST = '127.0.0.1'.freeze
29
+
30
+ # Being explicitly single-threaded, we have certain advantages in
31
+ # not having to worry about variables being clobbered :)
32
+ BUFFER = ' ' * Const::CHUNK_SIZE # initial size, may grow
33
+ PARSER = HttpParser.new
34
+ PARAMS = Hash.new
35
+
29
36
  def initialize(logger)
30
37
  @logger = logger
31
- @body = nil
32
- @buffer = ' ' * Const::CHUNK_SIZE # initial size, may grow
33
- @parser = HttpParser.new
34
- @params = Hash.new
38
+ reset
35
39
  end
36
40
 
37
41
  def reset
38
- @parser.reset
39
- @params.clear
40
- @body.close rescue nil
41
- @body.close! rescue nil
42
- @body = nil
42
+ PARAMS[Const::RACK_INPUT].close rescue nil
43
+ PARAMS[Const::RACK_INPUT].close! rescue nil
44
+ PARSER.reset
45
+ PARAMS.clear
43
46
  end
44
47
 
45
48
  # Does the majority of the IO processing. It has been written in
@@ -56,24 +59,34 @@ module Unicorn
56
59
  # This does minimal exception trapping and it is up to the caller
57
60
  # to handle any socket errors (e.g. user aborted upload).
58
61
  def read(socket)
62
+ # From http://www.ietf.org/rfc/rfc3875:
63
+ # "Script authors should be aware that the REMOTE_ADDR and
64
+ # REMOTE_HOST meta-variables (see sections 4.1.8 and 4.1.9)
65
+ # may not identify the ultimate source of the request. They
66
+ # identify the client for the immediate request to the server;
67
+ # that client may be a proxy, gateway, or other intermediary
68
+ # acting on behalf of the actual source client."
69
+ PARAMS[Const::REMOTE_ADDR] =
70
+ TCPSocket === socket ? socket.peeraddr.last : LOCALHOST
71
+
59
72
  # short circuit the common case with small GET requests first
60
- @parser.execute(@params, read_socket(socket)) and
73
+ PARSER.execute(PARAMS, read_socket(socket)) and
61
74
  return handle_body(socket)
62
75
 
63
- data = @buffer.dup # read_socket will clobber @buffer
76
+ data = BUFFER.dup # read_socket will clobber BUFFER
64
77
 
65
78
  # Parser is not done, queue up more data to read and continue parsing
66
- # an Exception thrown from the @parser will throw us out of the loop
67
- loop do
79
+ # an Exception thrown from the PARSER will throw us out of the loop
80
+ begin
68
81
  data << read_socket(socket)
69
- @parser.execute(@params, data) and return handle_body(socket)
70
- end
82
+ PARSER.execute(PARAMS, data) and return handle_body(socket)
83
+ end while true
71
84
  rescue HttpParserError => e
72
85
  @logger.error "HTTP parse error, malformed request " \
73
- "(#{@params[Const::HTTP_X_FORWARDED_FOR] ||
74
- socket.unicorn_peeraddr}): #{e.inspect}"
86
+ "(#{PARAMS[Const::HTTP_X_FORWARDED_FOR] ||
87
+ PARAMS[Const::REMOTE_ADDR]}): #{e.inspect}"
75
88
  @logger.error "REQUEST DATA: #{data.inspect}\n---\n" \
76
- "PARAMS: #{@params.inspect}\n---\n"
89
+ "PARAMS: #{PARAMS.inspect}\n---\n"
77
90
  raise e
78
91
  end
79
92
 
@@ -82,75 +95,54 @@ module Unicorn
82
95
  # Handles dealing with the rest of the request
83
96
  # returns a Rack environment if successful, raises an exception if not
84
97
  def handle_body(socket)
85
- http_body = @params.delete(:http_body)
86
- content_length = @params[Const::CONTENT_LENGTH].to_i
87
- remain = content_length - http_body.length
98
+ http_body = PARAMS.delete(:http_body)
99
+ content_length = PARAMS[Const::CONTENT_LENGTH].to_i
100
+
101
+ if content_length == 0 # short circuit the common case
102
+ PARAMS[Const::RACK_INPUT] = StringIO.new
103
+ return PARAMS.update(DEF_PARAMS)
104
+ end
88
105
 
89
106
  # must read more data to complete body
90
- @body = remain < Const::MAX_BODY ? StringIO.new : Tempfile.new('')
91
- @body.binmode
92
- @body.sync = true
93
- @body.syswrite(http_body)
107
+ remain = content_length - http_body.length
108
+
109
+ body = PARAMS[Const::RACK_INPUT] = (remain < Const::MAX_BODY) ?
110
+ StringIO.new : Tempfile.new('unicorn')
111
+
112
+ body.binmode
113
+ body.sync = true
114
+ body.syswrite(http_body)
94
115
 
95
116
  # Some clients (like FF1.0) report 0 for body and then send a body.
96
117
  # This will probably truncate them but at least the request goes through
97
118
  # usually.
98
- read_body(socket, remain) if remain > 0
99
- @body.rewind
100
- @body.sysseek(0) if @body.respond_to?(:sysseek)
119
+ read_body(socket, remain, body) if remain > 0
120
+ body.rewind
121
+ body.sysseek(0) if body.respond_to?(:sysseek)
101
122
 
102
123
  # in case read_body overread because the client tried to pipeline
103
124
  # another request, we'll truncate it. Again, we don't do pipelining
104
125
  # or keepalive
105
- @body.truncate(content_length)
106
- rack_env(socket)
107
- end
108
-
109
- # Returns an environment which is rackable:
110
- # http://rack.rubyforge.org/doc/files/SPEC.html
111
- # Based on Rack's old Mongrel handler.
112
- def rack_env(socket)
113
- # I'm considering enabling "unicorn.client". It gives
114
- # applications some rope to do some "interesting" things like
115
- # replacing a worker with another process that has full control
116
- # over the HTTP response.
117
- # @params["unicorn.client"] = socket
118
-
119
- # From http://www.ietf.org/rfc/rfc3875:
120
- # "Script authors should be aware that the REMOTE_ADDR and
121
- # REMOTE_HOST meta-variables (see sections 4.1.8 and 4.1.9)
122
- # may not identify the ultimate source of the request. They
123
- # identify the client for the immediate request to the server;
124
- # that client may be a proxy, gateway, or other intermediary
125
- # acting on behalf of the actual source client."
126
- @params[Const::REMOTE_ADDR] = socket.unicorn_peeraddr
127
-
128
- # It might be a dumbass full host request header
129
- @params[Const::PATH_INFO] = (
130
- @params[Const::REQUEST_PATH] ||=
131
- URI.parse(@params[Const::REQUEST_URI]).path) or
132
- raise "No REQUEST_PATH"
133
-
134
- @params[Const::QUERY_STRING] ||= ''
135
- @params[Const::RACK_INPUT] = @body
136
- @params.update(DEF_PARAMS)
126
+ body.truncate(content_length)
127
+ PARAMS.update(DEF_PARAMS)
137
128
  end
138
129
 
139
- # Does the heavy lifting of properly reading the larger body requests in
140
- # small chunks. It expects @body to be an IO object, socket to be valid,
141
- # It also expects any initial part of the body that has been read to be in
142
- # the @body already. It will return true if successful and false if not.
143
- def read_body(socket, remain)
130
+ # Does the heavy lifting of properly reading the larger body
131
+ # requests in small chunks. It expects PARAMS['rack.input'] to be
132
+ # an IO object, socket to be valid, It also expects any initial part
133
+ # of the body that has been read to be in the PARAMS['rack.input']
134
+ # already. It will return true if successful and false if not.
135
+ def read_body(socket, remain, body)
144
136
  while remain > 0
145
137
  # writes always write the requested amount on a POSIX filesystem
146
- remain -= @body.syswrite(read_socket(socket))
138
+ remain -= body.syswrite(read_socket(socket))
147
139
  end
148
140
  rescue Object => e
149
141
  @logger.error "Error reading HTTP body: #{e.inspect}"
150
142
 
151
143
  # Any errors means we should delete the file, including if the file
152
144
  # is dumped. Truncate it ASAP to help avoid page flushes to disk.
153
- @body.truncate(0) rescue nil
145
+ body.truncate(0) rescue nil
154
146
  reset
155
147
  raise e
156
148
  end
@@ -158,7 +150,7 @@ module Unicorn
158
150
  # read(2) on "slow" devices like sockets can be interrupted by signals
159
151
  def read_socket(socket)
160
152
  begin
161
- socket.sysread(Const::CHUNK_SIZE, @buffer)
153
+ socket.sysread(Const::CHUNK_SIZE, BUFFER)
162
154
  rescue Errno::EINTR
163
155
  retry
164
156
  end
@@ -25,22 +25,23 @@ module Unicorn
25
25
  # Connection: and Date: headers no matter what (if anything) our
26
26
  # Rack application sent us.
27
27
  SKIP = { 'connection' => true, 'date' => true, 'status' => true }.freeze
28
- HEADER_OUT = [ "Connection: close" ] # :nodoc
28
+ EMPTY = ''.freeze # :nodoc
29
+ OUT = [] # :nodoc
29
30
 
30
31
  # writes the rack_response to socket as an HTTP response
31
32
  def self.write(socket, rack_response)
32
33
  status, headers, body = rack_response
33
- status = "#{status} #{HTTP_STATUS_CODES[status]}"
34
- out = HEADER_OUT.dup # shallow copy
34
+ status = HTTP_STATUS_CODES[status]
35
+ OUT.clear
35
36
 
36
37
  # Don't bother enforcing duplicate supression, it's a Hash most of
37
38
  # the time anyways so just hope our app knows what it's doing
38
39
  headers.each do |key, value|
39
40
  next if SKIP.include?(key.downcase)
40
41
  if value =~ /\n/
41
- value.split(/\n/).each { |v| out << "#{key}: #{v}" }
42
+ value.split(/\n/).each { |v| OUT << "#{key}: #{v}\r\n" }
42
43
  else
43
- out << "#{key}: #{value}"
44
+ OUT << "#{key}: #{value}\r\n"
44
45
  end
45
46
  end
46
47
 
@@ -52,7 +53,8 @@ module Unicorn
52
53
  "HTTP/1.1 #{status}\r\n" \
53
54
  "Date: #{Time.now.httpdate}\r\n" \
54
55
  "Status: #{status}\r\n" \
55
- "#{out.join("\r\n")}\r\n\r\n")
56
+ "Connection: close\r\n" \
57
+ "#{OUT.join(EMPTY)}\r\n")
56
58
  body.each { |chunk| socket_write(socket, chunk) }
57
59
  socket.close # uncorks the socket immediately
58
60
  ensure
@@ -64,15 +66,12 @@ module Unicorn
64
66
  # write(2) can return short on slow devices like sockets as well
65
67
  # as fail with EINTR if a signal was caught.
66
68
  def self.socket_write(socket, buffer)
67
- loop do
68
- begin
69
- written = socket.syswrite(buffer)
70
- return written if written == buffer.length
71
- buffer = buffer[written..-1]
72
- rescue Errno::EINTR
73
- retry
74
- end
75
- end
69
+ begin
70
+ written = socket.syswrite(buffer)
71
+ return written if written == buffer.length
72
+ buffer = buffer[written..-1]
73
+ rescue Errno::EINTR
74
+ end while true
76
75
  end
77
76
 
78
77
  end
@@ -1,18 +1,5 @@
1
1
  require 'socket'
2
2
 
3
- class UNIXSocket
4
- UNICORN_PEERADDR = '127.0.0.1'.freeze
5
- def unicorn_peeraddr
6
- UNICORN_PEERADDR
7
- end
8
- end
9
-
10
- class TCPSocket
11
- def unicorn_peeraddr
12
- peeraddr.last
13
- end
14
- end
15
-
16
3
  module Unicorn
17
4
  module SocketHelper
18
5
  include Socket::Constants
@@ -98,6 +98,28 @@ end
98
98
  assert_shutdown(pid)
99
99
  end
100
100
 
101
+ def test_ttin_ttou
102
+ File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
103
+ pid = fork { redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port") } }
104
+ log = "test_stderr.#{pid}.log"
105
+ wait_master_ready(log)
106
+ [ 2, 3].each { |i|
107
+ assert_nothing_raised { Process.kill(:TTIN, pid) }
108
+ wait_workers_ready(log, i)
109
+ }
110
+ File.truncate(log, 0)
111
+ reaped = nil
112
+ [ 2, 1, 0].each { |i|
113
+ assert_nothing_raised { Process.kill(:TTOU, pid) }
114
+ DEFAULT_TRIES.times {
115
+ sleep DEFAULT_RES
116
+ reaped = File.readlines(log).grep(/reaped.*\s*worker=#{i}$/)
117
+ break if reaped.size == 1
118
+ }
119
+ assert_equal 1, reaped.size
120
+ }
121
+ end
122
+
101
123
  def test_help
102
124
  redirect_test_io do
103
125
  assert(system($unicorn_bin, "-h"), "help text returns true")
@@ -459,6 +481,7 @@ end
459
481
  results = nil
460
482
  sock = Tempfile.new('unicorn_test_sock')
461
483
  sock_path = sock.path
484
+ @sockets << sock_path
462
485
  sock.close!
463
486
  ucfg = Tempfile.new('unicorn_test_config')
464
487
  ucfg.syswrite("listen \"#{sock_path}\"\n")
@@ -22,7 +22,7 @@ class HttpParserTest < Test::Unit::TestCase
22
22
  assert_equal '/', req['REQUEST_URI']
23
23
  assert_equal 'GET', req['REQUEST_METHOD']
24
24
  assert_nil req['FRAGMENT']
25
- assert_nil req['QUERY_STRING']
25
+ assert_equal '', req['QUERY_STRING']
26
26
 
27
27
  parser.reset
28
28
  req.clear
@@ -40,7 +40,40 @@ class HttpParserTest < Test::Unit::TestCase
40
40
  assert_equal '/hello-world', req['REQUEST_URI']
41
41
  assert_equal 'GET', req['REQUEST_METHOD']
42
42
  assert_nil req['FRAGMENT']
43
- assert_nil req['QUERY_STRING']
43
+ assert_equal '', req['QUERY_STRING']
44
+ end
45
+
46
+ def test_parse_server_host_default_port
47
+ parser = HttpParser.new
48
+ req = {}
49
+ assert parser.execute(req, "GET / HTTP/1.1\r\nHost: foo\r\n\r\n")
50
+ assert_equal 'foo', req['SERVER_NAME']
51
+ assert_equal '80', req['SERVER_PORT']
52
+ end
53
+
54
+ def test_parse_server_host_alt_port
55
+ parser = HttpParser.new
56
+ req = {}
57
+ assert parser.execute(req, "GET / HTTP/1.1\r\nHost: foo:999\r\n\r\n")
58
+ assert_equal 'foo', req['SERVER_NAME']
59
+ assert_equal '999', req['SERVER_PORT']
60
+ end
61
+
62
+ def test_parse_server_host_empty_port
63
+ parser = HttpParser.new
64
+ req = {}
65
+ assert parser.execute(req, "GET / HTTP/1.1\r\nHost: foo:\r\n\r\n")
66
+ assert_equal 'foo', req['SERVER_NAME']
67
+ assert_equal '80', req['SERVER_PORT']
68
+ end
69
+
70
+ def test_parse_server_host_xfp_https
71
+ parser = HttpParser.new
72
+ req = {}
73
+ assert parser.execute(req, "GET / HTTP/1.1\r\nHost: foo:\r\n" \
74
+ "X-Forwarded-Proto: https\r\n\r\n")
75
+ assert_equal 'foo', req['SERVER_NAME']
76
+ assert_equal '443', req['SERVER_PORT']
44
77
  end
45
78
 
46
79
  def test_parse_strange_headers
@@ -100,7 +133,88 @@ class HttpParserTest < Test::Unit::TestCase
100
133
  assert parser.execute(req, http << "\n")
101
134
  assert_equal 'HTTP/1.1', req['SERVER_PROTOCOL']
102
135
  assert_nil req['FRAGMENT']
103
- assert_nil req['QUERY_STRING']
136
+ assert_equal '', req['QUERY_STRING']
137
+ end
138
+
139
+ # not common, but underscores do appear in practice
140
+ def test_absolute_uri_underscores
141
+ parser = HttpParser.new
142
+ req = {}
143
+ http = "GET http://under_score.example.com/foo?q=bar HTTP/1.0\r\n\r\n"
144
+ assert parser.execute(req, http)
145
+ assert_equal 'http', req['rack.url_scheme']
146
+ assert_equal '/foo?q=bar', req['REQUEST_URI']
147
+ assert_equal '/foo', req['REQUEST_PATH']
148
+ assert_equal 'q=bar', req['QUERY_STRING']
149
+
150
+ assert_equal 'under_score.example.com', req['HTTP_HOST']
151
+ assert_equal 'under_score.example.com', req['SERVER_NAME']
152
+ assert_equal '80', req['SERVER_PORT']
153
+ end
154
+
155
+ def test_absolute_uri
156
+ parser = HttpParser.new
157
+ req = {}
158
+ http = "GET http://example.com/foo?q=bar HTTP/1.0\r\n\r\n"
159
+ assert parser.execute(req, http)
160
+ assert_equal 'http', req['rack.url_scheme']
161
+ assert_equal '/foo?q=bar', req['REQUEST_URI']
162
+ assert_equal '/foo', req['REQUEST_PATH']
163
+ assert_equal 'q=bar', req['QUERY_STRING']
164
+
165
+ assert_equal 'example.com', req['HTTP_HOST']
166
+ assert_equal 'example.com', req['SERVER_NAME']
167
+ assert_equal '80', req['SERVER_PORT']
168
+ end
169
+
170
+ # X-Forwarded-Proto is not in rfc2616, absolute URIs are, however...
171
+ def test_absolute_uri_https
172
+ parser = HttpParser.new
173
+ req = {}
174
+ http = "GET https://example.com/foo?q=bar HTTP/1.1\r\n" \
175
+ "X-Forwarded-Proto: http\r\n\r\n"
176
+ assert parser.execute(req, http)
177
+ assert_equal 'https', req['rack.url_scheme']
178
+ assert_equal '/foo?q=bar', req['REQUEST_URI']
179
+ assert_equal '/foo', req['REQUEST_PATH']
180
+ assert_equal 'q=bar', req['QUERY_STRING']
181
+
182
+ assert_equal 'example.com', req['HTTP_HOST']
183
+ assert_equal 'example.com', req['SERVER_NAME']
184
+ assert_equal '443', req['SERVER_PORT']
185
+ end
186
+
187
+ # Host: header should be ignored for absolute URIs
188
+ def test_absolute_uri_with_port
189
+ parser = HttpParser.new
190
+ req = {}
191
+ http = "GET http://example.com:8080/foo?q=bar HTTP/1.2\r\n" \
192
+ "Host: bad.example.com\r\n\r\n"
193
+ assert parser.execute(req, http)
194
+ assert_equal 'http', req['rack.url_scheme']
195
+ assert_equal '/foo?q=bar', req['REQUEST_URI']
196
+ assert_equal '/foo', req['REQUEST_PATH']
197
+ assert_equal 'q=bar', req['QUERY_STRING']
198
+
199
+ assert_equal 'example.com:8080', req['HTTP_HOST']
200
+ assert_equal 'example.com', req['SERVER_NAME']
201
+ assert_equal '8080', req['SERVER_PORT']
202
+ end
203
+
204
+ def test_absolute_uri_with_empty_port
205
+ parser = HttpParser.new
206
+ req = {}
207
+ http = "GET https://example.com:/foo?q=bar HTTP/1.1\r\n" \
208
+ "Host: bad.example.com\r\n\r\n"
209
+ assert parser.execute(req, http)
210
+ assert_equal 'https', req['rack.url_scheme']
211
+ assert_equal '/foo?q=bar', req['REQUEST_URI']
212
+ assert_equal '/foo', req['REQUEST_PATH']
213
+ assert_equal 'q=bar', req['QUERY_STRING']
214
+
215
+ assert_equal 'example.com:', req['HTTP_HOST']
216
+ assert_equal 'example.com', req['SERVER_NAME']
217
+ assert_equal '443', req['SERVER_PORT']
104
218
  end
105
219
 
106
220
  def test_put_body_oneshot