passenger 4.0.36 → 4.0.37

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of passenger might be problematic. Click here for more details.

Files changed (41) hide show
  1. checksums.yaml +8 -8
  2. checksums.yaml.gz.asc +7 -7
  3. data.tar.gz.asc +7 -7
  4. data/NEWS +51 -0
  5. data/README.md +3 -1
  6. data/build/integration_tests.rb +5 -1
  7. data/build/test_basics.rb +2 -2
  8. data/dev/run_travis.sh +3 -0
  9. data/doc/Users guide Nginx.txt +3 -3
  10. data/ext/common/ApplicationPool2/Group.h +2 -1
  11. data/ext/common/ApplicationPool2/Implementation.cpp +30 -1
  12. data/ext/common/ApplicationPool2/Pool.h +26 -3
  13. data/ext/common/ApplicationPool2/Process.h +39 -10
  14. data/ext/common/Constants.h +1 -1
  15. data/ext/common/MultiLibeio.cpp +4 -0
  16. data/ext/common/ServerInstanceDir.h +1 -1
  17. data/ext/common/Utils.cpp +29 -0
  18. data/ext/common/Utils.h +7 -1
  19. data/ext/common/Utils/BufferedIO.h +13 -0
  20. data/ext/common/agents/HelperAgent/Main.cpp +6 -2
  21. data/ext/common/agents/HelperAgent/RequestHandler.h +32 -1
  22. data/helper-scripts/meteor-loader.rb +126 -10
  23. data/helper-scripts/node-loader.js +5 -3
  24. data/helper-scripts/wsgi-loader.py +23 -11
  25. data/lib/phusion_passenger.rb +1 -1
  26. data/lib/phusion_passenger/config/detach_process_command.rb +96 -0
  27. data/lib/phusion_passenger/config/main.rb +1 -0
  28. data/lib/phusion_passenger/request_handler.rb +11 -4
  29. data/node_lib/phusion_passenger/httplib_emulation.js +20 -14
  30. data/test/cxx/RequestHandlerTest.cpp +80 -0
  31. data/test/integration_tests/apache2_tests.rb +57 -0
  32. data/test/integration_tests/nginx_tests.rb +62 -0
  33. data/test/node/httplib_emulation_spec.js +137 -5
  34. data/test/node/spec_helper.js +13 -0
  35. data/test/stub/node/app.js +125 -0
  36. data/test/stub/node/public/.gitignore +0 -0
  37. data/test/stub/node/tmp/.gitignore +0 -0
  38. data/test/stub/rack/config.ru +19 -0
  39. data/test/stub/wsgi/passenger_wsgi.py +37 -1
  40. metadata +6 -2
  41. metadata.gz.asc +7 -7
@@ -55,6 +55,10 @@ struct Data {
55
55
  : libev(_libev),
56
56
  callback(_callback)
57
57
  {
58
+ // If this assertion fails, then in the context of RequestHandler it means
59
+ // that it was operating on a client that has already been disconnected.
60
+ // The RequestHandler code is probably missing some necessary checks on
61
+ // `client->connected()`.
58
62
  assert(_libev != NULL);
59
63
  }
60
64
  };
@@ -213,7 +213,7 @@ private:
213
213
  * generations no matter what user they're running as.
214
214
  */
215
215
  if (owner) {
216
- switch (getFileType(path)) {
216
+ switch (getFileTypeNoFollowSymlinks(path)) {
217
217
  case FT_NONEXISTANT:
218
218
  createDirectory(path);
219
219
  break;
@@ -143,6 +143,35 @@ getFileType(const StaticString &filename, CachedFileStat *cstat, unsigned int th
143
143
  }
144
144
  }
145
145
 
146
+ FileType
147
+ getFileTypeNoFollowSymlinks(const StaticString &filename) {
148
+ struct stat buf;
149
+ int ret;
150
+
151
+ ret = lstat(filename.c_str(), &buf);
152
+ if (ret == 0) {
153
+ if (S_ISREG(buf.st_mode)) {
154
+ return FT_REGULAR;
155
+ } else if (S_ISDIR(buf.st_mode)) {
156
+ return FT_DIRECTORY;
157
+ } else if (S_ISLNK(buf.st_mode)) {
158
+ return FT_SYMLINK;
159
+ } else {
160
+ return FT_OTHER;
161
+ }
162
+ } else {
163
+ if (errno == ENOENT) {
164
+ return FT_NONEXISTANT;
165
+ } else {
166
+ int e = errno;
167
+ string message("Cannot lstat '");
168
+ message.append(filename);
169
+ message.append("'");
170
+ throw FileSystemException(message, e, filename);
171
+ }
172
+ }
173
+ }
174
+
146
175
  void
147
176
  createFile(const string &filename, const StaticString &contents, mode_t permissions, uid_t owner,
148
177
  gid_t group, bool overwrite)
@@ -65,6 +65,8 @@ typedef enum {
65
65
  FT_REGULAR,
66
66
  /** A directory. */
67
67
  FT_DIRECTORY,
68
+ /** A symlink. Only returned by getFileTypeNoFollowSymlinks(), not by getFileType(). */
69
+ FT_SYMLINK,
68
70
  /** Something else, e.g. a pipe or a socket. */
69
71
  FT_OTHER
70
72
  } FileType;
@@ -110,7 +112,7 @@ bool fileExists(const StaticString &filename, CachedFileStat *cstat = 0,
110
112
  /**
111
113
  * Check whether 'filename' exists and what kind of file it is.
112
114
  *
113
- * @param filename The filename to check.
115
+ * @param filename The filename to check. It MUST be NULL-terminated.
114
116
  * @param mstat A CachedFileStat object, if you want to use cached statting.
115
117
  * @param throttleRate A throttle rate for cstat. Only applicable if cstat is not NULL.
116
118
  * @return The file type.
@@ -121,6 +123,10 @@ bool fileExists(const StaticString &filename, CachedFileStat *cstat = 0,
121
123
  */
122
124
  FileType getFileType(const StaticString &filename, CachedFileStat *cstat = 0,
123
125
  unsigned int throttleRate = 0);
126
+ /**
127
+ * Like getFileType(), but does not follow symlinks.
128
+ */
129
+ FileType getFileTypeNoFollowSymlinks(const StaticString &filename);
124
130
 
125
131
  /**
126
132
  * Create the given file with the given contents, permissions and ownership.
@@ -171,6 +171,19 @@ public:
171
171
  return output;
172
172
  }
173
173
 
174
+ /**
175
+ * Reads a line and returns the line including the newline character. Upon
176
+ * encountering EOF, the empty string is returned.
177
+ *
178
+ * The `max` parameter dictates the maximum length of the returned line.
179
+ * If the line is longer than this number of characters, then a SecurityException
180
+ * is thrown, and the BufferedIO becomes unusable (enters an undefined state).
181
+ *
182
+ * @throws SystemException
183
+ * @throws TimeoutException
184
+ * @throws SecurityException
185
+ * @throws boost::thread_interrupted
186
+ */
174
187
  string readLine(unsigned int max = 1024, unsigned long long *timeout = NULL) {
175
188
  string output;
176
189
  readUntil(boost::bind(newlineFound, _1, _2, &output, max), timeout);
@@ -1,6 +1,6 @@
1
1
  /*
2
2
  * Phusion Passenger - https://www.phusionpassenger.com/
3
- * Copyright (c) 2010-2013 Phusion
3
+ * Copyright (c) 2010-2014 Phusion
4
4
  *
5
5
  * "Phusion Passenger" is a trademark of Hongli Lai & Ninh Bui.
6
6
  *
@@ -437,7 +437,7 @@ public:
437
437
  accountsDatabase->add("_passenger-status", options.adminToolStatusPassword, false,
438
438
  Account::INSPECT_BASIC_INFO | Account::INSPECT_SENSITIVE_INFO |
439
439
  Account::INSPECT_BACKTRACES | Account::INSPECT_REQUESTS |
440
- Account::RESTART);
440
+ Account::DETACH | Account::RESTART);
441
441
  accountsDatabase->add("_web_server", options.exitPassword, false, Account::EXIT);
442
442
  messageServer = boost::make_shared<MessageServer>(
443
443
  parseUnixSocketAddress(options.adminSocketAddress), accountsDatabase);
@@ -584,6 +584,9 @@ public:
584
584
  * inaccessible.
585
585
  */
586
586
  P_DEBUG("Watchdog seems to be killed; forcing shutdown of all subprocesses");
587
+ // We send a SIGTERM first to allow processes to gracefully shut down.
588
+ syscalls::killpg(getpgrp(), SIGTERM);
589
+ usleep(500000);
587
590
  syscalls::killpg(getpgrp(), SIGKILL);
588
591
  _exit(2); // In case killpg() fails.
589
592
  } else {
@@ -592,6 +595,7 @@ public:
592
595
  */
593
596
  P_DEBUG("Received command to exit gracefully. "
594
597
  "Waiting until 5 seconds after all clients have disconnected...");
598
+ pool->prepareForShutdown();
595
599
  requestHandler->resetInactivityTime();
596
600
  while (requestHandler->inactivityTime() < 5000) {
597
601
  syscalls::usleep(250000);
@@ -696,8 +696,17 @@ private:
696
696
  status, (unsigned long) data.size());
697
697
 
698
698
  client->clientOutputPipe->write(header, pos - header);
699
+ if (!client->connected()) {
700
+ return;
701
+ }
699
702
  client->clientOutputPipe->write(data.data(), data.size());
703
+ if (!client->connected()) {
704
+ return;
705
+ }
700
706
  client->clientOutputPipe->end();
707
+ if (!client->connected()) {
708
+ return;
709
+ }
701
710
 
702
711
  if (client->useUnionStation()) {
703
712
  snprintf(header, end - header, "Status: %d %s",
@@ -778,8 +787,17 @@ private:
778
787
 
779
788
  const string header = str.str();
780
789
  client->clientOutputPipe->write(header.data(), header.size());
790
+ if (!client->connected()) {
791
+ return;
792
+ }
781
793
  client->clientOutputPipe->write(data.data(), data.size());
794
+ if (!client->connected()) {
795
+ return;
796
+ }
782
797
  client->clientOutputPipe->end();
798
+ if (!client->connected()) {
799
+ return;
800
+ }
783
801
 
784
802
  if (client->useUnionStation()) {
785
803
  client->logMessage("Status: 500 Internal Server Error");
@@ -1094,6 +1112,10 @@ private:
1094
1112
  void writeToClientOutputPipe(const ClientPtr &client, const StaticString &data) {
1095
1113
  bool wasCommittingToDisk = client->clientOutputPipe->isCommittingToDisk();
1096
1114
  bool nowCommittingToDisk = !client->clientOutputPipe->write(data.data(), data.size());
1115
+ if (!client->connected()) {
1116
+ // EPIPE/ECONNRESET detected.
1117
+ return;
1118
+ }
1097
1119
  if (!wasCommittingToDisk && nowCommittingToDisk) {
1098
1120
  RH_TRACE(client, 3, "Buffering response data to disk; temporarily stopping application socket.");
1099
1121
  client->backgroundOperations++;
@@ -2010,6 +2032,7 @@ private:
2010
2032
  }
2011
2033
 
2012
2034
  void sessionCheckedOut_real(ClientPtr client, const SessionPtr &session, const ExceptionPtr &e) {
2035
+ RH_LOG_EVENT(client, "sessionCheckedOut");
2013
2036
  if (!client->connected()) {
2014
2037
  return;
2015
2038
  }
@@ -2200,7 +2223,6 @@ private:
2200
2223
  data.append(" ");
2201
2224
  data.append(parser.getHeader("REQUEST_URI"));
2202
2225
  data.append(" HTTP/1.1\r\n");
2203
- data.append("Connection: close\r\n");
2204
2226
 
2205
2227
  for (it = parser.begin(); it != end; it++) {
2206
2228
  if (startsWith(it->first, "HTTP_") && it->first != "HTTP_CONNECTION") {
@@ -2221,6 +2243,15 @@ private:
2221
2243
  }
2222
2244
  }
2223
2245
 
2246
+ StaticString connection = parser.getHeader("HTTP_CONNECTION");
2247
+ if (connection == "upgrade" || connection == "Upgrade") {
2248
+ data.append("Connection: ");
2249
+ data.append(connection.data(), connection.size());
2250
+ data.append("\r\n");
2251
+ } else {
2252
+ data.append("Connection: close\r\n");
2253
+ }
2254
+
2224
2255
  StaticString header = parser.getHeader("CONTENT_LENGTH");
2225
2256
  if (!header.empty()) {
2226
2257
  data.append("Content-Length: ");
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
  # encoding: binary
3
3
  # Phusion Passenger - https://www.phusionpassenger.com/
4
- # Copyright (c) 2010-2013 Phusion
4
+ # Copyright (c) 2010-2014 Phusion
5
5
  #
6
6
  # "Phusion Passenger" is a trademark of Hongli Lai & Ninh Bui.
7
7
  #
@@ -24,6 +24,8 @@
24
24
  # THE SOFTWARE.
25
25
 
26
26
  require 'socket'
27
+ require 'thread'
28
+ require 'logger'
27
29
 
28
30
  module PhusionPassenger
29
31
  module App
@@ -52,6 +54,13 @@ module App
52
54
  end
53
55
  end
54
56
 
57
+ def self.init_passenger
58
+ require "#{options["ruby_libdir"]}/phusion_passenger"
59
+ PhusionPassenger.locate_directories(options["passenger_root"])
60
+ PhusionPassenger.require_passenger_lib 'message_channel'
61
+ PhusionPassenger.require_passenger_lib 'utils/tmpio'
62
+ end
63
+
55
64
  def self.ping_port(port)
56
65
  socket_domain = Socket::Constants::AF_INET
57
66
  sockaddr = Socket.pack_sockaddr_in(port, '127.0.0.1')
@@ -77,7 +86,14 @@ module App
77
86
  end
78
87
  end
79
88
 
80
- def self.load_app
89
+ def self.create_control_server
90
+ dir = Utils.mktmpdir('meteor')
91
+ filename = "#{dir}/control"
92
+ server = UNIXServer.new(filename)
93
+ return [server, dir, filename]
94
+ end
95
+
96
+ def self.load_app(control_server)
81
97
  port = nil
82
98
  tries = 0
83
99
  while port.nil? && tries < 200
@@ -101,34 +117,134 @@ module App
101
117
  # Meteor its own process group, and sending signals to the
102
118
  # entire process group.
103
119
  Process.setpgrp
120
+ control_server.close
104
121
  exec("meteor run -p #{port} #{production}")
105
122
  end
106
123
  $0 = options["process_title"] if options["process_title"]
107
124
  $0 = "#{$0} (#{pid})"
108
125
  return [pid, port]
109
126
  end
127
+
128
+ class ExitFlag
129
+ def initialize
130
+ @mutex = Mutex.new
131
+ @cond = ConditionVariable.new
132
+ @exit = false
133
+ end
134
+
135
+ def set
136
+ @mutex.synchronize do
137
+ @exit = true
138
+ @cond.broadcast
139
+ end
140
+ end
141
+
142
+ def wait
143
+ @mutex.synchronize do
144
+ while !@exit
145
+ @cond.wait(@mutex)
146
+ end
147
+ end
148
+ end
149
+ end
150
+
151
+ # When the HelperAgent is shutting down, it first sends a message (A) to application
152
+ # processes through the control socket that this is happening. The HelperAgent then
153
+ # waits until all HTTP connections are closed, before sending another message
154
+ # to application processes that they should shut down (B).
155
+ # Because Meteor opens long-running connections (e.g. for WebSocket), we have to shut
156
+ # down the Meteor app when A arrives, otherwise the HelperAgent will never send B.
157
+ def self.wait_for_exit_message(control_server)
158
+ exit_flag = ExitFlag.new
159
+ start_control_server_thread(control_server, exit_flag)
160
+ start_stdin_waiter_thread(exit_flag)
161
+ exit_flag.wait
162
+ end
163
+
164
+ def self.start_control_server_thread(control_server, exit_flag)
165
+ Thread.new do
166
+ Thread.current.abort_on_exception = true
167
+ while true
168
+ process_next_control_client(control_server, exit_flag)
169
+ end
170
+ end
171
+ end
172
+
173
+ def self.process_next_control_client(control_server, exit_flag)
174
+ logger = Logger.new(STDERR)
175
+ begin
176
+ client = control_server.accept
177
+ channel = MessageChannel.new(client)
178
+ while message = channel.read
179
+ process_next_control_message(message, logger, exit_flag)
180
+ end
181
+ rescue Exception => e
182
+ logger.error("#{e} (#{e.class})\n " + e.backtrace.join("\n "))
183
+ ensure
184
+ begin
185
+ client.close if client
186
+ rescue SystemCallError, IOError, SocketError
187
+ end
188
+ end
189
+ end
190
+
191
+ def self.process_next_control_message(message, logger, exit_flag)
192
+ if message[0] == "abort_long_running_connections"
193
+ logger.debug("Aborting long-running connections")
194
+ exit_flag.set
195
+ else
196
+ logger.error("Invalid control message: #{message.inspect}")
197
+ end
198
+ end
199
+
200
+ def self.start_stdin_waiter_thread(exit_flag)
201
+ Thread.new do
202
+ Thread.current.abort_on_exception = true
203
+ begin
204
+ STDIN.readline
205
+ rescue EOFError
206
+ ensure
207
+ exit_flag.set
208
+ end
209
+ end
210
+ end
211
+
110
212
 
111
213
 
112
214
  ################## Main code ##################
113
215
 
114
216
 
115
217
  handshake_and_read_startup_request
116
- pid, port = load_app
218
+ init_passenger
117
219
  begin
220
+ control_server, control_dir, control_filename = create_control_server
221
+ pid, port = load_app(control_server)
118
222
  while !ping_port(port)
119
223
  sleep 0.01
120
224
  end
121
225
  puts "!> Ready"
122
226
  puts "!> socket: main;tcp://127.0.0.1:#{port};http_session;0"
227
+ puts "!> socket: control;unix:#{control_filename};control;0"
123
228
  puts "!> "
124
- begin
125
- STDIN.readline
126
- rescue EOFError
127
- end
229
+ wait_for_exit_message(control_server)
128
230
  ensure
129
- Process.kill('INT', -pid) rescue nil
130
- Process.waitpid(pid) rescue nil
131
- Process.kill('INT', -pid) rescue nil
231
+ if pid
232
+ Process.kill('INT', -pid) rescue nil
233
+ Process.waitpid(pid) rescue nil
234
+ Process.kill('INT', -pid) rescue nil
235
+ end
236
+ if control_server
237
+ control_server.close
238
+ begin
239
+ File.unlink(control_filename)
240
+ rescue SystemCallError
241
+ end
242
+ require 'fileutils'
243
+ begin
244
+ FileUtils.remove_entry_secure(control_dir)
245
+ rescue SystemCallError
246
+ end
247
+ end
132
248
  end
133
249
 
134
250
  end # module App
@@ -135,15 +135,17 @@ function installServer() {
135
135
  finalizeStartup();
136
136
 
137
137
  PhusionPassenger.on('request', function(headers, socket, bodyBegin) {
138
- var req = HttplibEmulation.createIncomingMessage(headers, socket, bodyBegin);
139
- if (req.headers['upgrade']) {
138
+ var req, res;
139
+ if (headers['HTTP_UPGRADE']) {
140
140
  if (EventEmitter.listenerCount(server, 'upgrade') > 0) {
141
+ req = HttplibEmulation.createIncomingMessage(headers, socket, bodyBegin);
141
142
  server.emit('upgrade', req, socket, bodyBegin);
142
143
  } else {
143
144
  socket.destroy();
144
145
  }
145
146
  } else {
146
- var res = HttplibEmulation.createServerResponse(req);
147
+ req = HttplibEmulation.createIncomingMessage(headers, socket, bodyBegin);
148
+ res = HttplibEmulation.createServerResponse(req);
147
149
  server.emit('request', req, res);
148
150
  }
149
151
  });
@@ -106,6 +106,7 @@ class RequestHandler:
106
106
  if not client:
107
107
  done = True
108
108
  break
109
+ socket_hijacked = False
109
110
  try:
110
111
  try:
111
112
  env, input_stream = self.parse_request(client)
@@ -113,7 +114,7 @@ class RequestHandler:
113
114
  if env['REQUEST_METHOD'] == 'ping':
114
115
  self.process_ping(env, input_stream, client)
115
116
  else:
116
- self.process_request(env, input_stream, client)
117
+ socket_hijacked = self.process_request(env, input_stream, client)
117
118
  except KeyboardInterrupt:
118
119
  done = True
119
120
  except IOError:
@@ -123,16 +124,17 @@ class RequestHandler:
123
124
  except Exception:
124
125
  logging.exception("WSGI application raised an exception!")
125
126
  finally:
126
- try:
127
- # Shutdown the socket like this just in case the app
128
- # spawned a child process that keeps it open.
129
- client.shutdown(socket.SHUT_WR)
130
- except:
131
- pass
132
- try:
133
- client.close()
134
- except:
135
- pass
127
+ if not socket_hijacked:
128
+ try:
129
+ # Shutdown the socket like this just in case the app
130
+ # spawned a child process that keeps it open.
131
+ client.shutdown(socket.SHUT_WR)
132
+ except:
133
+ pass
134
+ try:
135
+ client.close()
136
+ except:
137
+ pass
136
138
  except KeyboardInterrupt:
137
139
  pass
138
140
 
@@ -232,7 +234,16 @@ class RequestHandler:
232
234
  headers_set[:] = [status, response_headers]
233
235
  return write
234
236
 
237
+ def hijack():
238
+ env['passenger.hijacked_socket'] = output_stream
239
+ return output_stream
240
+
241
+ env['passenger.hijack'] = hijack
242
+
235
243
  result = self.app(env, start_response)
244
+ if 'passenger.hijacked_socket' in env:
245
+ # Socket connection hijacked. Don't do anything.
246
+ return True
236
247
  try:
237
248
  for data in result:
238
249
  # Don't send headers until body appears.
@@ -244,6 +255,7 @@ class RequestHandler:
244
255
  finally:
245
256
  if hasattr(result, 'close'):
246
257
  result.close()
258
+ return False
247
259
 
248
260
  def process_ping(self, env, input_stream, output_stream):
249
261
  output_stream.sendall(b"pong")