unicorn 0.5.4 → 0.6.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.
@@ -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