Gestionnaire de fichiers - Editer - /home/wwgoat/public_html/blog/Core.tar
Arrière
Ed25519.php 0000644 00000000142 14720701675 0006221 0 ustar 00 <?php namespace ParagonIE\Sodium\Core; class Ed25519 extends \ParagonIE_Sodium_Core_Ed25519 { } AEGIS256.php 0000644 00000007016 14720701675 0006357 0 ustar 00 <?php if (!defined('SODIUM_COMPAT_AEGIS_C0')) { define('SODIUM_COMPAT_AEGIS_C0', "\x00\x01\x01\x02\x03\x05\x08\x0d\x15\x22\x37\x59\x90\xe9\x79\x62"); } if (!defined('SODIUM_COMPAT_AEGIS_C1')) { define('SODIUM_COMPAT_AEGIS_C1', "\xdb\x3d\x18\x55\x6d\xc2\x2f\xf1\x20\x11\x31\x42\x73\xb5\x28\xdd"); } class ParagonIE_Sodium_Core_AEGIS256 extends ParagonIE_Sodium_Core_AES { /** * @param string $ct * @param string $tag * @param string $ad * @param string $key * @param string $nonce * @return string * @throws SodiumException */ public static function decrypt($ct, $tag, $ad, $key, $nonce) { $state = self::init($key, $nonce); // ad_blocks = Split(ZeroPad(ad, 128), 128) $ad_blocks = (self::strlen($ad) + 15) >> 4; // for ai in ad_blocks: // Absorb(ai) for ($i = 0; $i < $ad_blocks; ++$i) { $ai = self::substr($ad, $i << 4, 16); if (self::strlen($ai) < 16) { $ai = str_pad($ai, 16, "\0", STR_PAD_RIGHT); } $state->absorb($ai); } $msg = ''; $cn = self::strlen($ct) & 15; $ct_blocks = self::strlen($ct) >> 4; // ct_blocks = Split(ZeroPad(ct, 128), 128) // cn = Tail(ct, |ct| mod 128) for ($i = 0; $i < $ct_blocks; ++$i) { $msg .= $state->dec(self::substr($ct, $i << 4, 16)); } // if cn is not empty: // msg = msg || DecPartial(cn) if ($cn) { $start = $ct_blocks << 4; $msg .= $state->decPartial(self::substr($ct, $start, $cn)); } $expected_tag = $state->finalize( self::strlen($ad) << 3, self::strlen($msg) << 3 ); if (!self::hashEquals($expected_tag, $tag)) { try { // The RFC says to erase msg, so we shall try: ParagonIE_Sodium_Compat::memzero($msg); } catch (SodiumException $ex) { // Do nothing if we cannot memzero } throw new SodiumException('verification failed'); } return $msg; } /** * @param string $msg * @param string $ad * @param string $key * @param string $nonce * @return array * @throws SodiumException */ public static function encrypt($msg, $ad, $key, $nonce) { $state = self::init($key, $nonce); $ad_len = self::strlen($ad); $msg_len = self::strlen($msg); $ad_blocks = ($ad_len + 15) >> 4; for ($i = 0; $i < $ad_blocks; ++$i) { $ai = self::substr($ad, $i << 4, 16); if (self::strlen($ai) < 16) { $ai = str_pad($ai, 16, "\0", STR_PAD_RIGHT); } $state->absorb($ai); } $ct = ''; $msg_blocks = ($msg_len + 15) >> 4; for ($i = 0; $i < $msg_blocks; ++$i) { $xi = self::substr($msg, $i << 4, 16); if (self::strlen($xi) < 16) { $xi = str_pad($xi, 16, "\0", STR_PAD_RIGHT); } $ct .= $state->enc($xi); } $tag = $state->finalize( $ad_len << 3, $msg_len << 3 ); return array( self::substr($ct, 0, $msg_len), $tag ); } /** * @param string $key * @param string $nonce * @return ParagonIE_Sodium_Core_AEGIS_State256 */ public static function init($key, $nonce) { return ParagonIE_Sodium_Core_AEGIS_State256::init($key, $nonce); } } ChaCha20/Ctx.php 0000644 00000000154 14720701675 0007275 0 ustar 00 <?php namespace ParagonIE\Sodium\Core\ChaCha20; class Ctx extends \ParagonIE_Sodium_Core_ChaCha20_Ctx { } ChaCha20/IetfCtx.php 0000644 00000000164 14720701675 0010106 0 ustar 00 <?php namespace ParagonIE\Sodium\Core\ChaCha20; class IetfCtx extends \ParagonIE_Sodium_Core_ChaCha20_IetfCtx { } SipHash.php 0000644 00000000142 14720701675 0006622 0 ustar 00 <?php namespace ParagonIE\Sodium\Core; class SipHash extends \ParagonIE_Sodium_Core_SipHash { } AES/Block.php 0000644 00000024342 14720701675 0006735 0 ustar 00 <?php if (class_exists('ParagonIE_Sodium_Core_AES_Block', false)) { return; } /** * @internal This should only be used by sodium_compat */ class ParagonIE_Sodium_Core_AES_Block extends SplFixedArray { /** * @var array<int, int> */ protected $values = array(); /** * @var int */ protected $size; /** * @param int $size */ public function __construct($size = 8) { parent::__construct($size); $this->size = $size; $this->values = array_fill(0, $size, 0); } /** * @return self */ public static function init() { return new self(8); } /** * @internal You should not use this directly from another application * * @param array<int, int> $array * @param bool $save_indexes * @return self * * @psalm-suppress MethodSignatureMismatch */ #[ReturnTypeWillChange] public static function fromArray($array, $save_indexes = null) { $count = count($array); if ($save_indexes) { $keys = array_keys($array); } else { $keys = range(0, $count - 1); } $array = array_values($array); /** @var array<int, int> $keys */ $obj = new ParagonIE_Sodium_Core_AES_Block(); if ($save_indexes) { for ($i = 0; $i < $count; ++$i) { $obj->offsetSet($keys[$i], $array[$i]); } } else { for ($i = 0; $i < $count; ++$i) { $obj->offsetSet($i, $array[$i]); } } return $obj; } /** * @internal You should not use this directly from another application * * @param int|null $offset * @param int $value * @return void * * @psalm-suppress MethodSignatureMismatch * @psalm-suppress MixedArrayOffset */ #[ReturnTypeWillChange] public function offsetSet($offset, $value) { if (!is_int($value)) { throw new InvalidArgumentException('Expected an integer'); } if (is_null($offset)) { $this->values[] = $value; } else { $this->values[$offset] = $value; } } /** * @internal You should not use this directly from another application * * @param int $offset * @return bool * * @psalm-suppress MethodSignatureMismatch * @psalm-suppress MixedArrayOffset */ #[ReturnTypeWillChange] public function offsetExists($offset) { return isset($this->values[$offset]); } /** * @internal You should not use this directly from another application * * @param int $offset * @return void * * @psalm-suppress MethodSignatureMismatch * @psalm-suppress MixedArrayOffset */ #[ReturnTypeWillChange] public function offsetUnset($offset) { unset($this->values[$offset]); } /** * @internal You should not use this directly from another application * * @param int $offset * @return int * * @psalm-suppress MethodSignatureMismatch * @psalm-suppress MixedArrayOffset */ #[ReturnTypeWillChange] public function offsetGet($offset) { if (!isset($this->values[$offset])) { $this->values[$offset] = 0; } return (int) ($this->values[$offset]); } /** * @internal You should not use this directly from another application * * @return array */ public function __debugInfo() { $out = array(); foreach ($this->values as $v) { $out[] = str_pad(dechex($v), 8, '0', STR_PAD_LEFT); } return array(implode(', ', $out)); /* return array(implode(', ', $this->values)); */ } /** * @param int $cl low bit mask * @param int $ch high bit mask * @param int $s shift * @param int $x index 1 * @param int $y index 2 * @return self */ public function swapN($cl, $ch, $s, $x, $y) { static $u32mask = ParagonIE_Sodium_Core_Util::U32_MAX; $a = $this->values[$x] & $u32mask; $b = $this->values[$y] & $u32mask; // (x) = (a & cl) | ((b & cl) << (s)); $this->values[$x] = ($a & $cl) | ((($b & $cl) << $s) & $u32mask); // (y) = ((a & ch) >> (s)) | (b & ch); $this->values[$y] = ((($a & $ch) & $u32mask) >> $s) | ($b & $ch); return $this; } /** * @param int $x index 1 * @param int $y index 2 * @return self */ public function swap2($x, $y) { return $this->swapN(0x55555555, 0xAAAAAAAA, 1, $x, $y); } /** * @param int $x index 1 * @param int $y index 2 * @return self */ public function swap4($x, $y) { return $this->swapN(0x33333333, 0xCCCCCCCC, 2, $x, $y); } /** * @param int $x index 1 * @param int $y index 2 * @return self */ public function swap8($x, $y) { return $this->swapN(0x0F0F0F0F, 0xF0F0F0F0, 4, $x, $y); } /** * @return self */ public function orthogonalize() { return $this ->swap2(0, 1) ->swap2(2, 3) ->swap2(4, 5) ->swap2(6, 7) ->swap4(0, 2) ->swap4(1, 3) ->swap4(4, 6) ->swap4(5, 7) ->swap8(0, 4) ->swap8(1, 5) ->swap8(2, 6) ->swap8(3, 7); } /** * @return self */ public function shiftRows() { for ($i = 0; $i < 8; ++$i) { $x = $this->values[$i] & ParagonIE_Sodium_Core_Util::U32_MAX; $this->values[$i] = ( ($x & 0x000000FF) | (($x & 0x0000FC00) >> 2) | (($x & 0x00000300) << 6) | (($x & 0x00F00000) >> 4) | (($x & 0x000F0000) << 4) | (($x & 0xC0000000) >> 6) | (($x & 0x3F000000) << 2) ) & ParagonIE_Sodium_Core_Util::U32_MAX; } return $this; } /** * @param int $x * @return int */ public static function rotr16($x) { return (($x << 16) & ParagonIE_Sodium_Core_Util::U32_MAX) | ($x >> 16); } /** * @return self */ public function mixColumns() { $q0 = $this->values[0]; $q1 = $this->values[1]; $q2 = $this->values[2]; $q3 = $this->values[3]; $q4 = $this->values[4]; $q5 = $this->values[5]; $q6 = $this->values[6]; $q7 = $this->values[7]; $r0 = (($q0 >> 8) | ($q0 << 24)) & ParagonIE_Sodium_Core_Util::U32_MAX; $r1 = (($q1 >> 8) | ($q1 << 24)) & ParagonIE_Sodium_Core_Util::U32_MAX; $r2 = (($q2 >> 8) | ($q2 << 24)) & ParagonIE_Sodium_Core_Util::U32_MAX; $r3 = (($q3 >> 8) | ($q3 << 24)) & ParagonIE_Sodium_Core_Util::U32_MAX; $r4 = (($q4 >> 8) | ($q4 << 24)) & ParagonIE_Sodium_Core_Util::U32_MAX; $r5 = (($q5 >> 8) | ($q5 << 24)) & ParagonIE_Sodium_Core_Util::U32_MAX; $r6 = (($q6 >> 8) | ($q6 << 24)) & ParagonIE_Sodium_Core_Util::U32_MAX; $r7 = (($q7 >> 8) | ($q7 << 24)) & ParagonIE_Sodium_Core_Util::U32_MAX; $this->values[0] = $q7 ^ $r7 ^ $r0 ^ self::rotr16($q0 ^ $r0); $this->values[1] = $q0 ^ $r0 ^ $q7 ^ $r7 ^ $r1 ^ self::rotr16($q1 ^ $r1); $this->values[2] = $q1 ^ $r1 ^ $r2 ^ self::rotr16($q2 ^ $r2); $this->values[3] = $q2 ^ $r2 ^ $q7 ^ $r7 ^ $r3 ^ self::rotr16($q3 ^ $r3); $this->values[4] = $q3 ^ $r3 ^ $q7 ^ $r7 ^ $r4 ^ self::rotr16($q4 ^ $r4); $this->values[5] = $q4 ^ $r4 ^ $r5 ^ self::rotr16($q5 ^ $r5); $this->values[6] = $q5 ^ $r5 ^ $r6 ^ self::rotr16($q6 ^ $r6); $this->values[7] = $q6 ^ $r6 ^ $r7 ^ self::rotr16($q7 ^ $r7); return $this; } /** * @return self */ public function inverseMixColumns() { $q0 = $this->values[0]; $q1 = $this->values[1]; $q2 = $this->values[2]; $q3 = $this->values[3]; $q4 = $this->values[4]; $q5 = $this->values[5]; $q6 = $this->values[6]; $q7 = $this->values[7]; $r0 = (($q0 >> 8) | ($q0 << 24)) & ParagonIE_Sodium_Core_Util::U32_MAX; $r1 = (($q1 >> 8) | ($q1 << 24)) & ParagonIE_Sodium_Core_Util::U32_MAX; $r2 = (($q2 >> 8) | ($q2 << 24)) & ParagonIE_Sodium_Core_Util::U32_MAX; $r3 = (($q3 >> 8) | ($q3 << 24)) & ParagonIE_Sodium_Core_Util::U32_MAX; $r4 = (($q4 >> 8) | ($q4 << 24)) & ParagonIE_Sodium_Core_Util::U32_MAX; $r5 = (($q5 >> 8) | ($q5 << 24)) & ParagonIE_Sodium_Core_Util::U32_MAX; $r6 = (($q6 >> 8) | ($q6 << 24)) & ParagonIE_Sodium_Core_Util::U32_MAX; $r7 = (($q7 >> 8) | ($q7 << 24)) & ParagonIE_Sodium_Core_Util::U32_MAX; $this->values[0] = $q5 ^ $q6 ^ $q7 ^ $r0 ^ $r5 ^ $r7 ^ self::rotr16($q0 ^ $q5 ^ $q6 ^ $r0 ^ $r5); $this->values[1] = $q0 ^ $q5 ^ $r0 ^ $r1 ^ $r5 ^ $r6 ^ $r7 ^ self::rotr16($q1 ^ $q5 ^ $q7 ^ $r1 ^ $r5 ^ $r6); $this->values[2] = $q0 ^ $q1 ^ $q6 ^ $r1 ^ $r2 ^ $r6 ^ $r7 ^ self::rotr16($q0 ^ $q2 ^ $q6 ^ $r2 ^ $r6 ^ $r7); $this->values[3] = $q0 ^ $q1 ^ $q2 ^ $q5 ^ $q6 ^ $r0 ^ $r2 ^ $r3 ^ $r5 ^ self::rotr16($q0 ^ $q1 ^ $q3 ^ $q5 ^ $q6 ^ $q7 ^ $r0 ^ $r3 ^ $r5 ^ $r7); $this->values[4] = $q1 ^ $q2 ^ $q3 ^ $q5 ^ $r1 ^ $r3 ^ $r4 ^ $r5 ^ $r6 ^ $r7 ^ self::rotr16($q1 ^ $q2 ^ $q4 ^ $q5 ^ $q7 ^ $r1 ^ $r4 ^ $r5 ^ $r6); $this->values[5] = $q2 ^ $q3 ^ $q4 ^ $q6 ^ $r2 ^ $r4 ^ $r5 ^ $r6 ^ $r7 ^ self::rotr16($q2 ^ $q3 ^ $q5 ^ $q6 ^ $r2 ^ $r5 ^ $r6 ^ $r7); $this->values[6] = $q3 ^ $q4 ^ $q5 ^ $q7 ^ $r3 ^ $r5 ^ $r6 ^ $r7 ^ self::rotr16($q3 ^ $q4 ^ $q6 ^ $q7 ^ $r3 ^ $r6 ^ $r7); $this->values[7] = $q4 ^ $q5 ^ $q6 ^ $r4 ^ $r6 ^ $r7 ^ self::rotr16($q4 ^ $q5 ^ $q7 ^ $r4 ^ $r7); return $this; } /** * @return self */ public function inverseShiftRows() { for ($i = 0; $i < 8; ++$i) { $x = $this->values[$i]; $this->values[$i] = ParagonIE_Sodium_Core_Util::U32_MAX & ( ($x & 0x000000FF) | (($x & 0x00003F00) << 2) | (($x & 0x0000C000) >> 6) | (($x & 0x000F0000) << 4) | (($x & 0x00F00000) >> 4) | (($x & 0x03000000) << 6) | (($x & 0xFC000000) >> 2) ); } return $this; } } AES/Expanded.php 0000644 00000000460 14720701675 0007426 0 ustar 00 <?php if (class_exists('ParagonIE_Sodium_Core_AES_Expanded', false)) { return; } /** * @internal This should only be used by sodium_compat */ class ParagonIE_Sodium_Core_AES_Expanded extends ParagonIE_Sodium_Core_AES_KeySchedule { /** @var bool $expanded */ protected $expanded = true; } AES/KeySchedule.php 0000644 00000003531 14720701675 0010105 0 ustar 00 <?php if (class_exists('ParagonIE_Sodium_Core_AES_KeySchedule', false)) { return; } /** * @internal This should only be used by sodium_compat */ class ParagonIE_Sodium_Core_AES_KeySchedule { /** @var array<int, int> $skey -- has size 120 */ protected $skey; /** @var bool $expanded */ protected $expanded = false; /** @var int $numRounds */ private $numRounds; /** * @param array $skey * @param int $numRounds */ public function __construct(array $skey, $numRounds = 10) { $this->skey = $skey; $this->numRounds = $numRounds; } /** * Get a value at an arbitrary index. Mostly used for unit testing. * * @param int $i * @return int */ public function get($i) { return $this->skey[$i]; } /** * @return int */ public function getNumRounds() { return $this->numRounds; } /** * @param int $offset * @return ParagonIE_Sodium_Core_AES_Block */ public function getRoundKey($offset) { return ParagonIE_Sodium_Core_AES_Block::fromArray( array_slice($this->skey, $offset, 8) ); } /** * Return an expanded key schedule * * @return ParagonIE_Sodium_Core_AES_Expanded */ public function expand() { $exp = new ParagonIE_Sodium_Core_AES_Expanded( array_fill(0, 120, 0), $this->numRounds ); $n = ($exp->numRounds + 1) << 2; for ($u = 0, $v = 0; $u < $n; ++$u, $v += 2) { $x = $y = $this->skey[$u]; $x &= 0x55555555; $exp->skey[$v] = ($x | ($x << 1)) & ParagonIE_Sodium_Core_Util::U32_MAX; $y &= 0xAAAAAAAA; $exp->skey[$v + 1] = ($y | ($y >> 1)) & ParagonIE_Sodium_Core_Util::U32_MAX; } return $exp; } } Poly1305/State.php 0000644 00000000160 14720701675 0007577 0 ustar 00 <?php namespace ParagonIE\Sodium\Core\Poly1305; class State extends \ParagonIE_Sodium_Core_Poly1305_State { } XSalsa20.php 0000644 00000002533 14720701675 0006626 0 ustar 00 <?php if (class_exists('ParagonIE_Sodium_Core_XSalsa20', false)) { return; } /** * Class ParagonIE_Sodium_Core_XSalsa20 */ abstract class ParagonIE_Sodium_Core_XSalsa20 extends ParagonIE_Sodium_Core_HSalsa20 { /** * Expand a key and nonce into an xsalsa20 keystream. * * @internal You should not use this directly from another application * * @param int $len * @param string $nonce * @param string $key * @return string * @throws SodiumException * @throws TypeError */ public static function xsalsa20($len, $nonce, $key) { $ret = self::salsa20( $len, self::substr($nonce, 16, 8), self::hsalsa20($nonce, $key) ); return $ret; } /** * Encrypt a string with XSalsa20. Doesn't provide integrity. * * @internal You should not use this directly from another application * * @param string $message * @param string $nonce * @param string $key * @return string * @throws SodiumException * @throws TypeError */ public static function xsalsa20_xor($message, $nonce, $key) { return self::xorStrings( $message, self::xsalsa20( self::strlen($message), $nonce, $key ) ); } } HSalsa20.php 0000644 00000000144 14720701675 0006602 0 ustar 00 <?php namespace ParagonIE\Sodium\Core; class HSalsa20 extends \ParagonIE_Sodium_Core_HSalsa20 { } Salsa20.php 0000644 00000000142 14720701675 0006470 0 ustar 00 <?php namespace ParagonIE\Sodium\Core; class Salsa20 extends \ParagonIE_Sodium_Core_Salsa20 { } BLAKE2b.php 0000644 00000000142 14720701675 0006325 0 ustar 00 <?php namespace ParagonIE\Sodium\Core; class BLAKE2b extends \ParagonIE_Sodium_Core_BLAKE2b { } Curve25519/Fe.php 0000644 00000000156 14720701675 0007314 0 ustar 00 <?php namespace ParagonIE\Sodium\Core\Curve25519; class Fe extends \ParagonIE_Sodium_Core_Curve25519_Fe { } Curve25519/README.md 0000644 00000000332 14720701675 0007524 0 ustar 00 # Curve25519 Data Structures These are PHP implementation of the [structs used in the ref10 curve25519 code](https://github.com/jedisct1/libsodium/blob/master/src/libsodium/include/sodium/private/curve25519_ref10.h). Curve25519/Ge/P2.php 0000644 00000000164 14720701675 0007575 0 ustar 00 <?php namespace ParagonIE\Sodium\Core\Curve25519\Ge; class P2 extends \ParagonIE_Sodium_Core_Curve25519_Ge_P2 { } Curve25519/Ge/P3.php 0000644 00000000164 14720701675 0007576 0 ustar 00 <?php namespace ParagonIE\Sodium\Core\Curve25519\Ge; class P3 extends \ParagonIE_Sodium_Core_Curve25519_Ge_P3 { } Curve25519/Ge/Precomp.php 0000644 00000000176 14720701675 0010724 0 ustar 00 <?php namespace ParagonIE\Sodium\Core\Curve25519\Ge; class Precomp extends \ParagonIE_Sodium_Core_Curve25519_Ge_Precomp { } Curve25519/Ge/Cached.php 0000644 00000000174 14720701675 0010464 0 ustar 00 <?php namespace ParagonIE\Sodium\Core\Curve25519\Ge; class Cached extends \ParagonIE_Sodium_Core_Curve25519_Ge_Cached { } Curve25519/Ge/P1p1.php 0000644 00000000170 14720701675 0010032 0 ustar 00 <?php namespace ParagonIE\Sodium\Core\Curve25519\Ge; class P1p1 extends \ParagonIE_Sodium_Core_Curve25519_Ge_P1p1 { } Curve25519/H.php 0000644 00000000154 14720701675 0007147 0 ustar 00 <?php namespace ParagonIE\Sodium\Core\Curve25519; class H extends \ParagonIE_Sodium_Core_Curve25519_H { } AES.php 0000644 00000037015 14720701675 0005704 0 ustar 00 <?php if (class_exists('ParagonIE_Sodium_Core_AES', false)) { return; } /** * Bitsliced implementation of the AES block cipher. * * Based on the implementation provided by BearSSL. * * @internal This should only be used by sodium_compat */ class ParagonIE_Sodium_Core_AES extends ParagonIE_Sodium_Core_Util { /** * @var int[] AES round constants */ private static $Rcon = array( 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1B, 0x36 ); /** * Mutates the values of $q! * * @param ParagonIE_Sodium_Core_AES_Block $q * @return void */ public static function sbox(ParagonIE_Sodium_Core_AES_Block $q) { /** * @var int $x0 * @var int $x1 * @var int $x2 * @var int $x3 * @var int $x4 * @var int $x5 * @var int $x6 * @var int $x7 */ $x0 = $q[7] & self::U32_MAX; $x1 = $q[6] & self::U32_MAX; $x2 = $q[5] & self::U32_MAX; $x3 = $q[4] & self::U32_MAX; $x4 = $q[3] & self::U32_MAX; $x5 = $q[2] & self::U32_MAX; $x6 = $q[1] & self::U32_MAX; $x7 = $q[0] & self::U32_MAX; $y14 = $x3 ^ $x5; $y13 = $x0 ^ $x6; $y9 = $x0 ^ $x3; $y8 = $x0 ^ $x5; $t0 = $x1 ^ $x2; $y1 = $t0 ^ $x7; $y4 = $y1 ^ $x3; $y12 = $y13 ^ $y14; $y2 = $y1 ^ $x0; $y5 = $y1 ^ $x6; $y3 = $y5 ^ $y8; $t1 = $x4 ^ $y12; $y15 = $t1 ^ $x5; $y20 = $t1 ^ $x1; $y6 = $y15 ^ $x7; $y10 = $y15 ^ $t0; $y11 = $y20 ^ $y9; $y7 = $x7 ^ $y11; $y17 = $y10 ^ $y11; $y19 = $y10 ^ $y8; $y16 = $t0 ^ $y11; $y21 = $y13 ^ $y16; $y18 = $x0 ^ $y16; /* * Non-linear section. */ $t2 = $y12 & $y15; $t3 = $y3 & $y6; $t4 = $t3 ^ $t2; $t5 = $y4 & $x7; $t6 = $t5 ^ $t2; $t7 = $y13 & $y16; $t8 = $y5 & $y1; $t9 = $t8 ^ $t7; $t10 = $y2 & $y7; $t11 = $t10 ^ $t7; $t12 = $y9 & $y11; $t13 = $y14 & $y17; $t14 = $t13 ^ $t12; $t15 = $y8 & $y10; $t16 = $t15 ^ $t12; $t17 = $t4 ^ $t14; $t18 = $t6 ^ $t16; $t19 = $t9 ^ $t14; $t20 = $t11 ^ $t16; $t21 = $t17 ^ $y20; $t22 = $t18 ^ $y19; $t23 = $t19 ^ $y21; $t24 = $t20 ^ $y18; $t25 = $t21 ^ $t22; $t26 = $t21 & $t23; $t27 = $t24 ^ $t26; $t28 = $t25 & $t27; $t29 = $t28 ^ $t22; $t30 = $t23 ^ $t24; $t31 = $t22 ^ $t26; $t32 = $t31 & $t30; $t33 = $t32 ^ $t24; $t34 = $t23 ^ $t33; $t35 = $t27 ^ $t33; $t36 = $t24 & $t35; $t37 = $t36 ^ $t34; $t38 = $t27 ^ $t36; $t39 = $t29 & $t38; $t40 = $t25 ^ $t39; $t41 = $t40 ^ $t37; $t42 = $t29 ^ $t33; $t43 = $t29 ^ $t40; $t44 = $t33 ^ $t37; $t45 = $t42 ^ $t41; $z0 = $t44 & $y15; $z1 = $t37 & $y6; $z2 = $t33 & $x7; $z3 = $t43 & $y16; $z4 = $t40 & $y1; $z5 = $t29 & $y7; $z6 = $t42 & $y11; $z7 = $t45 & $y17; $z8 = $t41 & $y10; $z9 = $t44 & $y12; $z10 = $t37 & $y3; $z11 = $t33 & $y4; $z12 = $t43 & $y13; $z13 = $t40 & $y5; $z14 = $t29 & $y2; $z15 = $t42 & $y9; $z16 = $t45 & $y14; $z17 = $t41 & $y8; /* * Bottom linear transformation. */ $t46 = $z15 ^ $z16; $t47 = $z10 ^ $z11; $t48 = $z5 ^ $z13; $t49 = $z9 ^ $z10; $t50 = $z2 ^ $z12; $t51 = $z2 ^ $z5; $t52 = $z7 ^ $z8; $t53 = $z0 ^ $z3; $t54 = $z6 ^ $z7; $t55 = $z16 ^ $z17; $t56 = $z12 ^ $t48; $t57 = $t50 ^ $t53; $t58 = $z4 ^ $t46; $t59 = $z3 ^ $t54; $t60 = $t46 ^ $t57; $t61 = $z14 ^ $t57; $t62 = $t52 ^ $t58; $t63 = $t49 ^ $t58; $t64 = $z4 ^ $t59; $t65 = $t61 ^ $t62; $t66 = $z1 ^ $t63; $s0 = $t59 ^ $t63; $s6 = $t56 ^ ~$t62; $s7 = $t48 ^ ~$t60; $t67 = $t64 ^ $t65; $s3 = $t53 ^ $t66; $s4 = $t51 ^ $t66; $s5 = $t47 ^ $t65; $s1 = $t64 ^ ~$s3; $s2 = $t55 ^ ~$t67; $q[7] = $s0 & self::U32_MAX; $q[6] = $s1 & self::U32_MAX; $q[5] = $s2 & self::U32_MAX; $q[4] = $s3 & self::U32_MAX; $q[3] = $s4 & self::U32_MAX; $q[2] = $s5 & self::U32_MAX; $q[1] = $s6 & self::U32_MAX; $q[0] = $s7 & self::U32_MAX; } /** * Mutates the values of $q! * * @param ParagonIE_Sodium_Core_AES_Block $q * @return void */ public static function invSbox(ParagonIE_Sodium_Core_AES_Block $q) { self::processInversion($q); self::sbox($q); self::processInversion($q); } /** * This is some boilerplate code needed to invert an S-box. Rather than repeat the code * twice, I moved it to a protected method. * * Mutates $q * * @param ParagonIE_Sodium_Core_AES_Block $q * @return void */ protected static function processInversion(ParagonIE_Sodium_Core_AES_Block $q) { $q0 = (~$q[0]) & self::U32_MAX; $q1 = (~$q[1]) & self::U32_MAX; $q2 = $q[2] & self::U32_MAX; $q3 = $q[3] & self::U32_MAX; $q4 = $q[4] & self::U32_MAX; $q5 = (~$q[5]) & self::U32_MAX; $q6 = (~$q[6]) & self::U32_MAX; $q7 = $q[7] & self::U32_MAX; $q[7] = ($q1 ^ $q4 ^ $q6) & self::U32_MAX; $q[6] = ($q0 ^ $q3 ^ $q5) & self::U32_MAX; $q[5] = ($q7 ^ $q2 ^ $q4) & self::U32_MAX; $q[4] = ($q6 ^ $q1 ^ $q3) & self::U32_MAX; $q[3] = ($q5 ^ $q0 ^ $q2) & self::U32_MAX; $q[2] = ($q4 ^ $q7 ^ $q1) & self::U32_MAX; $q[1] = ($q3 ^ $q6 ^ $q0) & self::U32_MAX; $q[0] = ($q2 ^ $q5 ^ $q7) & self::U32_MAX; } /** * @param int $x * @return int */ public static function subWord($x) { $q = ParagonIE_Sodium_Core_AES_Block::fromArray( array($x, $x, $x, $x, $x, $x, $x, $x) ); $q->orthogonalize(); self::sbox($q); $q->orthogonalize(); return $q[0] & self::U32_MAX; } /** * Calculate the key schedule from a given random key * * @param string $key * @return ParagonIE_Sodium_Core_AES_KeySchedule * @throws SodiumException */ public static function keySchedule($key) { $key_len = self::strlen($key); switch ($key_len) { case 16: $num_rounds = 10; break; case 24: $num_rounds = 12; break; case 32: $num_rounds = 14; break; default: throw new SodiumException('Invalid key length: ' . $key_len); } $skey = array(); $comp_skey = array(); $nk = $key_len >> 2; $nkf = ($num_rounds + 1) << 2; $tmp = 0; for ($i = 0; $i < $nk; ++$i) { $tmp = self::load_4(self::substr($key, $i << 2, 4)); $skey[($i << 1)] = $tmp; $skey[($i << 1) + 1] = $tmp; } for ($i = $nk, $j = 0, $k = 0; $i < $nkf; ++$i) { if ($j === 0) { $tmp = (($tmp & 0xff) << 24) | ($tmp >> 8); $tmp = (self::subWord($tmp) ^ self::$Rcon[$k]) & self::U32_MAX; } elseif ($nk > 6 && $j === 4) { $tmp = self::subWord($tmp); } $tmp ^= $skey[($i - $nk) << 1]; $skey[($i << 1)] = $tmp & self::U32_MAX; $skey[($i << 1) + 1] = $tmp & self::U32_MAX; if (++$j === $nk) { /** @psalm-suppress LoopInvalidation */ $j = 0; ++$k; } } for ($i = 0; $i < $nkf; $i += 4) { $q = ParagonIE_Sodium_Core_AES_Block::fromArray( array_slice($skey, $i << 1, 8) ); $q->orthogonalize(); // We have to overwrite $skey since we're not using C pointers like BearSSL did for ($j = 0; $j < 8; ++$j) { $skey[($i << 1) + $j] = $q[$j]; } } for ($i = 0, $j = 0; $i < $nkf; ++$i, $j += 2) { $comp_skey[$i] = ($skey[$j] & 0x55555555) | ($skey[$j + 1] & 0xAAAAAAAA); } return new ParagonIE_Sodium_Core_AES_KeySchedule($comp_skey, $num_rounds); } /** * Mutates $q * * @param ParagonIE_Sodium_Core_AES_KeySchedule $skey * @param ParagonIE_Sodium_Core_AES_Block $q * @param int $offset * @return void */ public static function addRoundKey( ParagonIE_Sodium_Core_AES_Block $q, ParagonIE_Sodium_Core_AES_KeySchedule $skey, $offset = 0 ) { $block = $skey->getRoundKey($offset); for ($j = 0; $j < 8; ++$j) { $q[$j] = ($q[$j] ^ $block[$j]) & ParagonIE_Sodium_Core_Util::U32_MAX; } } /** * This mainly exists for testing, as we need the round key features for AEGIS. * * @param string $message * @param string $key * @return string * @throws SodiumException */ public static function decryptBlockECB($message, $key) { if (self::strlen($message) !== 16) { throw new SodiumException('decryptBlockECB() expects a 16 byte message'); } $skey = self::keySchedule($key)->expand(); $q = ParagonIE_Sodium_Core_AES_Block::init(); $q[0] = self::load_4(self::substr($message, 0, 4)); $q[2] = self::load_4(self::substr($message, 4, 4)); $q[4] = self::load_4(self::substr($message, 8, 4)); $q[6] = self::load_4(self::substr($message, 12, 4)); $q->orthogonalize(); self::bitsliceDecryptBlock($skey, $q); $q->orthogonalize(); return self::store32_le($q[0]) . self::store32_le($q[2]) . self::store32_le($q[4]) . self::store32_le($q[6]); } /** * This mainly exists for testing, as we need the round key features for AEGIS. * * @param string $message * @param string $key * @return string * @throws SodiumException */ public static function encryptBlockECB($message, $key) { if (self::strlen($message) !== 16) { throw new SodiumException('encryptBlockECB() expects a 16 byte message'); } $comp_skey = self::keySchedule($key); $skey = $comp_skey->expand(); $q = ParagonIE_Sodium_Core_AES_Block::init(); $q[0] = self::load_4(self::substr($message, 0, 4)); $q[2] = self::load_4(self::substr($message, 4, 4)); $q[4] = self::load_4(self::substr($message, 8, 4)); $q[6] = self::load_4(self::substr($message, 12, 4)); $q->orthogonalize(); self::bitsliceEncryptBlock($skey, $q); $q->orthogonalize(); return self::store32_le($q[0]) . self::store32_le($q[2]) . self::store32_le($q[4]) . self::store32_le($q[6]); } /** * Mutates $q * * @param ParagonIE_Sodium_Core_AES_Expanded $skey * @param ParagonIE_Sodium_Core_AES_Block $q * @return void */ public static function bitsliceEncryptBlock( ParagonIE_Sodium_Core_AES_Expanded $skey, ParagonIE_Sodium_Core_AES_Block $q ) { self::addRoundKey($q, $skey); for ($u = 1; $u < $skey->getNumRounds(); ++$u) { self::sbox($q); $q->shiftRows(); $q->mixColumns(); self::addRoundKey($q, $skey, ($u << 3)); } self::sbox($q); $q->shiftRows(); self::addRoundKey($q, $skey, ($skey->getNumRounds() << 3)); } /** * @param string $x * @param string $y * @return string */ public static function aesRound($x, $y) { $q = ParagonIE_Sodium_Core_AES_Block::init(); $q[0] = self::load_4(self::substr($x, 0, 4)); $q[2] = self::load_4(self::substr($x, 4, 4)); $q[4] = self::load_4(self::substr($x, 8, 4)); $q[6] = self::load_4(self::substr($x, 12, 4)); $rk = ParagonIE_Sodium_Core_AES_Block::init(); $rk[0] = $rk[1] = self::load_4(self::substr($y, 0, 4)); $rk[2] = $rk[3] = self::load_4(self::substr($y, 4, 4)); $rk[4] = $rk[5] = self::load_4(self::substr($y, 8, 4)); $rk[6] = $rk[7] = self::load_4(self::substr($y, 12, 4)); $q->orthogonalize(); self::sbox($q); $q->shiftRows(); $q->mixColumns(); $q->orthogonalize(); // add round key without key schedule: for ($i = 0; $i < 8; ++$i) { $q[$i] ^= $rk[$i]; } return self::store32_le($q[0]) . self::store32_le($q[2]) . self::store32_le($q[4]) . self::store32_le($q[6]); } /** * Process two AES blocks in one shot. * * @param string $b0 First AES block * @param string $rk0 First round key * @param string $b1 Second AES block * @param string $rk1 Second round key * @return string[] */ public static function doubleRound($b0, $rk0, $b1, $rk1) { $q = ParagonIE_Sodium_Core_AES_Block::init(); // First block $q[0] = self::load_4(self::substr($b0, 0, 4)); $q[2] = self::load_4(self::substr($b0, 4, 4)); $q[4] = self::load_4(self::substr($b0, 8, 4)); $q[6] = self::load_4(self::substr($b0, 12, 4)); // Second block $q[1] = self::load_4(self::substr($b1, 0, 4)); $q[3] = self::load_4(self::substr($b1, 4, 4)); $q[5] = self::load_4(self::substr($b1, 8, 4)); $q[7] = self::load_4(self::substr($b1, 12, 4));; $rk = ParagonIE_Sodium_Core_AES_Block::init(); // First round key $rk[0] = self::load_4(self::substr($rk0, 0, 4)); $rk[2] = self::load_4(self::substr($rk0, 4, 4)); $rk[4] = self::load_4(self::substr($rk0, 8, 4)); $rk[6] = self::load_4(self::substr($rk0, 12, 4)); // Second round key $rk[1] = self::load_4(self::substr($rk1, 0, 4)); $rk[3] = self::load_4(self::substr($rk1, 4, 4)); $rk[5] = self::load_4(self::substr($rk1, 8, 4)); $rk[7] = self::load_4(self::substr($rk1, 12, 4)); $q->orthogonalize(); self::sbox($q); $q->shiftRows(); $q->mixColumns(); $q->orthogonalize(); // add round key without key schedule: for ($i = 0; $i < 8; ++$i) { $q[$i] ^= $rk[$i]; } return array( self::store32_le($q[0]) . self::store32_le($q[2]) . self::store32_le($q[4]) . self::store32_le($q[6]), self::store32_le($q[1]) . self::store32_le($q[3]) . self::store32_le($q[5]) . self::store32_le($q[7]), ); } /** * @param ParagonIE_Sodium_Core_AES_Expanded $skey * @param ParagonIE_Sodium_Core_AES_Block $q * @return void */ public static function bitsliceDecryptBlock( ParagonIE_Sodium_Core_AES_Expanded $skey, ParagonIE_Sodium_Core_AES_Block $q ) { self::addRoundKey($q, $skey, ($skey->getNumRounds() << 3)); for ($u = $skey->getNumRounds() - 1; $u > 0; --$u) { $q->inverseShiftRows(); self::invSbox($q); self::addRoundKey($q, $skey, ($u << 3)); $q->inverseMixColumns(); } $q->inverseShiftRows(); self::invSbox($q); self::addRoundKey($q, $skey, ($u << 3)); } } ChaCha20.php 0000644 00000000144 14720701675 0006536 0 ustar 00 <?php namespace ParagonIE\Sodium\Core; class ChaCha20 extends \ParagonIE_Sodium_Core_ChaCha20 { } AEGIS128L.php 0000644 00000007124 14720701675 0006471 0 ustar 00 <?php if (!defined('SODIUM_COMPAT_AEGIS_C0')) { define('SODIUM_COMPAT_AEGIS_C0', "\x00\x01\x01\x02\x03\x05\x08\x0d\x15\x22\x37\x59\x90\xe9\x79\x62"); } if (!defined('SODIUM_COMPAT_AEGIS_C1')) { define('SODIUM_COMPAT_AEGIS_C1', "\xdb\x3d\x18\x55\x6d\xc2\x2f\xf1\x20\x11\x31\x42\x73\xb5\x28\xdd"); } class ParagonIE_Sodium_Core_AEGIS128L extends ParagonIE_Sodium_Core_AES { /** * @param string $ct * @param string $tag * @param string $ad * @param string $key * @param string $nonce * @return string * @throws SodiumException */ public static function decrypt($ct, $tag, $ad, $key, $nonce) { $state = self::init($key, $nonce); $ad_blocks = (self::strlen($ad) + 31) >> 5; for ($i = 0; $i < $ad_blocks; ++$i) { $ai = self::substr($ad, $i << 5, 32); if (self::strlen($ai) < 32) { $ai = str_pad($ai, 32, "\0", STR_PAD_RIGHT); } $state->absorb($ai); } $msg = ''; $cn = self::strlen($ct) & 31; $ct_blocks = self::strlen($ct) >> 5; for ($i = 0; $i < $ct_blocks; ++$i) { $msg .= $state->dec(self::substr($ct, $i << 5, 32)); } if ($cn) { $start = $ct_blocks << 5; $msg .= $state->decPartial(self::substr($ct, $start, $cn)); } $expected_tag = $state->finalize( self::strlen($ad) << 3, self::strlen($msg) << 3 ); if (!self::hashEquals($expected_tag, $tag)) { try { // The RFC says to erase msg, so we shall try: ParagonIE_Sodium_Compat::memzero($msg); } catch (SodiumException $ex) { // Do nothing if we cannot memzero } throw new SodiumException('verification failed'); } return $msg; } /** * @param string $msg * @param string $ad * @param string $key * @param string $nonce * @return array * * @throws SodiumException */ public static function encrypt($msg, $ad, $key, $nonce) { $state = self::init($key, $nonce); // ad_blocks = Split(ZeroPad(ad, 256), 256) // for ai in ad_blocks: // Absorb(ai) $ad_len = self::strlen($ad); $msg_len = self::strlen($msg); $ad_blocks = ($ad_len + 31) >> 5; for ($i = 0; $i < $ad_blocks; ++$i) { $ai = self::substr($ad, $i << 5, 32); if (self::strlen($ai) < 32) { $ai = str_pad($ai, 32, "\0", STR_PAD_RIGHT); } $state->absorb($ai); } // msg_blocks = Split(ZeroPad(msg, 256), 256) // for xi in msg_blocks: // ct = ct || Enc(xi) $ct = ''; $msg_blocks = ($msg_len + 31) >> 5; for ($i = 0; $i < $msg_blocks; ++$i) { $xi = self::substr($msg, $i << 5, 32); if (self::strlen($xi) < 32) { $xi = str_pad($xi, 32, "\0", STR_PAD_RIGHT); } $ct .= $state->enc($xi); } // tag = Finalize(|ad|, |msg|) // ct = Truncate(ct, |msg|) $tag = $state->finalize( $ad_len << 3, $msg_len << 3 ); // return ct and tag return array( self::substr($ct, 0, $msg_len), $tag ); } /** * @param string $key * @param string $nonce * @return ParagonIE_Sodium_Core_AEGIS_State128L */ public static function init($key, $nonce) { return ParagonIE_Sodium_Core_AEGIS_State128L::init($key, $nonce); } } Base64/Original.php 0000644 00000017055 14720701675 0010066 0 ustar 00 <?php /** * Class ParagonIE_Sodium_Core_Base64 * * Copyright (c) 2016 - 2018 Paragon Initiative Enterprises. * Copyright (c) 2014 Steve "Sc00bz" Thomas (steve at tobtu dot com) */ class ParagonIE_Sodium_Core_Base64_Original { // COPY ParagonIE_Sodium_Core_Base64_Common STARTING HERE /** * Encode into Base64 * * Base64 character set "[A-Z][a-z][0-9]+/" * * @param string $src * @return string * @throws TypeError */ public static function encode($src) { return self::doEncode($src, true); } /** * Encode into Base64, no = padding * * Base64 character set "[A-Z][a-z][0-9]+/" * * @param string $src * @return string * @throws TypeError */ public static function encodeUnpadded($src) { return self::doEncode($src, false); } /** * @param string $src * @param bool $pad Include = padding? * @return string * @throws TypeError */ protected static function doEncode($src, $pad = true) { $dest = ''; $srcLen = ParagonIE_Sodium_Core_Util::strlen($src); // Main loop (no padding): for ($i = 0; $i + 3 <= $srcLen; $i += 3) { /** @var array<int, int> $chunk */ $chunk = unpack('C*', ParagonIE_Sodium_Core_Util::substr($src, $i, 3)); $b0 = $chunk[1]; $b1 = $chunk[2]; $b2 = $chunk[3]; $dest .= self::encode6Bits( $b0 >> 2 ) . self::encode6Bits((($b0 << 4) | ($b1 >> 4)) & 63) . self::encode6Bits((($b1 << 2) | ($b2 >> 6)) & 63) . self::encode6Bits( $b2 & 63); } // The last chunk, which may have padding: if ($i < $srcLen) { /** @var array<int, int> $chunk */ $chunk = unpack('C*', ParagonIE_Sodium_Core_Util::substr($src, $i, $srcLen - $i)); $b0 = $chunk[1]; if ($i + 1 < $srcLen) { $b1 = $chunk[2]; $dest .= self::encode6Bits($b0 >> 2) . self::encode6Bits((($b0 << 4) | ($b1 >> 4)) & 63) . self::encode6Bits(($b1 << 2) & 63); if ($pad) { $dest .= '='; } } else { $dest .= self::encode6Bits( $b0 >> 2) . self::encode6Bits(($b0 << 4) & 63); if ($pad) { $dest .= '=='; } } } return $dest; } /** * decode from base64 into binary * * Base64 character set "./[A-Z][a-z][0-9]" * * @param string $src * @param bool $strictPadding * @return string * @throws RangeException * @throws TypeError * @psalm-suppress RedundantCondition */ public static function decode($src, $strictPadding = false) { // Remove padding $srcLen = ParagonIE_Sodium_Core_Util::strlen($src); if ($srcLen === 0) { return ''; } if ($strictPadding) { if (($srcLen & 3) === 0) { if ($src[$srcLen - 1] === '=') { $srcLen--; if ($src[$srcLen - 1] === '=') { $srcLen--; } } } if (($srcLen & 3) === 1) { throw new RangeException( 'Incorrect padding' ); } if ($src[$srcLen - 1] === '=') { throw new RangeException( 'Incorrect padding' ); } } else { $src = rtrim($src, '='); $srcLen = ParagonIE_Sodium_Core_Util::strlen($src); } $err = 0; $dest = ''; // Main loop (no padding): for ($i = 0; $i + 4 <= $srcLen; $i += 4) { /** @var array<int, int> $chunk */ $chunk = unpack('C*', ParagonIE_Sodium_Core_Util::substr($src, $i, 4)); $c0 = self::decode6Bits($chunk[1]); $c1 = self::decode6Bits($chunk[2]); $c2 = self::decode6Bits($chunk[3]); $c3 = self::decode6Bits($chunk[4]); $dest .= pack( 'CCC', ((($c0 << 2) | ($c1 >> 4)) & 0xff), ((($c1 << 4) | ($c2 >> 2)) & 0xff), ((($c2 << 6) | $c3) & 0xff) ); $err |= ($c0 | $c1 | $c2 | $c3) >> 8; } // The last chunk, which may have padding: if ($i < $srcLen) { /** @var array<int, int> $chunk */ $chunk = unpack('C*', ParagonIE_Sodium_Core_Util::substr($src, $i, $srcLen - $i)); $c0 = self::decode6Bits($chunk[1]); if ($i + 2 < $srcLen) { $c1 = self::decode6Bits($chunk[2]); $c2 = self::decode6Bits($chunk[3]); $dest .= pack( 'CC', ((($c0 << 2) | ($c1 >> 4)) & 0xff), ((($c1 << 4) | ($c2 >> 2)) & 0xff) ); $err |= ($c0 | $c1 | $c2) >> 8; } elseif ($i + 1 < $srcLen) { $c1 = self::decode6Bits($chunk[2]); $dest .= pack( 'C', ((($c0 << 2) | ($c1 >> 4)) & 0xff) ); $err |= ($c0 | $c1) >> 8; } elseif ($i < $srcLen && $strictPadding) { $err |= 1; } } /** @var bool $check */ $check = ($err === 0); if (!$check) { throw new RangeException( 'Base64::decode() only expects characters in the correct base64 alphabet' ); } return $dest; } // COPY ParagonIE_Sodium_Core_Base64_Common ENDING HERE /** * Uses bitwise operators instead of table-lookups to turn 6-bit integers * into 8-bit integers. * * Base64 character set: * [A-Z] [a-z] [0-9] + / * 0x41-0x5a, 0x61-0x7a, 0x30-0x39, 0x2b, 0x2f * * @param int $src * @return int */ protected static function decode6Bits($src) { $ret = -1; // if ($src > 0x40 && $src < 0x5b) $ret += $src - 0x41 + 1; // -64 $ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 64); // if ($src > 0x60 && $src < 0x7b) $ret += $src - 0x61 + 26 + 1; // -70 $ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 70); // if ($src > 0x2f && $src < 0x3a) $ret += $src - 0x30 + 52 + 1; // 5 $ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src + 5); // if ($src == 0x2b) $ret += 62 + 1; $ret += (((0x2a - $src) & ($src - 0x2c)) >> 8) & 63; // if ($src == 0x2f) ret += 63 + 1; $ret += (((0x2e - $src) & ($src - 0x30)) >> 8) & 64; return $ret; } /** * Uses bitwise operators instead of table-lookups to turn 8-bit integers * into 6-bit integers. * * @param int $src * @return string */ protected static function encode6Bits($src) { $diff = 0x41; // if ($src > 25) $diff += 0x61 - 0x41 - 26; // 6 $diff += ((25 - $src) >> 8) & 6; // if ($src > 51) $diff += 0x30 - 0x61 - 26; // -75 $diff -= ((51 - $src) >> 8) & 75; // if ($src > 61) $diff += 0x2b - 0x30 - 10; // -15 $diff -= ((61 - $src) >> 8) & 15; // if ($src > 62) $diff += 0x2f - 0x2b - 1; // 3 $diff += ((62 - $src) >> 8) & 3; return pack('C', $src + $diff); } } Base64/UrlSafe.php 0000644 00000017063 14720701675 0007662 0 ustar 00 <?php /** * Class ParagonIE_Sodium_Core_Base64UrlSafe * * Copyright (c) 2016 - 2018 Paragon Initiative Enterprises. * Copyright (c) 2014 Steve "Sc00bz" Thomas (steve at tobtu dot com) */ class ParagonIE_Sodium_Core_Base64_UrlSafe { // COPY ParagonIE_Sodium_Core_Base64_Common STARTING HERE /** * Encode into Base64 * * Base64 character set "[A-Z][a-z][0-9]+/" * * @param string $src * @return string * @throws TypeError */ public static function encode($src) { return self::doEncode($src, true); } /** * Encode into Base64, no = padding * * Base64 character set "[A-Z][a-z][0-9]+/" * * @param string $src * @return string * @throws TypeError */ public static function encodeUnpadded($src) { return self::doEncode($src, false); } /** * @param string $src * @param bool $pad Include = padding? * @return string * @throws TypeError */ protected static function doEncode($src, $pad = true) { $dest = ''; $srcLen = ParagonIE_Sodium_Core_Util::strlen($src); // Main loop (no padding): for ($i = 0; $i + 3 <= $srcLen; $i += 3) { /** @var array<int, int> $chunk */ $chunk = unpack('C*', ParagonIE_Sodium_Core_Util::substr($src, $i, 3)); $b0 = $chunk[1]; $b1 = $chunk[2]; $b2 = $chunk[3]; $dest .= self::encode6Bits( $b0 >> 2 ) . self::encode6Bits((($b0 << 4) | ($b1 >> 4)) & 63) . self::encode6Bits((($b1 << 2) | ($b2 >> 6)) & 63) . self::encode6Bits( $b2 & 63); } // The last chunk, which may have padding: if ($i < $srcLen) { /** @var array<int, int> $chunk */ $chunk = unpack('C*', ParagonIE_Sodium_Core_Util::substr($src, $i, $srcLen - $i)); $b0 = $chunk[1]; if ($i + 1 < $srcLen) { $b1 = $chunk[2]; $dest .= self::encode6Bits($b0 >> 2) . self::encode6Bits((($b0 << 4) | ($b1 >> 4)) & 63) . self::encode6Bits(($b1 << 2) & 63); if ($pad) { $dest .= '='; } } else { $dest .= self::encode6Bits( $b0 >> 2) . self::encode6Bits(($b0 << 4) & 63); if ($pad) { $dest .= '=='; } } } return $dest; } /** * decode from base64 into binary * * Base64 character set "./[A-Z][a-z][0-9]" * * @param string $src * @param bool $strictPadding * @return string * @throws RangeException * @throws TypeError * @psalm-suppress RedundantCondition */ public static function decode($src, $strictPadding = false) { // Remove padding $srcLen = ParagonIE_Sodium_Core_Util::strlen($src); if ($srcLen === 0) { return ''; } if ($strictPadding) { if (($srcLen & 3) === 0) { if ($src[$srcLen - 1] === '=') { $srcLen--; if ($src[$srcLen - 1] === '=') { $srcLen--; } } } if (($srcLen & 3) === 1) { throw new RangeException( 'Incorrect padding' ); } if ($src[$srcLen - 1] === '=') { throw new RangeException( 'Incorrect padding' ); } } else { $src = rtrim($src, '='); $srcLen = ParagonIE_Sodium_Core_Util::strlen($src); } $err = 0; $dest = ''; // Main loop (no padding): for ($i = 0; $i + 4 <= $srcLen; $i += 4) { /** @var array<int, int> $chunk */ $chunk = unpack('C*', ParagonIE_Sodium_Core_Util::substr($src, $i, 4)); $c0 = self::decode6Bits($chunk[1]); $c1 = self::decode6Bits($chunk[2]); $c2 = self::decode6Bits($chunk[3]); $c3 = self::decode6Bits($chunk[4]); $dest .= pack( 'CCC', ((($c0 << 2) | ($c1 >> 4)) & 0xff), ((($c1 << 4) | ($c2 >> 2)) & 0xff), ((($c2 << 6) | $c3) & 0xff) ); $err |= ($c0 | $c1 | $c2 | $c3) >> 8; } // The last chunk, which may have padding: if ($i < $srcLen) { /** @var array<int, int> $chunk */ $chunk = unpack('C*', ParagonIE_Sodium_Core_Util::substr($src, $i, $srcLen - $i)); $c0 = self::decode6Bits($chunk[1]); if ($i + 2 < $srcLen) { $c1 = self::decode6Bits($chunk[2]); $c2 = self::decode6Bits($chunk[3]); $dest .= pack( 'CC', ((($c0 << 2) | ($c1 >> 4)) & 0xff), ((($c1 << 4) | ($c2 >> 2)) & 0xff) ); $err |= ($c0 | $c1 | $c2) >> 8; } elseif ($i + 1 < $srcLen) { $c1 = self::decode6Bits($chunk[2]); $dest .= pack( 'C', ((($c0 << 2) | ($c1 >> 4)) & 0xff) ); $err |= ($c0 | $c1) >> 8; } elseif ($i < $srcLen && $strictPadding) { $err |= 1; } } /** @var bool $check */ $check = ($err === 0); if (!$check) { throw new RangeException( 'Base64::decode() only expects characters in the correct base64 alphabet' ); } return $dest; } // COPY ParagonIE_Sodium_Core_Base64_Common ENDING HERE /** * Uses bitwise operators instead of table-lookups to turn 6-bit integers * into 8-bit integers. * * Base64 character set: * [A-Z] [a-z] [0-9] + / * 0x41-0x5a, 0x61-0x7a, 0x30-0x39, 0x2b, 0x2f * * @param int $src * @return int */ protected static function decode6Bits($src) { $ret = -1; // if ($src > 0x40 && $src < 0x5b) $ret += $src - 0x41 + 1; // -64 $ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 64); // if ($src > 0x60 && $src < 0x7b) $ret += $src - 0x61 + 26 + 1; // -70 $ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 70); // if ($src > 0x2f && $src < 0x3a) $ret += $src - 0x30 + 52 + 1; // 5 $ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src + 5); // if ($src == 0x2c) $ret += 62 + 1; $ret += (((0x2c - $src) & ($src - 0x2e)) >> 8) & 63; // if ($src == 0x5f) ret += 63 + 1; $ret += (((0x5e - $src) & ($src - 0x60)) >> 8) & 64; return $ret; } /** * Uses bitwise operators instead of table-lookups to turn 8-bit integers * into 6-bit integers. * * @param int $src * @return string */ protected static function encode6Bits($src) { $diff = 0x41; // if ($src > 25) $diff += 0x61 - 0x41 - 26; // 6 $diff += ((25 - $src) >> 8) & 6; // if ($src > 51) $diff += 0x30 - 0x61 - 26; // -75 $diff -= ((51 - $src) >> 8) & 75; // if ($src > 61) $diff += 0x2d - 0x30 - 10; // -13 $diff -= ((61 - $src) >> 8) & 13; // if ($src > 62) $diff += 0x5f - 0x2b - 1; // 3 $diff += ((62 - $src) >> 8) & 49; return pack('C', $src + $diff); } } Util.php 0000644 00000000134 14720701675 0006201 0 ustar 00 <?php namespace ParagonIE\Sodium\Core; class Util extends \ParagonIE_Sodium_Core_Util { } HChaCha20.php 0000644 00000000146 14720701675 0006650 0 ustar 00 <?php namespace ParagonIE\Sodium\Core; class HChaCha20 extends \ParagonIE_Sodium_Core_HChaCha20 { } AEGIS/State128L.php 0000644 00000020052 14720701675 0007544 0 ustar 00 <?php if (class_exists('ParagonIE_Sodium_Core_AEGIS_State128L', false)) { return; } if (!defined('SODIUM_COMPAT_AEGIS_C0')) { define('SODIUM_COMPAT_AEGIS_C0', "\x00\x01\x01\x02\x03\x05\x08\x0d\x15\x22\x37\x59\x90\xe9\x79\x62"); } if (!defined('SODIUM_COMPAT_AEGIS_C1')) { define('SODIUM_COMPAT_AEGIS_C1', "\xdb\x3d\x18\x55\x6d\xc2\x2f\xf1\x20\x11\x31\x42\x73\xb5\x28\xdd"); } class ParagonIE_Sodium_Core_AEGIS_State128L { /** @var array<int, string> $state */ protected $state; public function __construct() { $this->state = array_fill(0, 8, ''); } /** * @internal Only use this for unit tests! * @return string[] */ public function getState() { return array_values($this->state); } /** * @param array $input * @return self * @throws SodiumException * * @internal Only for unit tests */ public static function initForUnitTests(array $input) { if (count($input) < 8) { throw new SodiumException('invalid input'); } $state = new self(); for ($i = 0; $i < 8; ++$i) { $state->state[$i] = $input[$i]; } return $state; } /** * @param string $key * @param string $nonce * @return self */ public static function init($key, $nonce) { $state = new self(); // S0 = key ^ nonce $state->state[0] = $key ^ $nonce; // S1 = C1 $state->state[1] = SODIUM_COMPAT_AEGIS_C1; // S2 = C0 $state->state[2] = SODIUM_COMPAT_AEGIS_C0; // S3 = C1 $state->state[3] = SODIUM_COMPAT_AEGIS_C1; // S4 = key ^ nonce $state->state[4] = $key ^ $nonce; // S5 = key ^ C0 $state->state[5] = $key ^ SODIUM_COMPAT_AEGIS_C0; // S6 = key ^ C1 $state->state[6] = $key ^ SODIUM_COMPAT_AEGIS_C1; // S7 = key ^ C0 $state->state[7] = $key ^ SODIUM_COMPAT_AEGIS_C0; // Repeat(10, Update(nonce, key)) for ($i = 0; $i < 10; ++$i) { $state->update($nonce, $key); } return $state; } /** * @param string $ai * @return self */ public function absorb($ai) { if (ParagonIE_Sodium_Core_Util::strlen($ai) !== 32) { throw new SodiumException('Input must be two AES blocks in size'); } $t0 = ParagonIE_Sodium_Core_Util::substr($ai, 0, 16); $t1 = ParagonIE_Sodium_Core_Util::substr($ai, 16, 16); return $this->update($t0, $t1); } /** * @param string $ci * @return string * @throws SodiumException */ public function dec($ci) { if (ParagonIE_Sodium_Core_Util::strlen($ci) !== 32) { throw new SodiumException('Input must be two AES blocks in size'); } // z0 = S6 ^ S1 ^ (S2 & S3) $z0 = $this->state[6] ^ $this->state[1] ^ ParagonIE_Sodium_Core_Util::andStrings($this->state[2], $this->state[3]); // z1 = S2 ^ S5 ^ (S6 & S7) $z1 = $this->state[2] ^ $this->state[5] ^ ParagonIE_Sodium_Core_Util::andStrings($this->state[6], $this->state[7]); // t0, t1 = Split(xi, 128) $t0 = ParagonIE_Sodium_Core_Util::substr($ci, 0, 16); $t1 = ParagonIE_Sodium_Core_Util::substr($ci, 16, 16); // out0 = t0 ^ z0 // out1 = t1 ^ z1 $out0 = $t0 ^ $z0; $out1 = $t1 ^ $z1; // Update(out0, out1) // xi = out0 || out1 $this->update($out0, $out1); return $out0 . $out1; } /** * @param string $cn * @return string */ public function decPartial($cn) { $len = ParagonIE_Sodium_Core_Util::strlen($cn); // z0 = S6 ^ S1 ^ (S2 & S3) $z0 = $this->state[6] ^ $this->state[1] ^ ParagonIE_Sodium_Core_Util::andStrings($this->state[2], $this->state[3]); // z1 = S2 ^ S5 ^ (S6 & S7) $z1 = $this->state[2] ^ $this->state[5] ^ ParagonIE_Sodium_Core_Util::andStrings($this->state[6], $this->state[7]); // t0, t1 = Split(ZeroPad(cn, 256), 128) $cn = str_pad($cn, 32, "\0", STR_PAD_RIGHT); $t0 = ParagonIE_Sodium_Core_Util::substr($cn, 0, 16); $t1 = ParagonIE_Sodium_Core_Util::substr($cn, 16, 16); // out0 = t0 ^ z0 // out1 = t1 ^ z1 $out0 = $t0 ^ $z0; $out1 = $t1 ^ $z1; // xn = Truncate(out0 || out1, |cn|) $xn = ParagonIE_Sodium_Core_Util::substr($out0 . $out1, 0, $len); // v0, v1 = Split(ZeroPad(xn, 256), 128) $padded = str_pad($xn, 32, "\0", STR_PAD_RIGHT); $v0 = ParagonIE_Sodium_Core_Util::substr($padded, 0, 16); $v1 = ParagonIE_Sodium_Core_Util::substr($padded, 16, 16); // Update(v0, v1) $this->update($v0, $v1); // return xn return $xn; } /** * @param string $xi * @return string * @throws SodiumException */ public function enc($xi) { if (ParagonIE_Sodium_Core_Util::strlen($xi) !== 32) { throw new SodiumException('Input must be two AES blocks in size'); } // z0 = S6 ^ S1 ^ (S2 & S3) $z0 = $this->state[6] ^ $this->state[1] ^ ParagonIE_Sodium_Core_Util::andStrings($this->state[2], $this->state[3]); // z1 = S2 ^ S5 ^ (S6 & S7) $z1 = $this->state[2] ^ $this->state[5] ^ ParagonIE_Sodium_Core_Util::andStrings($this->state[6], $this->state[7]); // t0, t1 = Split(xi, 128) $t0 = ParagonIE_Sodium_Core_Util::substr($xi, 0, 16); $t1 = ParagonIE_Sodium_Core_Util::substr($xi, 16, 16); // out0 = t0 ^ z0 // out1 = t1 ^ z1 $out0 = $t0 ^ $z0; $out1 = $t1 ^ $z1; // Update(t0, t1) // ci = out0 || out1 $this->update($t0, $t1); // return ci return $out0 . $out1; } /** * @param int $ad_len_bits * @param int $msg_len_bits * @return string */ public function finalize($ad_len_bits, $msg_len_bits) { $encoded = ParagonIE_Sodium_Core_Util::store64_le($ad_len_bits) . ParagonIE_Sodium_Core_Util::store64_le($msg_len_bits); $t = $this->state[2] ^ $encoded; for ($i = 0; $i < 7; ++$i) { $this->update($t, $t); } return ($this->state[0] ^ $this->state[1] ^ $this->state[2] ^ $this->state[3]) . ($this->state[4] ^ $this->state[5] ^ $this->state[6] ^ $this->state[7]); } /** * @param string $m0 * @param string $m1 * @return self */ public function update($m0, $m1) { /* S'0 = AESRound(S7, S0 ^ M0) S'1 = AESRound(S0, S1) S'2 = AESRound(S1, S2) S'3 = AESRound(S2, S3) S'4 = AESRound(S3, S4 ^ M1) S'5 = AESRound(S4, S5) S'6 = AESRound(S5, S6) S'7 = AESRound(S6, S7) */ list($s_0, $s_1) = ParagonIE_Sodium_Core_AES::doubleRound( $this->state[7], $this->state[0] ^ $m0, $this->state[0], $this->state[1] ); list($s_2, $s_3) = ParagonIE_Sodium_Core_AES::doubleRound( $this->state[1], $this->state[2], $this->state[2], $this->state[3] ); list($s_4, $s_5) = ParagonIE_Sodium_Core_AES::doubleRound( $this->state[3], $this->state[4] ^ $m1, $this->state[4], $this->state[5] ); list($s_6, $s_7) = ParagonIE_Sodium_Core_AES::doubleRound( $this->state[5], $this->state[6], $this->state[6], $this->state[7] ); /* S0 = S'0 S1 = S'1 S2 = S'2 S3 = S'3 S4 = S'4 S5 = S'5 S6 = S'6 S7 = S'7 */ $this->state[0] = $s_0; $this->state[1] = $s_1; $this->state[2] = $s_2; $this->state[3] = $s_3; $this->state[4] = $s_4; $this->state[5] = $s_5; $this->state[6] = $s_6; $this->state[7] = $s_7; return $this; } } AEGIS/State256.php 0000644 00000014575 14720701675 0007447 0 ustar 00 <?php if (class_exists('ParagonIE_Sodium_Core_AEGIS_State256', false)) { return; } if (!defined('SODIUM_COMPAT_AEGIS_C0')) { define('SODIUM_COMPAT_AEGIS_C0', "\x00\x01\x01\x02\x03\x05\x08\x0d\x15\x22\x37\x59\x90\xe9\x79\x62"); } if (!defined('SODIUM_COMPAT_AEGIS_C1')) { define('SODIUM_COMPAT_AEGIS_C1', "\xdb\x3d\x18\x55\x6d\xc2\x2f\xf1\x20\x11\x31\x42\x73\xb5\x28\xdd"); } class ParagonIE_Sodium_Core_AEGIS_State256 { /** @var array<int, string> $state */ protected $state; public function __construct() { $this->state = array_fill(0, 6, ''); } /** * @internal Only use this for unit tests! * @return string[] */ public function getState() { return array_values($this->state); } /** * @param array $input * @return self * @throws SodiumException * * @internal Only for unit tests */ public static function initForUnitTests(array $input) { if (count($input) < 6) { throw new SodiumException('invalid input'); } $state = new self(); for ($i = 0; $i < 6; ++$i) { $state->state[$i] = $input[$i]; } return $state; } /** * @param string $key * @param string $nonce * @return self */ public static function init($key, $nonce) { $state = new self(); $k0 = ParagonIE_Sodium_Core_Util::substr($key, 0, 16); $k1 = ParagonIE_Sodium_Core_Util::substr($key, 16, 16); $n0 = ParagonIE_Sodium_Core_Util::substr($nonce, 0, 16); $n1 = ParagonIE_Sodium_Core_Util::substr($nonce, 16, 16); // S0 = k0 ^ n0 // S1 = k1 ^ n1 // S2 = C1 // S3 = C0 // S4 = k0 ^ C0 // S5 = k1 ^ C1 $k0_n0 = $k0 ^ $n0; $k1_n1 = $k1 ^ $n1; $state->state[0] = $k0_n0; $state->state[1] = $k1_n1; $state->state[2] = SODIUM_COMPAT_AEGIS_C1; $state->state[3] = SODIUM_COMPAT_AEGIS_C0; $state->state[4] = $k0 ^ SODIUM_COMPAT_AEGIS_C0; $state->state[5] = $k1 ^ SODIUM_COMPAT_AEGIS_C1; // Repeat(4, // Update(k0) // Update(k1) // Update(k0 ^ n0) // Update(k1 ^ n1) // ) for ($i = 0; $i < 4; ++$i) { $state->update($k0); $state->update($k1); $state->update($k0 ^ $n0); $state->update($k1 ^ $n1); } return $state; } /** * @param string $ai * @return self * @throws SodiumException */ public function absorb($ai) { if (ParagonIE_Sodium_Core_Util::strlen($ai) !== 16) { throw new SodiumException('Input must be an AES block in size'); } return $this->update($ai); } /** * @param string $ci * @return string * @throws SodiumException */ public function dec($ci) { if (ParagonIE_Sodium_Core_Util::strlen($ci) !== 16) { throw new SodiumException('Input must be an AES block in size'); } // z = S1 ^ S4 ^ S5 ^ (S2 & S3) $z = $this->state[1] ^ $this->state[4] ^ $this->state[5] ^ ParagonIE_Sodium_Core_Util::andStrings($this->state[2], $this->state[3]); $xi = $ci ^ $z; $this->update($xi); return $xi; } /** * @param string $cn * @return string */ public function decPartial($cn) { $len = ParagonIE_Sodium_Core_Util::strlen($cn); // z = S1 ^ S4 ^ S5 ^ (S2 & S3) $z = $this->state[1] ^ $this->state[4] ^ $this->state[5] ^ ParagonIE_Sodium_Core_Util::andStrings($this->state[2], $this->state[3]); // t = ZeroPad(cn, 128) $t = str_pad($cn, 16, "\0", STR_PAD_RIGHT); // out = t ^ z $out = $t ^ $z; // xn = Truncate(out, |cn|) $xn = ParagonIE_Sodium_Core_Util::substr($out, 0, $len); // v = ZeroPad(xn, 128) $v = str_pad($xn, 16, "\0", STR_PAD_RIGHT); // Update(v) $this->update($v); // return xn return $xn; } /** * @param string $xi * @return string * @throws SodiumException */ public function enc($xi) { if (ParagonIE_Sodium_Core_Util::strlen($xi) !== 16) { throw new SodiumException('Input must be an AES block in size'); } // z = S1 ^ S4 ^ S5 ^ (S2 & S3) $z = $this->state[1] ^ $this->state[4] ^ $this->state[5] ^ ParagonIE_Sodium_Core_Util::andStrings($this->state[2], $this->state[3]); $this->update($xi); return $xi ^ $z; } /** * @param int $ad_len_bits * @param int $msg_len_bits * @return string */ public function finalize($ad_len_bits, $msg_len_bits) { $encoded = ParagonIE_Sodium_Core_Util::store64_le($ad_len_bits) . ParagonIE_Sodium_Core_Util::store64_le($msg_len_bits); $t = $this->state[3] ^ $encoded; for ($i = 0; $i < 7; ++$i) { $this->update($t); } return ($this->state[0] ^ $this->state[1] ^ $this->state[2]) . ($this->state[3] ^ $this->state[4] ^ $this->state[5]); } /** * @param string $m * @return self */ public function update($m) { /* S'0 = AESRound(S5, S0 ^ M) S'1 = AESRound(S0, S1) S'2 = AESRound(S1, S2) S'3 = AESRound(S2, S3) S'4 = AESRound(S3, S4) S'5 = AESRound(S4, S5) */ list($s_0, $s_1) = ParagonIE_Sodium_Core_AES::doubleRound( $this->state[5],$this->state[0] ^ $m, $this->state[0], $this->state[1] ); list($s_2, $s_3) = ParagonIE_Sodium_Core_AES::doubleRound( $this->state[1], $this->state[2], $this->state[2], $this->state[3] ); list($s_4, $s_5) = ParagonIE_Sodium_Core_AES::doubleRound( $this->state[3], $this->state[4], $this->state[4], $this->state[5] ); /* S0 = S'0 S1 = S'1 S2 = S'2 S3 = S'3 S4 = S'4 S5 = S'5 */ $this->state[0] = $s_0; $this->state[1] = $s_1; $this->state[2] = $s_2; $this->state[3] = $s_3; $this->state[4] = $s_4; $this->state[5] = $s_5; return $this; } } Ristretto255.php 0000644 00000052574 14720701675 0007536 0 ustar 00 <?php /** * Class ParagonIE_Sodium_Core_Ristretto255 */ class ParagonIE_Sodium_Core_Ristretto255 extends ParagonIE_Sodium_Core_Ed25519 { const crypto_core_ristretto255_HASHBYTES = 64; const HASH_SC_L = 48; const CORE_H2C_SHA256 = 1; const CORE_H2C_SHA512 = 2; /** * @param ParagonIE_Sodium_Core_Curve25519_Fe $f * @param int $b * @return ParagonIE_Sodium_Core_Curve25519_Fe */ public static function fe_cneg(ParagonIE_Sodium_Core_Curve25519_Fe $f, $b) { $negf = self::fe_neg($f); return self::fe_cmov($f, $negf, $b); } /** * @param ParagonIE_Sodium_Core_Curve25519_Fe $f * @return ParagonIE_Sodium_Core_Curve25519_Fe * @throws SodiumException */ public static function fe_abs(ParagonIE_Sodium_Core_Curve25519_Fe $f) { return self::fe_cneg($f, self::fe_isnegative($f)); } /** * Returns 0 if this field element results in all NUL bytes. * * @internal You should not use this directly from another application * * @param ParagonIE_Sodium_Core_Curve25519_Fe $f * @return int * @throws SodiumException */ public static function fe_iszero(ParagonIE_Sodium_Core_Curve25519_Fe $f) { static $zero; if ($zero === null) { $zero = str_repeat("\x00", 32); } /** @var string $zero */ $str = self::fe_tobytes($f); $d = 0; for ($i = 0; $i < 32; ++$i) { $d |= self::chrToInt($str[$i]); } return (($d - 1) >> 31) & 1; } /** * @param ParagonIE_Sodium_Core_Curve25519_Fe $u * @param ParagonIE_Sodium_Core_Curve25519_Fe $v * @return array{x: ParagonIE_Sodium_Core_Curve25519_Fe, nonsquare: int} * * @throws SodiumException */ public static function ristretto255_sqrt_ratio_m1( ParagonIE_Sodium_Core_Curve25519_Fe $u, ParagonIE_Sodium_Core_Curve25519_Fe $v ) { $sqrtm1 = ParagonIE_Sodium_Core_Curve25519_Fe::fromArray(self::$sqrtm1); $v3 = self::fe_mul( self::fe_sq($v), $v ); /* v3 = v^3 */ $x = self::fe_mul( self::fe_mul( self::fe_sq($v3), $u ), $v ); /* x = uv^7 */ $x = self::fe_mul( self::fe_mul( self::fe_pow22523($x), /* x = (uv^7)^((q-5)/8) */ $v3 ), $u ); /* x = uv^3(uv^7)^((q-5)/8) */ $vxx = self::fe_mul( self::fe_sq($x), $v ); /* vx^2 */ $m_root_check = self::fe_sub($vxx, $u); /* vx^2-u */ $p_root_check = self::fe_add($vxx, $u); /* vx^2+u */ $f_root_check = self::fe_mul($u, $sqrtm1); /* u*sqrt(-1) */ $f_root_check = self::fe_add($vxx, $f_root_check); /* vx^2+u*sqrt(-1) */ $has_m_root = self::fe_iszero($m_root_check); $has_p_root = self::fe_iszero($p_root_check); $has_f_root = self::fe_iszero($f_root_check); $x_sqrtm1 = self::fe_mul($x, $sqrtm1); /* x*sqrt(-1) */ $x = self::fe_abs( self::fe_cmov($x, $x_sqrtm1, $has_p_root | $has_f_root) ); return array( 'x' => $x, 'nonsquare' => $has_m_root | $has_p_root ); } /** * @param string $s * @return int * @throws SodiumException */ public static function ristretto255_point_is_canonical($s) { $c = (self::chrToInt($s[31]) & 0x7f) ^ 0x7f; for ($i = 30; $i > 0; --$i) { $c |= self::chrToInt($s[$i]) ^ 0xff; } $c = ($c - 1) >> 8; $d = (0xed - 1 - self::chrToInt($s[0])) >> 8; $e = self::chrToInt($s[31]) >> 7; return 1 - ((($c & $d) | $e | self::chrToInt($s[0])) & 1); } /** * @param string $s * @param bool $skipCanonicalCheck * @return array{h: ParagonIE_Sodium_Core_Curve25519_Ge_P3, res: int} * @throws SodiumException */ public static function ristretto255_frombytes($s, $skipCanonicalCheck = false) { if (!$skipCanonicalCheck) { if (!self::ristretto255_point_is_canonical($s)) { throw new SodiumException('S is not canonical'); } } $s_ = self::fe_frombytes($s); $ss = self::fe_sq($s_); /* ss = s^2 */ $u1 = self::fe_sub(self::fe_1(), $ss); /* u1 = 1-ss */ $u1u1 = self::fe_sq($u1); /* u1u1 = u1^2 */ $u2 = self::fe_add(self::fe_1(), $ss); /* u2 = 1+ss */ $u2u2 = self::fe_sq($u2); /* u2u2 = u2^2 */ $v = self::fe_mul( ParagonIE_Sodium_Core_Curve25519_Fe::fromArray(self::$d), $u1u1 ); /* v = d*u1^2 */ $v = self::fe_neg($v); /* v = -d*u1^2 */ $v = self::fe_sub($v, $u2u2); /* v = -(d*u1^2)-u2^2 */ $v_u2u2 = self::fe_mul($v, $u2u2); /* v_u2u2 = v*u2^2 */ // fe25519_1(one); // notsquare = ristretto255_sqrt_ratio_m1(inv_sqrt, one, v_u2u2); $one = self::fe_1(); $result = self::ristretto255_sqrt_ratio_m1($one, $v_u2u2); $inv_sqrt = $result['x']; $notsquare = $result['nonsquare']; $h = new ParagonIE_Sodium_Core_Curve25519_Ge_P3(); $h->X = self::fe_mul($inv_sqrt, $u2); $h->Y = self::fe_mul(self::fe_mul($inv_sqrt, $h->X), $v); $h->X = self::fe_mul($h->X, $s_); $h->X = self::fe_abs( self::fe_add($h->X, $h->X) ); $h->Y = self::fe_mul($u1, $h->Y); $h->Z = self::fe_1(); $h->T = self::fe_mul($h->X, $h->Y); $res = - ((1 - $notsquare) | self::fe_isnegative($h->T) | self::fe_iszero($h->Y)); return array('h' => $h, 'res' => $res); } /** * @param ParagonIE_Sodium_Core_Curve25519_Ge_P3 $h * @return string * @throws SodiumException */ public static function ristretto255_p3_tobytes(ParagonIE_Sodium_Core_Curve25519_Ge_P3 $h) { $sqrtm1 = ParagonIE_Sodium_Core_Curve25519_Fe::fromArray(self::$sqrtm1); $invsqrtamd = ParagonIE_Sodium_Core_Curve25519_Fe::fromArray(self::$invsqrtamd); $u1 = self::fe_add($h->Z, $h->Y); /* u1 = Z+Y */ $zmy = self::fe_sub($h->Z, $h->Y); /* zmy = Z-Y */ $u1 = self::fe_mul($u1, $zmy); /* u1 = (Z+Y)*(Z-Y) */ $u2 = self::fe_mul($h->X, $h->Y); /* u2 = X*Y */ $u1_u2u2 = self::fe_mul(self::fe_sq($u2), $u1); /* u1_u2u2 = u1*u2^2 */ $one = self::fe_1(); // fe25519_1(one); // (void) ristretto255_sqrt_ratio_m1(inv_sqrt, one, u1_u2u2); $result = self::ristretto255_sqrt_ratio_m1($one, $u1_u2u2); $inv_sqrt = $result['x']; $den1 = self::fe_mul($inv_sqrt, $u1); /* den1 = inv_sqrt*u1 */ $den2 = self::fe_mul($inv_sqrt, $u2); /* den2 = inv_sqrt*u2 */ $z_inv = self::fe_mul($h->T, self::fe_mul($den1, $den2)); /* z_inv = den1*den2*T */ $ix = self::fe_mul($h->X, $sqrtm1); /* ix = X*sqrt(-1) */ $iy = self::fe_mul($h->Y, $sqrtm1); /* iy = Y*sqrt(-1) */ $eden = self::fe_mul($den1, $invsqrtamd); $t_z_inv = self::fe_mul($h->T, $z_inv); /* t_z_inv = T*z_inv */ $rotate = self::fe_isnegative($t_z_inv); $x_ = self::fe_copy($h->X); $y_ = self::fe_copy($h->Y); $den_inv = self::fe_copy($den2); $x_ = self::fe_cmov($x_, $iy, $rotate); $y_ = self::fe_cmov($y_, $ix, $rotate); $den_inv = self::fe_cmov($den_inv, $eden, $rotate); $x_z_inv = self::fe_mul($x_, $z_inv); $y_ = self::fe_cneg($y_, self::fe_isnegative($x_z_inv)); // fe25519_sub(s_, h->Z, y_); // fe25519_mul(s_, den_inv, s_); // fe25519_abs(s_, s_); // fe25519_tobytes(s, s_); return self::fe_tobytes( self::fe_abs( self::fe_mul( $den_inv, self::fe_sub($h->Z, $y_) ) ) ); } /** * @param ParagonIE_Sodium_Core_Curve25519_Fe $t * @return ParagonIE_Sodium_Core_Curve25519_Ge_P3 * * @throws SodiumException */ public static function ristretto255_elligator(ParagonIE_Sodium_Core_Curve25519_Fe $t) { $sqrtm1 = ParagonIE_Sodium_Core_Curve25519_Fe::fromArray(self::$sqrtm1); $onemsqd = ParagonIE_Sodium_Core_Curve25519_Fe::fromArray(self::$onemsqd); $d = ParagonIE_Sodium_Core_Curve25519_Fe::fromArray(self::$d); $sqdmone = ParagonIE_Sodium_Core_Curve25519_Fe::fromArray(self::$sqdmone); $sqrtadm1 = ParagonIE_Sodium_Core_Curve25519_Fe::fromArray(self::$sqrtadm1); $one = self::fe_1(); $r = self::fe_mul($sqrtm1, self::fe_sq($t)); /* r = sqrt(-1)*t^2 */ $u = self::fe_mul(self::fe_add($r, $one), $onemsqd); /* u = (r+1)*(1-d^2) */ $c = self::fe_neg(self::fe_1()); /* c = -1 */ $rpd = self::fe_add($r, $d); /* rpd = r+d */ $v = self::fe_mul( self::fe_sub( $c, self::fe_mul($r, $d) ), $rpd ); /* v = (c-r*d)*(r+d) */ $result = self::ristretto255_sqrt_ratio_m1($u, $v); $s = $result['x']; $wasnt_square = 1 - $result['nonsquare']; $s_prime = self::fe_neg( self::fe_abs( self::fe_mul($s, $t) ) ); /* s_prime = -|s*t| */ $s = self::fe_cmov($s, $s_prime, $wasnt_square); $c = self::fe_cmov($c, $r, $wasnt_square); // fe25519_sub(n, r, one); /* n = r-1 */ // fe25519_mul(n, n, c); /* n = c*(r-1) */ // fe25519_mul(n, n, ed25519_sqdmone); /* n = c*(r-1)*(d-1)^2 */ // fe25519_sub(n, n, v); /* n = c*(r-1)*(d-1)^2-v */ $n = self::fe_sub( self::fe_mul( self::fe_mul( self::fe_sub($r, $one), $c ), $sqdmone ), $v ); /* n = c*(r-1)*(d-1)^2-v */ $w0 = self::fe_mul( self::fe_add($s, $s), $v ); /* w0 = 2s*v */ $w1 = self::fe_mul($n, $sqrtadm1); /* w1 = n*sqrt(ad-1) */ $ss = self::fe_sq($s); /* ss = s^2 */ $w2 = self::fe_sub($one, $ss); /* w2 = 1-s^2 */ $w3 = self::fe_add($one, $ss); /* w3 = 1+s^2 */ return new ParagonIE_Sodium_Core_Curve25519_Ge_P3( self::fe_mul($w0, $w3), self::fe_mul($w2, $w1), self::fe_mul($w1, $w3), self::fe_mul($w0, $w2) ); } /** * @param string $h * @return string * @throws SodiumException */ public static function ristretto255_from_hash($h) { if (self::strlen($h) !== 64) { throw new SodiumException('Hash must be 64 bytes'); } //fe25519_frombytes(r0, h); //fe25519_frombytes(r1, h + 32); $r0 = self::fe_frombytes(self::substr($h, 0, 32)); $r1 = self::fe_frombytes(self::substr($h, 32, 32)); //ristretto255_elligator(&p0, r0); //ristretto255_elligator(&p1, r1); $p0 = self::ristretto255_elligator($r0); $p1 = self::ristretto255_elligator($r1); //ge25519_p3_to_cached(&p1_cached, &p1); //ge25519_add_cached(&p_p1p1, &p0, &p1_cached); $p_p1p1 = self::ge_add( $p0, self::ge_p3_to_cached($p1) ); //ge25519_p1p1_to_p3(&p, &p_p1p1); //ristretto255_p3_tobytes(s, &p); return self::ristretto255_p3_tobytes( self::ge_p1p1_to_p3($p_p1p1) ); } /** * @param string $p * @return int * @throws SodiumException */ public static function is_valid_point($p) { $result = self::ristretto255_frombytes($p); if ($result['res'] !== 0) { return 0; } return 1; } /** * @param string $p * @param string $q * @return string * @throws SodiumException */ public static function ristretto255_add($p, $q) { $p_res = self::ristretto255_frombytes($p); $q_res = self::ristretto255_frombytes($q); if ($p_res['res'] !== 0 || $q_res['res'] !== 0) { throw new SodiumException('Could not add points'); } $p_p3 = $p_res['h']; $q_p3 = $q_res['h']; $q_cached = self::ge_p3_to_cached($q_p3); $r_p1p1 = self::ge_add($p_p3, $q_cached); $r_p3 = self::ge_p1p1_to_p3($r_p1p1); return self::ristretto255_p3_tobytes($r_p3); } /** * @param string $p * @param string $q * @return string * @throws SodiumException */ public static function ristretto255_sub($p, $q) { $p_res = self::ristretto255_frombytes($p); $q_res = self::ristretto255_frombytes($q); if ($p_res['res'] !== 0 || $q_res['res'] !== 0) { throw new SodiumException('Could not add points'); } $p_p3 = $p_res['h']; $q_p3 = $q_res['h']; $q_cached = self::ge_p3_to_cached($q_p3); $r_p1p1 = self::ge_sub($p_p3, $q_cached); $r_p3 = self::ge_p1p1_to_p3($r_p1p1); return self::ristretto255_p3_tobytes($r_p3); } /** * @param int $hLen * @param ?string $ctx * @param string $msg * @return string * @throws SodiumException * @psalm-suppress PossiblyInvalidArgument hash API */ protected static function h2c_string_to_hash_sha256($hLen, $ctx, $msg) { $h = array_fill(0, $hLen, 0); $ctx_len = !is_null($ctx) ? self::strlen($ctx) : 0; if ($hLen > 0xff) { throw new SodiumException('Hash must be less than 256 bytes'); } if ($ctx_len > 0xff) { $st = hash_init('sha256'); self::hash_update($st, "H2C-OVERSIZE-DST-"); self::hash_update($st, $ctx); $ctx = hash_final($st, true); $ctx_len = 32; } $t = array(0, $hLen, 0); $ux = str_repeat("\0", 64); $st = hash_init('sha256'); self::hash_update($st, $ux); self::hash_update($st, $msg); self::hash_update($st, self::intArrayToString($t)); self::hash_update($st, $ctx); self::hash_update($st, self::intToChr($ctx_len)); $u0 = hash_final($st, true); for ($i = 0; $i < $hLen; $i += 64) { $ux = self::xorStrings($ux, $u0); ++$t[2]; $st = hash_init('sha256'); self::hash_update($st, $ux); self::hash_update($st, self::intToChr($t[2])); self::hash_update($st, $ctx); self::hash_update($st, self::intToChr($ctx_len)); $ux = hash_final($st, true); $amount = min($hLen - $i, 64); for ($j = 0; $j < $amount; ++$j) { $h[$i + $j] = self::chrToInt($ux[$i]); } } return self::intArrayToString(array_slice($h, 0, $hLen)); } /** * @param int $hLen * @param ?string $ctx * @param string $msg * @return string * @throws SodiumException * @psalm-suppress PossiblyInvalidArgument hash API */ protected static function h2c_string_to_hash_sha512($hLen, $ctx, $msg) { $h = array_fill(0, $hLen, 0); $ctx_len = !is_null($ctx) ? self::strlen($ctx) : 0; if ($hLen > 0xff) { throw new SodiumException('Hash must be less than 256 bytes'); } if ($ctx_len > 0xff) { $st = hash_init('sha256'); self::hash_update($st, "H2C-OVERSIZE-DST-"); self::hash_update($st, $ctx); $ctx = hash_final($st, true); $ctx_len = 32; } $t = array(0, $hLen, 0); $ux = str_repeat("\0", 128); $st = hash_init('sha512'); self::hash_update($st, $ux); self::hash_update($st, $msg); self::hash_update($st, self::intArrayToString($t)); self::hash_update($st, $ctx); self::hash_update($st, self::intToChr($ctx_len)); $u0 = hash_final($st, true); for ($i = 0; $i < $hLen; $i += 128) { $ux = self::xorStrings($ux, $u0); ++$t[2]; $st = hash_init('sha512'); self::hash_update($st, $ux); self::hash_update($st, self::intToChr($t[2])); self::hash_update($st, $ctx); self::hash_update($st, self::intToChr($ctx_len)); $ux = hash_final($st, true); $amount = min($hLen - $i, 128); for ($j = 0; $j < $amount; ++$j) { $h[$i + $j] = self::chrToInt($ux[$i]); } } return self::intArrayToString(array_slice($h, 0, $hLen)); } /** * @param int $hLen * @param ?string $ctx * @param string $msg * @param int $hash_alg * @return string * @throws SodiumException */ public static function h2c_string_to_hash($hLen, $ctx, $msg, $hash_alg) { switch ($hash_alg) { case self::CORE_H2C_SHA256: return self::h2c_string_to_hash_sha256($hLen, $ctx, $msg); case self::CORE_H2C_SHA512: return self::h2c_string_to_hash_sha512($hLen, $ctx, $msg); default: throw new SodiumException('Invalid H2C hash algorithm'); } } /** * @param ?string $ctx * @param string $msg * @param int $hash_alg * @return string * @throws SodiumException */ protected static function _string_to_element($ctx, $msg, $hash_alg) { return self::ristretto255_from_hash( self::h2c_string_to_hash(self::crypto_core_ristretto255_HASHBYTES, $ctx, $msg, $hash_alg) ); } /** * @return string * @throws SodiumException * @throws Exception */ public static function ristretto255_random() { return self::ristretto255_from_hash( ParagonIE_Sodium_Compat::randombytes_buf(self::crypto_core_ristretto255_HASHBYTES) ); } /** * @return string * @throws SodiumException */ public static function ristretto255_scalar_random() { return self::scalar_random(); } /** * @param string $s * @return string * @throws SodiumException */ public static function ristretto255_scalar_complement($s) { return self::scalar_complement($s); } /** * @param string $s * @return string */ public static function ristretto255_scalar_invert($s) { return self::sc25519_invert($s); } /** * @param string $s * @return string * @throws SodiumException */ public static function ristretto255_scalar_negate($s) { return self::scalar_negate($s); } /** * @param string $x * @param string $y * @return string */ public static function ristretto255_scalar_add($x, $y) { return self::scalar_add($x, $y); } /** * @param string $x * @param string $y * @return string */ public static function ristretto255_scalar_sub($x, $y) { return self::scalar_sub($x, $y); } /** * @param string $x * @param string $y * @return string */ public static function ristretto255_scalar_mul($x, $y) { return self::sc25519_mul($x, $y); } /** * @param string $ctx * @param string $msg * @param int $hash_alg * @return string * @throws SodiumException */ public static function ristretto255_scalar_from_string($ctx, $msg, $hash_alg) { $h = array_fill(0, 64, 0); $h_be = self::stringToIntArray( self::h2c_string_to_hash( self::HASH_SC_L, $ctx, $msg, $hash_alg ) ); for ($i = 0; $i < self::HASH_SC_L; ++$i) { $h[$i] = $h_be[self::HASH_SC_L - 1 - $i]; } return self::ristretto255_scalar_reduce(self::intArrayToString($h)); } /** * @param string $s * @return string */ public static function ristretto255_scalar_reduce($s) { return self::sc_reduce($s); } /** * @param string $n * @param string $p * @return string * @throws SodiumException */ public static function scalarmult_ristretto255($n, $p) { if (self::strlen($n) !== 32) { throw new SodiumException('Scalar must be 32 bytes, ' . self::strlen($p) . ' given.'); } if (self::strlen($p) !== 32) { throw new SodiumException('Point must be 32 bytes, ' . self::strlen($p) . ' given.'); } $result = self::ristretto255_frombytes($p); if ($result['res'] !== 0) { throw new SodiumException('Could not multiply points'); } $P = $result['h']; $t = self::stringToIntArray($n); $t[31] &= 0x7f; $Q = self::ge_scalarmult(self::intArrayToString($t), $P); $q = self::ristretto255_p3_tobytes($Q); if (ParagonIE_Sodium_Compat::is_zero($q)) { throw new SodiumException('An unknown error has occurred'); } return $q; } /** * @param string $n * @return string * @throws SodiumException */ public static function scalarmult_ristretto255_base($n) { $t = self::stringToIntArray($n); $t[31] &= 0x7f; $Q = self::ge_scalarmult_base(self::intArrayToString($t)); $q = self::ristretto255_p3_tobytes($Q); if (ParagonIE_Sodium_Compat::is_zero($q)) { throw new SodiumException('An unknown error has occurred'); } return $q; } } Poly1305.php 0000644 00000000144 14720701675 0006521 0 ustar 00 <?php namespace ParagonIE\Sodium\Core; class Poly1305 extends \ParagonIE_Sodium_Core_Poly1305 { } SecretStream/State.php 0000644 00000007050 14720701675 0010751 0 ustar 00 <?php /** * Class ParagonIE_Sodium_Core_SecretStream_State */ class ParagonIE_Sodium_Core_SecretStream_State { /** @var string $key */ protected $key; /** @var int $counter */ protected $counter; /** @var string $nonce */ protected $nonce; /** @var string $_pad */ protected $_pad; /** * ParagonIE_Sodium_Core_SecretStream_State constructor. * @param string $key * @param string|null $nonce */ public function __construct($key, $nonce = null) { $this->key = $key; $this->counter = 1; if (is_null($nonce)) { $nonce = str_repeat("\0", 12); } $this->nonce = str_pad($nonce, 12, "\0", STR_PAD_RIGHT);; $this->_pad = str_repeat("\0", 4); } /** * @return self */ public function counterReset() { $this->counter = 1; $this->_pad = str_repeat("\0", 4); return $this; } /** * @return string */ public function getKey() { return $this->key; } /** * @return string */ public function getCounter() { return ParagonIE_Sodium_Core_Util::store32_le($this->counter); } /** * @return string */ public function getNonce() { if (!is_string($this->nonce)) { $this->nonce = str_repeat("\0", 12); } if (ParagonIE_Sodium_Core_Util::strlen($this->nonce) !== 12) { $this->nonce = str_pad($this->nonce, 12, "\0", STR_PAD_RIGHT); } return $this->nonce; } /** * @return string */ public function getCombinedNonce() { return $this->getCounter() . ParagonIE_Sodium_Core_Util::substr($this->getNonce(), 0, 8); } /** * @return self */ public function incrementCounter() { ++$this->counter; return $this; } /** * @return bool */ public function needsRekey() { return ($this->counter & 0xffff) === 0; } /** * @param string $newKeyAndNonce * @return self */ public function rekey($newKeyAndNonce) { $this->key = ParagonIE_Sodium_Core_Util::substr($newKeyAndNonce, 0, 32); $this->nonce = str_pad( ParagonIE_Sodium_Core_Util::substr($newKeyAndNonce, 32), 12, "\0", STR_PAD_RIGHT ); return $this; } /** * @param string $str * @return self */ public function xorNonce($str) { $this->nonce = ParagonIE_Sodium_Core_Util::xorStrings( $this->getNonce(), str_pad( ParagonIE_Sodium_Core_Util::substr($str, 0, 8), 12, "\0", STR_PAD_RIGHT ) ); return $this; } /** * @param string $string * @return self */ public static function fromString($string) { $state = new ParagonIE_Sodium_Core_SecretStream_State( ParagonIE_Sodium_Core_Util::substr($string, 0, 32) ); $state->counter = ParagonIE_Sodium_Core_Util::load_4( ParagonIE_Sodium_Core_Util::substr($string, 32, 4) ); $state->nonce = ParagonIE_Sodium_Core_Util::substr($string, 36, 12); $state->_pad = ParagonIE_Sodium_Core_Util::substr($string, 48, 8); return $state; } /** * @return string */ public function toString() { return $this->key . $this->getCounter() . $this->nonce . $this->_pad; } } X25519.php 0000644 00000000140 14720701675 0006076 0 ustar 00 <?php namespace ParagonIE\Sodium\Core; class X25519 extends \ParagonIE_Sodium_Core_X25519 { } XChaCha20.php 0000644 00000000146 14720701675 0006670 0 ustar 00 <?php namespace ParagonIE\Sodium\Core; class XChaCha20 extends \ParagonIE_Sodium_Core_XChaCha20 { } Curve25519.php 0000644 00000000150 14720701675 0006754 0 ustar 00 <?php namespace ParagonIE\Sodium\Core; class Curve25519 extends \ParagonIE_Sodium_Core_Curve25519 { } Xsalsa20.php 0000644 00000000144 14720707577 0006671 0 ustar 00 <?php namespace ParagonIE\Sodium\Core; class Xsalsa20 extends \ParagonIE_Sodium_Core_XSalsa20 { } error_log 0000644 00000001717 14720715700 0006472 0 ustar 00 [24-Nov-2024 21:16:51 UTC] PHP Fatal error: Uncaught Error: Class "ParagonIE_Sodium_Core_Util" not found in /home/wwgoat/public_html/blog/wp-includes/sodium_compat/namespaced/Core/Util.php:4 Stack trace: #0 {main} thrown in /home/wwgoat/public_html/blog/wp-includes/sodium_compat/namespaced/Core/Util.php on line 4 [24-Nov-2024 21:33:30 UTC] PHP Fatal error: Uncaught Error: Class "ParagonIE_Sodium_Core_Ed25519" not found in /home/wwgoat/public_html/blog/wp-includes/sodium_compat/namespaced/Core/Ed25519.php:4 Stack trace: #0 {main} thrown in /home/wwgoat/public_html/blog/wp-includes/sodium_compat/namespaced/Core/Ed25519.php on line 4 [24-Nov-2024 21:33:45 UTC] PHP Fatal error: Uncaught Error: Class "ParagonIE_Sodium_Core_XSalsa20" not found in /home/wwgoat/public_html/blog/wp-includes/sodium_compat/namespaced/Core/Xsalsa20.php:4 Stack trace: #0 {main} thrown in /home/wwgoat/public_html/blog/wp-includes/sodium_compat/namespaced/Core/Xsalsa20.php on line 4 TelemetryCollector.h 0000644 00000046721 14756456557 0010600 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2018 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef _PASSENGER_TELEMETRY_COLLECTOR_H_ #define _PASSENGER_TELEMETRY_COLLECTOR_H_ #include <string> #include <vector> #include <limits> #include <cstddef> #include <cstdlib> #include <cassert> #include <boost/cstdint.hpp> #include <boost/bind/bind.hpp> #include <oxt/thread.hpp> #include <oxt/backtrace.hpp> #include <curl/curl.h> #include <Constants.h> #include <Exceptions.h> #include <Core/Controller.h> #include <LoggingKit/LoggingKit.h> #include <ConfigKit/ConfigKit.h> #include <Utils/Curl.h> #include <StrIntTools/StrIntUtils.h> namespace Passenger { namespace Core { using namespace std; class TelemetryCollector { public: /* * BEGIN ConfigKit schema: Passenger::Core::TelemetryCollector::Schema * (do not edit: following text is automatically generated * by 'rake configkit_schemas_inline_comments') * * ca_certificate_path string - - * debug_curl boolean - default(false) * disabled boolean - default(false) * final_run_timeout unsigned integer - default(5) * first_interval unsigned integer - default(7200) * interval unsigned integer - default(21600) * interval_jitter unsigned integer - default(7200) * proxy_url string - - * timeout unsigned integer - default(180) * url string - default("https://anontelemetry.phusionpassenger.com/v1/collect.json") * verify_server boolean - default(true) * * END */ class Schema: public ConfigKit::Schema { private: static void validateProxyUrl(const ConfigKit::Store &config, vector<ConfigKit::Error> &errors) { if (config["proxy_url"].isNull()) { return; } if (config["proxy_url"].asString().empty()) { errors.push_back(ConfigKit::Error("'{{proxy_url}}', if specified, may not be empty")); return; } try { prepareCurlProxy(config["proxy_url"].asString()); } catch (const ArgumentException &e) { errors.push_back(ConfigKit::Error( P_STATIC_STRING("'{{proxy_url}}': ") + e.what())); } } public: Schema() { using namespace ConfigKit; add("disabled", BOOL_TYPE, OPTIONAL, false); add("url", STRING_TYPE, OPTIONAL, "https://anontelemetry.phusionpassenger.com/v1/collect.json"); // Should be in the form: scheme://user:password@proxy_host:proxy_port add("proxy_url", STRING_TYPE, OPTIONAL); add("ca_certificate_path", STRING_TYPE, OPTIONAL); add("verify_server", BOOL_TYPE, OPTIONAL, true); add("first_interval", UINT_TYPE, OPTIONAL, 2 * 60 * 60); add("interval", UINT_TYPE, OPTIONAL, 6 * 60 * 60); add("interval_jitter", UINT_TYPE, OPTIONAL, 2 * 60 * 60); add("debug_curl", BOOL_TYPE, OPTIONAL, false); add("timeout", UINT_TYPE, OPTIONAL, 180); add("final_run_timeout", UINT_TYPE, OPTIONAL, 5); addValidator(validateProxyUrl); finalize(); } }; struct ConfigRealization { CurlProxyInfo proxyInfo; string url; string caCertificatePath; ConfigRealization(const ConfigKit::Store &config) : proxyInfo(prepareCurlProxy(config["proxy_url"].asString())), url(config["url"].asString()), caCertificatePath(config["ca_certificate_path"].asString()) { } void swap(ConfigRealization &other) BOOST_NOEXCEPT_OR_NOTHROW { proxyInfo.swap(other.proxyInfo); url.swap(other.url); caCertificatePath.swap(other.caCertificatePath); } }; struct ConfigChangeRequest { boost::scoped_ptr<ConfigKit::Store> config; boost::scoped_ptr<ConfigRealization> configRlz; }; struct TelemetryData { vector<boost::uint64_t> requestsHandled; MonotonicTimeUsec timestamp; }; private: /* * Since the telemetry collector runs in a separate thread, * and the configuration can change while the collector is active, * we make a copy of the current configuration at the beginning * of each collection cycle. */ struct SessionState { ConfigKit::Store config; ConfigRealization configRlz; SessionState(const ConfigKit::Store ¤tConfig, const ConfigRealization ¤tConfigRlz) : config(currentConfig), configRlz(currentConfigRlz) { } }; mutable boost::mutex configSyncher; ConfigKit::Store config; ConfigRealization configRlz; TelemetryData lastTelemetryData; oxt::thread *collectorThread; void threadMain() { TRACE_POINT(); { // Sleep for a short while to allow interruption during the Apache integration // double startup procedure, this prevents running the update check twice boost::unique_lock<boost::mutex> l(configSyncher); ConfigKit::Store config(this->config); l.unlock(); unsigned int backoffSec = config["first_interval"].asUInt() + calculateIntervalJitter(config); P_DEBUG("Next anonymous telemetry collection in " << distanceOfTimeInWords(SystemTime::get() + backoffSec)); boost::this_thread::sleep_for(boost::chrono::seconds(backoffSec)); } while (!boost::this_thread::interruption_requested()) { UPDATE_TRACE_POINT(); unsigned int backoffSec = 0; try { backoffSec = runOneCycle(); } catch (const oxt::tracable_exception &e) { P_ERROR(e.what() << "\n" << e.backtrace()); } if (backoffSec == 0) { boost::unique_lock<boost::mutex> l(configSyncher); backoffSec = config["interval"].asUInt() + calculateIntervalJitter(config); } UPDATE_TRACE_POINT(); P_DEBUG("Next anonymous telemetry collection in " << distanceOfTimeInWords(SystemTime::get() + backoffSec)); boost::this_thread::sleep_for(boost::chrono::seconds(backoffSec)); } } static unsigned int calculateIntervalJitter(const ConfigKit::Store &config) { unsigned int jitter = config["interval_jitter"].asUInt(); if (jitter == 0) { return 0; } else { return std::rand() % jitter; } } // Virtual to allow mocking in unit tests. virtual TelemetryData collectTelemetryData(bool isFinalRun) const { TRACE_POINT(); TelemetryData tmData; unsigned int counter = 0; boost::mutex syncher; boost::condition_variable cond; tmData.requestsHandled.resize(controllers.size(), 0); UPDATE_TRACE_POINT(); for (unsigned int i = 0; i < controllers.size(); i++) { if (isFinalRun) { inspectController(&tmData, controllers[i], i, &counter, &syncher, &cond); } else { controllers[i]->getContext()->libev->runLater(boost::bind( &TelemetryCollector::inspectController, this, &tmData, controllers[i], i, &counter, &syncher, &cond)); } } UPDATE_TRACE_POINT(); { boost::unique_lock<boost::mutex> l(syncher); while (counter != controllers.size()) { cond.wait(l); } } tmData.timestamp = SystemTime::getMonotonicUsecWithGranularity <SystemTime::GRAN_1SEC>(); return tmData; } void inspectController(TelemetryData *tmData, Controller *controller, unsigned int index, unsigned int *counter, boost::mutex *syncher, boost::condition_variable *cond) const { boost::unique_lock<boost::mutex> l(*syncher); tmData->requestsHandled[index] = controller->totalRequestsBegun; (*counter)++; cond->notify_one(); } string createRequestBody(const TelemetryData &tmData) const { Json::Value doc; boost::uint64_t totalRequestsHandled = 0; P_ASSERT_EQ(tmData.requestsHandled.size(), lastTelemetryData.requestsHandled.size()); for (unsigned int i = 0; i < tmData.requestsHandled.size(); i++) { if (tmData.requestsHandled[i] >= lastTelemetryData.requestsHandled[i]) { totalRequestsHandled += tmData.requestsHandled[i] - lastTelemetryData.requestsHandled[i]; } else { // Counter overflowed totalRequestsHandled += std::numeric_limits<boost::uint64_t>::max() - lastTelemetryData.requestsHandled[i] + 1 + tmData.requestsHandled[i]; } } doc["requests_handled"] = (Json::UInt64) totalRequestsHandled; doc["begin_time"] = (Json::UInt64) monoTimeToRealTime( lastTelemetryData.timestamp); doc["end_time"] = (Json::UInt64) monoTimeToRealTime( tmData.timestamp); doc["version"] = PASSENGER_VERSION; #ifdef PASSENGER_IS_ENTERPRISE doc["edition"] = "enterprise"; #else doc["edition"] = "oss"; #endif return doc.toStyledString(); } static time_t monoTimeToRealTime(MonotonicTimeUsec monoTime) { MonotonicTimeUsec monoNow = SystemTime::getMonotonicUsecWithGranularity <SystemTime::GRAN_1SEC>(); unsigned long long realNow = SystemTime::getUsec(); MonotonicTimeUsec diff; if (monoNow >= monoTime) { diff = monoNow - monoTime; return (realNow - diff) / 1000000; } else { diff = monoTime - monoNow; return (realNow + diff) / 1000000; } } static CURL *prepareCurlRequest(SessionState &sessionState, bool isFinalRun, struct curl_slist **headers, char *lastErrorMessage, const string &requestBody, string &responseData) { CURL *curl; CURLcode code; curl = curl_easy_init(); if (curl == NULL) { P_ERROR("Error initializing libcurl"); return NULL; } code = curl_easy_setopt(curl, CURLOPT_VERBOSE, sessionState.config["debug_curl"].asBool() ? 1L : 0L); if (code != CURLE_OK) { goto error; } code = setCurlDefaultCaInfo(curl); if (code != CURLE_OK) { goto error; } code = setCurlProxy(curl, sessionState.configRlz.proxyInfo); if (code != CURLE_OK) { goto error; } code = curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1); if (code != CURLE_OK) { goto error; } code = curl_easy_setopt(curl, CURLOPT_URL, sessionState.configRlz.url.c_str()); if (code != CURLE_OK) { goto error; } code = curl_easy_setopt(curl, CURLOPT_HTTPGET, 0); if (code != CURLE_OK) { goto error; } code = curl_easy_setopt(curl, CURLOPT_POSTFIELDS, requestBody.c_str()); if (code != CURLE_OK) { goto error; } code = curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, requestBody.length()); if (code != CURLE_OK) { goto error; } *headers = curl_slist_append(NULL, "Content-Type: application/json"); code = curl_easy_setopt(curl, CURLOPT_HTTPHEADER, *headers); if (code != CURLE_OK) { goto error; } if (!sessionState.configRlz.caCertificatePath.empty()) { code = curl_easy_setopt(curl, CURLOPT_CAINFO, sessionState.configRlz.caCertificatePath.c_str()); if (code != CURLE_OK) { goto error; } } if (sessionState.config["verify_server"].asBool()) { // These should be on by default, but make sure. code = curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); if (code != CURLE_OK) { goto error; } code = curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); if (code != CURLE_OK) { goto error; } } else { code = curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); if (code != CURLE_OK) { goto error; } code = curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); if (code != CURLE_OK) { goto error; } } code = curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, lastErrorMessage); if (code != CURLE_OK) { goto error; } code = curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, receiveResponseBytes); if (code != CURLE_OK) { goto error; } code = curl_easy_setopt(curl, CURLOPT_WRITEDATA, &responseData); if (code != CURLE_OK) { goto error; } // setopt failure(s) below don't abort the check. if (isFinalRun) { curl_easy_setopt(curl, CURLOPT_TIMEOUT, sessionState.config["final_run_timeout"].asUInt()); } else { curl_easy_setopt(curl, CURLOPT_TIMEOUT, sessionState.config["timeout"].asUInt()); } return curl; error: curl_easy_cleanup(curl); curl_slist_free_all(*headers); P_ERROR("Error setting libcurl handle parameters: " << curl_easy_strerror(code)); return NULL; } static size_t receiveResponseBytes(void *buffer, size_t size, size_t nmemb, void *userData) { string *responseData = (string *) userData; responseData->append((const char *) buffer, size * nmemb); return size * nmemb; } // Virtual to allow mocking in unit tests. virtual CURLcode performCurlAction(CURL *curl, const char *lastErrorMessage, const string &_requestBody, // only used by unit tests string &_responseData, // only used by unit tests long &responseCode) { TRACE_POINT(); CURLcode code = curl_easy_perform(curl); if (code != CURLE_OK) { P_ERROR("Error contacting anonymous telemetry server: " << lastErrorMessage); return code; } code = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &responseCode); if (code != CURLE_OK) { P_ERROR("Error querying libcurl handle for HTTP response code: " << curl_easy_strerror(code)); return code; } return CURLE_OK; } static bool responseCodeSupported(long code) { return code == 200 || code == 400 || code == 422 || code == 500; } static bool parseResponseBody(const string &responseData, Json::Value &jsonBody) { Json::Reader reader; if (reader.parse(responseData, jsonBody, false)) { return true; } else { P_ERROR("Error in anonymous telemetry server response:" " JSON response parse error: " << reader.getFormattedErrorMessages() << "; data: \"" << cEscapeString(responseData) << "\""); return false; } } static bool validateResponseBody(const Json::Value &jsonBody) { if (!jsonBody.isObject()) { P_ERROR("Error in anonymous telemetry server response:" " JSON response is not an object (data: " << stringifyJson(jsonBody) << ")"); return false; } if (!jsonBody.isMember("data_processed")) { P_ERROR("Error in anonymous telemetry server response:" " JSON response must contain a 'data_processed' field (data: " << stringifyJson(jsonBody) << ")"); return false; } if (!jsonBody["data_processed"].isBool()) { P_ERROR("Error in anonymous telemetry server response:" " 'data_processed' field must be a boolean (data: " << stringifyJson(jsonBody) << ")"); return false; } if (jsonBody.isMember("backoff") && !jsonBody["backoff"].isUInt()) { P_ERROR("Error in anonymous telemetry server response:" " 'backoff' field must be an unsigned integer (data: " << stringifyJson(jsonBody) << ")"); return false; } if (jsonBody.isMember("log_message") && !jsonBody["log_message"].isString()) { P_ERROR("Error in anonymous telemetry server response:" " 'log_message' field must be a string (data: " << stringifyJson(jsonBody) << ")"); return false; } return true; } unsigned int handleResponseBody(const TelemetryData &tmData, const Json::Value &jsonBody) { unsigned int backoffSec = 0; if (jsonBody["data_processed"].asBool()) { lastTelemetryData = tmData; } if (jsonBody.isMember("backoff")) { backoffSec = jsonBody["backoff"].asUInt(); } if (jsonBody.isMember("log_message")) { P_NOTICE("Message from " PROGRAM_AUTHOR ": " << jsonBody["log_message"].asString()); } return backoffSec; } public: // Dependencies vector<Controller *> controllers; TelemetryCollector(const Schema &schema, const Json::Value &initialConfig = Json::Value(), const ConfigKit::Translator &translator = ConfigKit::DummyTranslator()) : config(schema, initialConfig, translator), configRlz(config), collectorThread(NULL) { } virtual ~TelemetryCollector() { stop(); } void initialize() { if (controllers.empty()) { throw RuntimeException("controllers must be initialized"); } lastTelemetryData.requestsHandled.resize(controllers.size(), 0); lastTelemetryData.timestamp = SystemTime::getMonotonicUsecWithGranularity<SystemTime::GRAN_1SEC>(); } void start() { assert(!lastTelemetryData.requestsHandled.empty()); collectorThread = new oxt::thread( boost::bind(&TelemetryCollector::threadMain, this), "Telemetry collector", 1024 * 512 ); } void stop() { if (collectorThread != NULL) { collectorThread->interrupt_and_join(); delete collectorThread; collectorThread = NULL; } } unsigned int runOneCycle(bool isFinalRun = false) { TRACE_POINT(); boost::unique_lock<boost::mutex> l(configSyncher); SessionState sessionState(config, configRlz); l.unlock(); if (sessionState.config["disabled"].asBool()) { P_DEBUG("Telemetry collector disabled; not sending anonymous telemetry data"); return 0; } UPDATE_TRACE_POINT(); TelemetryData tmData = collectTelemetryData(isFinalRun); UPDATE_TRACE_POINT(); CURL *curl = NULL; CURLcode code; struct curl_slist *headers = NULL; string requestBody = createRequestBody(tmData); string responseData; char lastErrorMessage[CURL_ERROR_SIZE] = "unknown error"; Json::Value jsonBody; curl = prepareCurlRequest(sessionState, isFinalRun, &headers, lastErrorMessage, requestBody, responseData); if (curl == NULL) { // Error message already printed goto error; } P_INFO("Sending anonymous telemetry data to " PROGRAM_AUTHOR); P_DEBUG("Telemetry server URL is: " << sessionState.configRlz.url); P_DEBUG("Telemetry data to be sent is: " << requestBody); UPDATE_TRACE_POINT(); long responseCode; code = performCurlAction(curl, lastErrorMessage, requestBody, responseData, responseCode); if (code != CURLE_OK) { // Error message already printed goto error; } UPDATE_TRACE_POINT(); P_DEBUG("Response from telemetry server: status=" << responseCode << ", body=" << responseData); if (!responseCodeSupported(responseCode)) { P_ERROR("Error from anonymous telemetry server:" " response status not supported: " << responseCode); goto error; } if (!parseResponseBody(responseData, jsonBody) || !validateResponseBody(jsonBody)) { // Error message already printed goto error; } curl_slist_free_all(headers); curl_easy_cleanup(curl); return handleResponseBody(tmData, jsonBody); error: curl_slist_free_all(headers); if (curl != NULL) { curl_easy_cleanup(curl); } return 0; } bool prepareConfigChange(const Json::Value &updates, vector<ConfigKit::Error> &errors, ConfigChangeRequest &req) { { boost::lock_guard<boost::mutex> l(configSyncher); req.config.reset(new ConfigKit::Store(config, updates, errors)); } if (errors.empty()) { req.configRlz.reset(new ConfigRealization(*req.config)); } return errors.empty(); } void commitConfigChange(ConfigChangeRequest &req) BOOST_NOEXCEPT_OR_NOTHROW { boost::lock_guard<boost::mutex> l(configSyncher); config.swap(*req.config); configRlz.swap(*req.configRlz); } Json::Value inspectConfig() const { boost::lock_guard<boost::mutex> l(configSyncher); return config.inspect(); } }; } // namespace Core } // namespace Passenger #endif /* _PASSENGER_TELEMETRY_COLLECTOR_H_ */ ConfigChange.cpp 0000644 00000030345 14756456557 0007620 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2011-2018 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include <boost/thread.hpp> #include <boost/scoped_ptr.hpp> #include <vector> #include <cassert> #include <LoggingKit/LoggingKit.h> #include <Core/ConfigChange.h> #include <Core/Config.h> namespace Passenger { namespace Core { using namespace std; struct ConfigChangeRequest { Json::Value updates; PrepareConfigChangeCallback prepareCallback; CommitConfigChangeCallback commitCallback; unsigned int counter; vector<ConfigKit::Error> errors; boost::scoped_ptr<ConfigKit::Store> config; LoggingKit::ConfigChangeRequest forLoggingKit; SecurityUpdateChecker::ConfigChangeRequest forSecurityUpdateChecker; TelemetryCollector::ConfigChangeRequest forTelemetryCollector; vector<ServerKit::ConfigChangeRequest *> forControllerServerKit; vector<Controller::ConfigChangeRequest *> forController; ServerKit::ConfigChangeRequest forApiServerKit; ApiServer::ConfigChangeRequest forApiServer; AdminPanelConnector::ConfigChangeRequest forAdminPanelConnector; ConfigChangeRequest() : counter(0) { } ~ConfigChangeRequest() { { vector<ServerKit::ConfigChangeRequest *>::iterator it; for (it = forControllerServerKit.begin(); it != forControllerServerKit.end(); it++) { delete *it; } } { vector<Controller::ConfigChangeRequest *>::iterator it; for (it = forController.begin(); it != forController.end(); it++) { delete *it; } } } }; /**************** Functions: prepare config change ****************/ static void asyncPrepareConfigChangeCompletedOne(ConfigChangeRequest *req) { assert(req->counter > 0); req->counter--; if (req->counter == 0) { req->errors = ConfigKit::deduplicateErrors(req->errors); if (req->errors.empty()) { P_INFO("Changing configuration: " << req->updates.toStyledString()); } else { P_ERROR("Error changing configuration: " << ConfigKit::toString(req->errors) << "\nThe proposed configuration was: " << req->updates.toStyledString()); } oxt::thread(boost::bind(req->prepareCallback, req->errors, req), "Core config callback thread", 128 * 1024); } } static void asyncPrepareConfigChangeForController(unsigned int i, const Json::Value &updates, ConfigChangeRequest *req) { ThreadWorkingObjects *two = &workingObjects->threadWorkingObjects[i]; vector<ConfigKit::Error> errors1, errors2; req->forControllerServerKit[i] = new ServerKit::ConfigChangeRequest(); ConfigKit::prepareConfigChangeForSubComponent( *two->serverKitContext, coreSchema->controllerServerKit.translator, req->config->inspectEffectiveValues(), errors1, *req->forControllerServerKit[i]); req->forController[i] = new Controller::ConfigChangeRequest(); ConfigKit::prepareConfigChangeForSubComponent( *two->controller, coreSchema->controller.translator, req->config->inspectEffectiveValues(), errors2, *req->forController[i]); boost::lock_guard<boost::mutex> l(workingObjects->configSyncher); P_DEBUG("asyncPrepareConfigChangeForController(" << i << "): counter " << req->counter << " -> " << (req->counter - 1)); req->errors.insert(req->errors.begin(), errors1.begin(), errors1.end()); req->errors.insert(req->errors.begin(), errors2.begin(), errors2.end()); asyncPrepareConfigChangeCompletedOne(req); } static void asyncPrepareConfigChangeForApiServer(const Json::Value &updates, ConfigChangeRequest *req) { vector<ConfigKit::Error> errors1, errors2; ConfigKit::prepareConfigChangeForSubComponent( *workingObjects->apiWorkingObjects.serverKitContext, coreSchema->apiServerKit.translator, req->config->inspectEffectiveValues(), errors1, req->forApiServerKit); ConfigKit::prepareConfigChangeForSubComponent( *workingObjects->apiWorkingObjects.apiServer, coreSchema->apiServer.translator, req->config->inspectEffectiveValues(), errors2, req->forApiServer); boost::lock_guard<boost::mutex> l(workingObjects->configSyncher); P_DEBUG("asyncPrepareConfigChangeForApiServer: counter " << req->counter << " -> " << (req->counter - 1)); req->errors.insert(req->errors.begin(), errors1.begin(), errors1.end()); req->errors.insert(req->errors.begin(), errors2.begin(), errors2.end()); asyncPrepareConfigChangeCompletedOne(req); } static void asyncPrepareConfigChangeForAdminPanelConnectorDone(const vector<ConfigKit::Error> &errors, AdminPanelConnector::ConfigChangeRequest &_, ConfigChangeRequest *req) { vector<ConfigKit::Error> translatedErrors = coreSchema->adminPanelConnector.translator.reverseTranslate(errors); boost::lock_guard<boost::mutex> l(workingObjects->configSyncher); P_DEBUG("asyncPrepareConfigChangeForAdminPanelConnectorDone: counter " << req->counter << " -> " << (req->counter - 1)); req->errors.insert(req->errors.begin(), translatedErrors.begin(), translatedErrors.end()); asyncPrepareConfigChangeCompletedOne(req); } // // void asyncPrepareConfigChange(const Json::Value &updates, ConfigChangeRequest *req, const PrepareConfigChangeCallback &callback) { P_DEBUG("Preparing configuration change: " << updates.toStyledString()); WorkingObjects *wo = workingObjects; boost::lock_guard<boost::mutex> l(workingObjects->configSyncher); req->updates = updates; req->prepareCallback = callback; req->counter++; req->config.reset(new ConfigKit::Store(*coreConfig, updates, req->errors)); if (!req->errors.empty()) { asyncPrepareConfigChangeCompletedOne(req); return; } ConfigKit::prepareConfigChangeForSubComponent( *LoggingKit::context, coreSchema->loggingKit.translator, manipulateLoggingKitConfig(*req->config, req->config->inspectEffectiveValues()), req->errors, req->forLoggingKit); ConfigKit::prepareConfigChangeForSubComponent( *workingObjects->securityUpdateChecker, coreSchema->securityUpdateChecker.translator, req->config->inspectEffectiveValues(), req->errors, req->forSecurityUpdateChecker); if (workingObjects->telemetryCollector != NULL) { ConfigKit::prepareConfigChangeForSubComponent( *workingObjects->telemetryCollector, coreSchema->telemetryCollector.translator, req->config->inspectEffectiveValues(), req->errors, req->forTelemetryCollector); } req->forControllerServerKit.resize(wo->threadWorkingObjects.size(), NULL); req->forController.resize(wo->threadWorkingObjects.size(), NULL); for (unsigned int i = 0; i < wo->threadWorkingObjects.size(); i++) { ThreadWorkingObjects *two = &wo->threadWorkingObjects[i]; req->counter++; two->bgloop->safe->runLater(boost::bind(asyncPrepareConfigChangeForController, i, updates, req)); } if (wo->apiWorkingObjects.apiServer != NULL) { req->counter++; wo->apiWorkingObjects.bgloop->safe->runLater(boost::bind( asyncPrepareConfigChangeForApiServer, updates, req)); } if (wo->adminPanelConnector != NULL) { req->counter++; wo->adminPanelConnector->asyncPrepareConfigChange( coreSchema->adminPanelConnector.translator.translate(updates), req->forAdminPanelConnector, boost::bind(asyncPrepareConfigChangeForAdminPanelConnectorDone, boost::placeholders::_1, boost::placeholders::_2, req)); } /***************/ /***************/ asyncPrepareConfigChangeCompletedOne(req); } /**************** Functions: commit config change ****************/ static void asyncCommitConfigChangeCompletedOne(ConfigChangeRequest *req) { assert(req->counter > 0); req->counter--; if (req->counter == 0) { oxt::thread(boost::bind(req->commitCallback, req), "Core config callback thread", 128 * 1024); } } static void asyncCommitConfigChangeForController(unsigned int i, ConfigChangeRequest *req) { ThreadWorkingObjects *two = &workingObjects->threadWorkingObjects[i]; two->serverKitContext->commitConfigChange(*req->forControllerServerKit[i]); two->controller->commitConfigChange(*req->forController[i]); boost::lock_guard<boost::mutex> l(workingObjects->configSyncher); P_DEBUG("asyncCommitConfigChangeForController(" << i << "): counter " << req->counter << " -> " << (req->counter - 1)); asyncCommitConfigChangeCompletedOne(req); } static void asyncCommitConfigChangeForApiServer(ConfigChangeRequest *req) { ApiWorkingObjects *awo = &workingObjects->apiWorkingObjects; awo->serverKitContext->commitConfigChange(req->forApiServerKit); awo->apiServer->commitConfigChange(req->forApiServer); boost::lock_guard<boost::mutex> l(workingObjects->configSyncher); P_DEBUG("asyncCommitConfigChangeForApiServer: counter " << req->counter << " -> " << (req->counter - 1)); asyncCommitConfigChangeCompletedOne(req); } static void asyncCommitConfigChangeForAdminPanelConnectorDone(AdminPanelConnector::ConfigChangeRequest &_, ConfigChangeRequest *req) { boost::lock_guard<boost::mutex> l(workingObjects->configSyncher); P_DEBUG("asyncCommitConfigChangeForAdminPanelConnectorDone: counter " << req->counter << " -> " << (req->counter - 1)); asyncCommitConfigChangeCompletedOne(req); } // // void asyncCommitConfigChange(ConfigChangeRequest *req, const CommitConfigChangeCallback &callback) BOOST_NOEXCEPT_OR_NOTHROW { WorkingObjects *wo = workingObjects; boost::lock_guard<boost::mutex> l(workingObjects->configSyncher); req->commitCallback = callback; req->counter++; coreConfig->swap(*req->config); LoggingKit::context->commitConfigChange(req->forLoggingKit); workingObjects->securityUpdateChecker->commitConfigChange( req->forSecurityUpdateChecker); if (workingObjects->telemetryCollector != NULL) { workingObjects->telemetryCollector->commitConfigChange( req->forTelemetryCollector); } wo->appPool->setMax(coreConfig->get("max_pool_size").asInt()); wo->appPool->setMaxIdleTime(coreConfig->get("pool_idle_time").asInt() * 1000000ULL); wo->appPool->enableSelfChecking(coreConfig->get("pool_selfchecks").asBool()); { LockGuard l(wo->appPoolContext->agentConfigSyncher); wo->appPoolContext->agentConfig = coreConfig->inspectEffectiveValues(); } for (unsigned int i = 0; i < wo->threadWorkingObjects.size(); i++) { ThreadWorkingObjects *two = &wo->threadWorkingObjects[i]; req->counter++; two->bgloop->safe->runLater(boost::bind(asyncCommitConfigChangeForController, i, req)); } if (wo->apiWorkingObjects.apiServer != NULL) { req->counter++; wo->apiWorkingObjects.bgloop->safe->runLater(boost::bind( asyncCommitConfigChangeForApiServer, req)); } if (wo->adminPanelConnector != NULL) { req->counter++; wo->adminPanelConnector->asyncCommitConfigChange( req->forAdminPanelConnector, boost::bind(asyncCommitConfigChangeForAdminPanelConnectorDone, boost::placeholders::_1, req)); } /***************/ /***************/ asyncCommitConfigChangeCompletedOne(req); } /**************** Functions: miscellaneous ****************/ inline ConfigChangeRequest * createConfigChangeRequest() { return new ConfigChangeRequest(); } inline void freeConfigChangeRequest(ConfigChangeRequest *req) { delete req; } Json::Value inspectConfig() { boost::lock_guard<boost::mutex> l(workingObjects->configSyncher); return coreConfig->inspect(); } Json::Value manipulateLoggingKitConfig(const ConfigKit::Store &coreConfig, const Json::Value &loggingKitConfig) { Json::Value result = loggingKitConfig; result["buffer_logs"] = !coreConfig["admin_panel_url"].isNull(); return result; } } // namespace Core } // namespace Passenger AdminPanelConnector.h 0000644 00000054674 14756456557 0010650 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2017-2018 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef _PASSENGER_ADMIN_PANEL_CONNECTOR_H_ #define _PASSENGER_ADMIN_PANEL_CONNECTOR_H_ #include <sys/wait.h> #include <sstream> #include <unistd.h> #include <boost/scoped_ptr.hpp> #include <boost/thread.hpp> #include <boost/bind/bind.hpp> #include <boost/foreach.hpp> #include <limits> #include <string> #include <vector> #include <Constants.h> #include <WebSocketCommandReverseServer.h> #include <InstanceDirectory.h> #include <ConfigKit/SchemaUtils.h> #include <Core/ApplicationPool/Pool.h> #include <Core/Controller.h> #include <ProcessManagement/Ruby.h> #include <FileTools/FileManip.h> #include <SystemTools/UserDatabase.h> #include <Utils.h> #include <StrIntTools/StrIntUtils.h> #include <IOTools/IOUtils.h> #include <Utils/AsyncSignalSafeUtils.h> #include <LoggingKit/Context.h> #include <jsoncpp/json.h> namespace Passenger { namespace Core { using namespace std; using namespace oxt; namespace ASSU = AsyncSignalSafeUtils; class AdminPanelConnector { public: /** * BEGIN ConfigKit schema: Passenger::Core::AdminPanelConnector::Schema * (do not edit: following text is automatically generated * by 'rake configkit_schemas_inline_comments') * * auth_type string - default("basic") * close_timeout float - default(10.0) * connect_timeout float - default(30.0) * data_debug boolean - default(false) * instance_dir string - read_only * integration_mode string - default("standalone") * log_prefix string - - * password string - secret * password_file string - - * ping_interval float - default(30.0) * ping_timeout float - default(30.0) * proxy_password string - secret * proxy_timeout float - default(30.0) * proxy_url string - - * proxy_username string - - * reconnect_timeout float - default(5.0) * ruby string - default("ruby") * standalone_engine string - default * url string required - * username string - - * web_server_module_version string - read_only * web_server_version string - read_only * websocketpp_debug_access boolean - default(false) * websocketpp_debug_error boolean - default(false) * * END */ struct Schema: public WebSocketCommandReverseServer::Schema { Schema() : WebSocketCommandReverseServer::Schema(false) { using namespace ConfigKit; add("integration_mode", STRING_TYPE, OPTIONAL, DEFAULT_INTEGRATION_MODE); addWithDynamicDefault("standalone_engine", STRING_TYPE, OPTIONAL, ConfigKit::getDefaultStandaloneEngine); add("instance_dir", STRING_TYPE, OPTIONAL | READ_ONLY); add("web_server_version", STRING_TYPE, OPTIONAL | READ_ONLY); add("web_server_module_version", STRING_TYPE, OPTIONAL | READ_ONLY); add("ruby", STRING_TYPE, OPTIONAL, "ruby"); addValidator(ConfigKit::validateIntegrationMode); addValidator(ConfigKit::validateStandaloneEngine); finalize(); } }; typedef WebSocketCommandReverseServer::ConfigChangeRequest ConfigChangeRequest; typedef WebSocketCommandReverseServer::ConnectionPtr ConnectionPtr; typedef WebSocketCommandReverseServer::MessagePtr MessagePtr; typedef boost::function<Json::Value (void)> ConfigGetter; typedef vector<Controller*> Controllers; private: WebSocketCommandReverseServer server; dynamic_thread_group threads; Json::Value globalPropertiesFromInstanceDir; bool onMessage(WebSocketCommandReverseServer *server, const ConnectionPtr &conn, const MessagePtr &msg) { Json::Value doc; try { doc = parseAndBasicValidateMessageAsJSON(msg->get_payload()); } catch (const RuntimeException &e) { Json::Value reply; reply["result"] = "error"; reply["request_id"] = doc["request_id"]; reply["data"]["message"] = e.what(); sendJsonReply(conn, reply); return true; } if (doc["action"] == "get") { return onGetMessage(conn, doc); } else { return onUnknownMessageAction(conn, doc); } } bool onGetMessage(const ConnectionPtr &conn, const Json::Value &doc) { const string resource = doc["resource"].asString(); if (resource == "server_properties") { return onGetServerProperties(conn, doc); } else if (resource == "global_properties") { return onGetGlobalProperties(conn, doc); } else if (resource == "global_configuration") { return onGetGlobalConfiguration(conn, doc); } else if (resource == "global_statistics") { return onGetGlobalStatistics(conn, doc); } else if (resource == "application_properties") { return onGetApplicationProperties(conn, doc); } else if (resource == "application_configuration") { return onGetApplicationConfig(conn, doc); } else if (resource == "application_logs") { return onGetApplicationLogs(conn, doc); } else { return onUnknownResource(conn, doc); } } bool onGetServerProperties(const ConnectionPtr &conn, const Json::Value &doc) { threads.create_thread( boost::bind(&AdminPanelConnector::onGetServerPropertiesBgJob, this, conn, doc, server.getConfig()["ruby"].asString()), "AdminPanelCommandServer: get_server_properties background job", 128 * 1024); return false; } void onGetServerPropertiesBgJob(const ConnectionPtr &conn, const Json::Value &doc, const string &ruby) { vector<string> args; args.push_back("passenger-config"); args.push_back("system-properties"); int status = 0; SubprocessOutput output; try { runInternalRubyTool(*resourceLocator, ruby, args, &status, &output); } catch (const std::exception &e) { server.getIoService().post(boost::bind( &AdminPanelConnector::onGetServerPropertiesDone, this, conn, doc, string(), -1, e.what() )); return; } server.getIoService().post(boost::bind( &AdminPanelConnector::onGetServerPropertiesDone, this, conn, doc, output.data, status, string() )); } void onGetServerPropertiesDone(const ConnectionPtr &conn, const Json::Value &doc, const string output, int status, const string &error) { Json::Value reply; reply["request_id"] = doc["request_id"]; if (error.empty()) { if (status == 0 || status == -1) { Json::Reader reader; Json::Value dataDoc; if (output.empty()) { reply["result"] = "error"; reply["data"]["message"] = "Error parsing internal helper tool output"; P_ERROR(getLogPrefix() << "Error parsing internal helper tool output.\n" << "Raw data: \"\""); } else if (reader.parse(output, dataDoc)) { reply["result"] = "ok"; reply["data"] = dataDoc; } else { reply["result"] = "error"; reply["data"]["message"] = "Error parsing internal helper tool output"; P_ERROR(getLogPrefix() << "Error parsing internal helper tool output.\n" << "Error: " << reader.getFormattedErrorMessages() << "\n" "Raw data: \"" << cEscapeString(output) << "\""); } } else { int exitStatus = WEXITSTATUS(status); reply["result"] = "error"; reply["data"]["message"] = "Internal helper tool exited with status " + toString(exitStatus); P_ERROR(getLogPrefix() << "Internal helper tool exited with status " << exitStatus << ". Raw output: \"" << cEscapeString(output) << "\""); } } else { reply["result"] = "error"; reply["data"]["message"] = error; } sendJsonReply(conn, reply); server.doneReplying(conn); } bool onGetGlobalProperties(const ConnectionPtr &conn, const Json::Value &doc) { const ConfigKit::Store &config = server.getConfig(); Json::Value reply, data; reply["result"] = "ok"; reply["request_id"] = doc["request_id"]; data = globalPropertiesFromInstanceDir; data["version"] = PASSENGER_VERSION; data["core_pid"] = Json::UInt(getpid()); string integrationMode = config["integration_mode"].asString(); data["integration_mode"]["name"] = integrationMode; if (!config["web_server_module_version"].isNull()) { data["integration_mode"]["web_server_module_version"] = config["web_server_module_version"]; } if (integrationMode == "standalone") { data["integration_mode"]["standalone_engine"] = config["standalone_engine"]; } if (!config["web_server_version"].isNull()) { data["integration_mode"]["web_server_version"] = config["web_server_version"]; } data["originally_packaged"] = resourceLocator->isOriginallyPackaged(); if (!resourceLocator->isOriginallyPackaged()) { data["packaging_method"] = resourceLocator->getPackagingMethod(); } reply["data"] = data; sendJsonReply(conn, reply); return true; } bool onGetGlobalConfiguration(const ConnectionPtr &conn, const Json::Value &doc) { threads.create_thread( boost::bind(&AdminPanelConnector::onGetGlobalConfigurationBgJob, this, conn, doc), "AdminPanelCommandServer: get_global_config background job", 128 * 1024); return false; } void onGetGlobalConfigurationBgJob(const ConnectionPtr &conn, const Json::Value &input) { Json::Value globalConfig = configGetter()["config_manifest"]["effective_value"]["global_configuration"]; server.getIoService().post(boost::bind( &AdminPanelConnector::onGetGlobalConfigDone, this, conn, input, globalConfig )); } void onGetGlobalConfigDone(const ConnectionPtr &conn, const Json::Value &input, Json::Value config) { Json::Value reply; reply["result"] = "ok"; reply["request_id"] = input["request_id"]; reply["data"]["options"] = config; sendJsonReply(conn, reply); server.doneReplying(conn); } bool onGetGlobalStatistics(const ConnectionPtr &conn, const Json::Value &doc) { Json::Value reply; reply["result"] = "ok"; reply["request_id"] = doc["request_id"]; reply["data"]["message"] = Json::arrayValue; for (unsigned int i = 0; i < controllers.size(); i++) { reply["data"]["message"].append(controllers[i]->inspectStateAsJson()); } sendJsonReply(conn, reply); return true; } bool onGetApplicationProperties(const ConnectionPtr &conn, const Json::Value &doc) { ConfigKit::Schema argumentsSchema = ApplicationPool2::Pool::ToJsonOptions::createSchema(); Json::Value args(Json::objectValue), reply; ApplicationPool2::Pool::ToJsonOptions inspectOptions = ApplicationPool2::Pool::ToJsonOptions::makeAuthorized(); if (doc.isMember("arguments")) { ConfigKit::Store store(argumentsSchema); vector<ConfigKit::Error> errors; if (store.update(doc["arguments"], errors)) { inspectOptions.set(store.inspectEffectiveValues()); } else { reply["result"] = "error"; reply["request_id"] = doc["request_id"]; reply["data"]["message"] = "Invalid arguments: " + ConfigKit::toString(errors); sendJsonReply(conn, reply); return true; } } reply["result"] = "ok"; reply["request_id"] = doc["request_id"]; reply["data"]["applications"] = appPool->inspectPropertiesInAdminPanelFormat( inspectOptions); sendJsonReply(conn, reply); return true; } static void modifyEnvironmentVariables(Json::Value &option) { Json::Value::iterator it; for (it = option.begin(); it != option.end(); it++) { Json::Value &suboption = *it; suboption["value"] = suboption["value"].toStyledString(); } } bool onGetApplicationConfig(const ConnectionPtr &conn, const Json::Value &doc) { Json::Value appConfigsContainer = configGetter()["config_manifest"] ["effective_value"]["application_configurations"]; Json::Value appConfigsContainerOutput; Json::Value reply; if (doc.isMember("arguments")) { ConfigKit::Schema argumentsSchema = ApplicationPool2::Pool::ToJsonOptions::createSchema(); ConfigKit::Store store(argumentsSchema); vector<ConfigKit::Error> errors; if (!store.update(doc["arguments"], errors)) { reply["result"] = "error"; reply["request_id"] = doc["request_id"]; reply["data"]["message"] = "Invalid arguments: " + ConfigKit::toString(errors); sendJsonReply(conn, reply); return true; } Json::Value allowedApplicationIds = store.inspectEffectiveValues()["application_ids"]; if (allowedApplicationIds.isNull()) { appConfigsContainerOutput = appConfigsContainer; } else { appConfigsContainerOutput = filterJsonObject( appConfigsContainer, allowedApplicationIds); } } else { appConfigsContainerOutput = appConfigsContainer; } reply["result"] = "ok"; reply["request_id"] = doc["request_id"]; reply["data"]["options"] = appConfigsContainerOutput; sendJsonReply(conn, reply); return true; } void addWatchedFiles() { Json::Value appConfigs = configGetter()["config_manifest"]["effective_value"]["application_configurations"]; // As a hack, we look up the watched files config (passenger monitor log file) in the manifest. The manifest // is meant for users, which means that key names depend on the integration mode. In the future when // component configuration more routed through ConfigKit we can get rid of the hack. string integrationMode = server.getConfig()["integration_mode"].asString(); string passengerMonitorLogFile; string passengerAppRoot; if (integrationMode == "apache") { passengerMonitorLogFile = "PassengerMonitorLogFile"; passengerAppRoot = "PassengerAppRoot"; } else { passengerMonitorLogFile = "passenger_monitor_log_file"; passengerAppRoot = "passenger_app_root"; // TODO: this probably doesn't give any results with the builtin engine (not supported in other places either) } foreach (HashedStaticString key, appConfigs.getMemberNames()) { Json::Value files = appConfigs[key]["options"][passengerMonitorLogFile]["value_hierarchy"][0]["value"]; string appRoot = appConfigs[key]["options"][passengerAppRoot]["value_hierarchy"][0]["value"].asString(); pair<uid_t, gid_t> ids; try { ids = appPool->getGroupRunUidAndGids(key); } catch (const RuntimeException &) { files = Json::nullValue; } if (!files.isNull()) { string usernameOrUid = lookupSystemUsernameByUid(ids.first, P_STATIC_STRING("%d")); foreach (Json::Value file, files) { string f = file.asString(); string maxLines = toString(LOG_MONITORING_MAX_LINES); Pipe pipe = createPipe(__FILE__, __LINE__); string agentExe = resourceLocator->findSupportBinary(AGENT_EXE); vector<const char *> execArgs; execArgs.push_back(agentExe.c_str()); execArgs.push_back("exec-helper"); if (geteuid() == 0) { execArgs.push_back("--user"); execArgs.push_back(usernameOrUid.c_str()); } execArgs.push_back("tail"); execArgs.push_back("-n"); execArgs.push_back(maxLines.c_str()); execArgs.push_back(f.c_str()); execArgs.push_back(NULL); pid_t pid = syscalls::fork(); if (pid == -1) { int e = errno; throw SystemException("Cannot fork a new process", e); } else if (pid == 0) { chdir(appRoot.c_str()); dup2(pipe.second, STDOUT_FILENO); pipe.first.close(); pipe.second.close(); closeAllFileDescriptors(2); execvp(execArgs[0], (char * const *) &execArgs[0]); int e = errno; char buf[256]; char *pos = buf; const char *end = pos + 256; pos = ASSU::appendData(pos, end, "Cannot execute \""); pos = ASSU::appendData(pos, end, agentExe.c_str()); pos = ASSU::appendData(pos, end, "\": "); pos = ASSU::appendData(pos, end, strerror(e)); pos = ASSU::appendData(pos, end, " (errno="); pos = ASSU::appendInteger<int, 10>(pos, end, e); pos = ASSU::appendData(pos, end, ")\n"); ASSU::writeNoWarn(STDERR_FILENO, buf, pos - buf); _exit(1); } else { pipe.second.close(); string out = readAll(pipe.first, std::numeric_limits<size_t>::max()).first; LoggingKit::context->saveMonitoredFileLog(key, f.c_str(), f.size(), out.data(), out.size()); pipe.first.close(); syscalls::waitpid(pid, NULL, 0); } } } } } bool onGetApplicationLogs(const ConnectionPtr &conn, const Json::Value &doc) { Json::Value reply; reply["result"] = "ok"; reply["request_id"] = doc["request_id"]; addWatchedFiles(); reply["data"]["logs"] = LoggingKit::context->convertLog(); sendJsonReply(conn, reply); return true; } bool onUnknownResource(const ConnectionPtr &conn, const Json::Value &doc) { Json::Value reply; reply["result"] = "error"; reply["request_id"] = doc["request_id"]; reply["data"]["message"] = "Unknown resource '" + doc["resource"].asString() + "'"; sendJsonReply(conn, reply); return true; } bool onUnknownMessageAction(const ConnectionPtr &conn, const Json::Value &doc) { Json::Value reply; reply["result"] = "error"; reply["request_id"] = doc["request_id"]; reply["data"]["message"] = "Unknown action '" + doc["action"].asString() + "'"; sendJsonReply(conn, reply); return true; } Json::Value parseAndBasicValidateMessageAsJSON(const string &msg) const { Json::Value doc; Json::Reader reader; if (!reader.parse(msg, doc)) { throw RuntimeException("Error parsing command JSON document: " + reader.getFormattedErrorMessages()); } if (!doc.isObject()) { throw RuntimeException("Invalid command JSON document: must be an object"); } if (!doc.isMember("action")) { throw RuntimeException("Invalid command JSON document: missing 'action' key"); } if (!doc["action"].isString()) { throw RuntimeException("Invalid command JSON document: the 'action' key must be a string"); } if (!doc.isMember("request_id")) { throw RuntimeException("Invalid command JSON document: missing 'request_id' key"); } if (!doc.isMember("resource")) { throw RuntimeException("Invalid command JSON document: missing 'resource' key"); } if (!doc["resource"].isString()) { throw RuntimeException("Invalid command JSON document: the 'resource' key must be a string"); } if (doc.isMember("arguments") && !doc["arguments"].isObject()) { throw RuntimeException("Invalid command JSON document: the 'arguments' key, when present, must be an object"); } return doc; } void sendJsonReply(const ConnectionPtr &conn, const Json::Value &doc) { Json::FastWriter writer; string str = writer.write(doc); WCRS_DEBUG_FRAME(&server, "Replying with:", str); conn->send(str); } void readInstanceDirProperties(const string &instanceDir) { Json::Value doc; Json::Reader reader; if (!reader.parse(unsafeReadFile(instanceDir + "/properties.json"), doc)) { throw RuntimeException("Cannot parse " + instanceDir + "/properties.json: " + reader.getFormattedErrorMessages()); } globalPropertiesFromInstanceDir["instance_id"] = doc["instance_id"]; globalPropertiesFromInstanceDir["watchdog_pid"] = doc["watchdog_pid"]; } Json::Value filterJsonObject(const Json::Value &object, const Json::Value &allowedKeys) const { Json::Value::const_iterator it, end = allowedKeys.end(); Json::Value result(Json::objectValue); for (it = allowedKeys.begin(); it != end; it++) { if (object.isMember(it->asString())) { result[it->asString()] = object[it->asString()]; } } return result; } void initializePropertiesWithoutInstanceDir() { globalPropertiesFromInstanceDir["instance_id"] = InstanceDirectory::generateInstanceId(); } string getLogPrefix() const { return server.getConfig()["log_prefix"].asString(); } WebSocketCommandReverseServer::MessageHandler createMessageFunctor() { return boost::bind(&AdminPanelConnector::onMessage, this, boost::placeholders::_1, boost::placeholders::_2, boost::placeholders::_3); } public: /******* Dependencies *******/ ResourceLocator *resourceLocator; ApplicationPool2::PoolPtr appPool; ConfigGetter configGetter; Controllers controllers; AdminPanelConnector(const Schema &schema, const Json::Value &config, const ConfigKit::Translator &translator = ConfigKit::DummyTranslator()) : server(schema, createMessageFunctor(), config, translator), resourceLocator(NULL) { if (!config["instance_dir"].isNull()) { readInstanceDirProperties(config["instance_dir"].asString()); } else { initializePropertiesWithoutInstanceDir(); } } void initialize() { if (resourceLocator == NULL) { throw RuntimeException("resourceLocator must be non-NULL"); } if (appPool == NULL) { throw RuntimeException("appPool must be non-NULL"); } if (configGetter.empty()) { throw RuntimeException("configGetter must be non-NULL"); } server.initialize(); } void run() { server.run(); } void asyncPrepareConfigChange(const Json::Value &updates, ConfigChangeRequest &req, const ConfigKit::CallbackTypes<WebSocketCommandReverseServer>::PrepareConfigChange &callback) { server.asyncPrepareConfigChange(updates, req, callback); } void asyncCommitConfigChange(ConfigChangeRequest &req, const ConfigKit::CallbackTypes<WebSocketCommandReverseServer>::CommitConfigChange &callback) BOOST_NOEXCEPT_OR_NOTHROW { server.asyncCommitConfigChange(req, callback); } void asyncShutdown(const WebSocketCommandReverseServer::Callback &callback = WebSocketCommandReverseServer::Callback()) { server.asyncShutdown(callback); } }; } // namespace Core } // namespace Passenger #endif /* _PASSENGER_ADMIN_PANEL_CONNECTOR_H_ */ OptionParser.h 0000644 00000055105 14756456557 0007400 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2010-2018 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef _PASSENGER_CORE_OPTION_PARSER_H_ #define _PASSENGER_CORE_OPTION_PARSER_H_ #include <boost/thread.hpp> #include <cstdio> #include <cstdlib> #include <cstring> #include <Constants.h> #include <JsonTools/Autocast.h> #include <Utils.h> #include <Utils/OptionParsing.h> #include <StrIntTools/StrIntUtils.h> #include <jsoncpp/json.h> namespace Passenger { using namespace std; inline void coreUsage() { // ....|---------------Keep output within standard terminal width (80 chars)------------| printf("Usage: " AGENT_EXE " core <OPTIONS...> [APP DIRECTORY]\n"); printf("Runs the " PROGRAM_NAME " core.\n"); printf("\n"); printf("The core starts in single-app mode, unless --multi-app is specified. When\n"); printf("in single-app mode, it serves the app at the current working directory, or the\n"); printf("app specified by APP DIRECTORY.\n"); printf("\n"); printf("Required options:\n"); printf(" --passenger-root PATH The location to the " PROGRAM_NAME " source\n"); printf(" directory\n"); printf("\n"); printf("Socket options (optional):\n"); printf(" -l, --listen ADDRESS Listen on the given address. The address must be\n"); printf(" formatted as tcp://IP:PORT for TCP sockets, or\n"); printf(" unix:PATH for Unix domain sockets. You can specify\n"); printf(" this option multiple times (up to %u times) to\n", SERVER_KIT_MAX_SERVER_ENDPOINTS); printf(" listen on multiple addresses. Default:\n"); printf(" " DEFAULT_HTTP_SERVER_LISTEN_ADDRESS "\n"); printf(" --api-listen ADDRESS Listen on the given address for API commands.\n"); printf(" The same syntax and limitations as with --listen\n"); printf(" are applicable\n"); printf(" --socket-backlog Override size of the socket backlog.\n"); printf(" Default: %d\n", DEFAULT_SOCKET_BACKLOG); printf("\n"); printf("Daemon options (optional):\n"); printf(" --pid-file PATH Store the core's PID in the given file. The file\n"); printf(" is deleted on exit\n"); printf("\n"); printf("Security options (optional):\n"); printf(" --multi-app-password-file PATH\n"); printf(" Password-protect access to the core's HTTP server\n"); printf(" (multi-app mode only)\n"); printf(" --authorize [LEVEL]:USERNAME:PASSWORDFILE\n"); printf(" Enables authentication on the API server, through\n"); printf(" the given API account. LEVEL indicates the\n"); printf(" privilege level (see below). PASSWORDFILE must\n"); printf(" point to a file containing the password\n"); printf(" --no-user-switching Disables user switching support\n"); printf(" --default-user NAME Default user to start apps as, when user\n"); printf(" switching is enabled. Default: " DEFAULT_WEB_APP_USER "\n"); printf(" --default-group NAME Default group to start apps as, when user\n"); printf(" switching is disabled. Default: the default\n"); printf(" user's primary group\n"); printf(" --disable-security-update-check\n"); printf(" Disable the periodic check and notice about\n"); printf(" important security updates\n"); printf(" --security-update-check-proxy PROXY\n"); printf(" Use HTTP/SOCKS proxy for the security update check:\n"); printf(" scheme://user:password@proxy_host:proxy_port\n"); printf(" --disable-anonymous-telemetry\n"); printf(" Disable anonymous telemetry collection\n"); printf(" --anonymous-telemetry-proxy PROXY\n"); printf(" Use HTTP/SOCKS proxy for anonymous telemetry sending:\n"); printf(" scheme://user:password@proxy_host:proxy_port\n"); printf("\n"); printf("Application serving options (optional):\n"); printf(" -e, --environment NAME Default framework environment name to use.\n"); printf(" Default: " DEFAULT_APP_ENV "\n"); printf(" --app-type TYPE The type of application you want to serve\n"); printf(" (single-app mode only)\n"); printf(" --startup-file PATH The path of the app's startup file, relative to\n"); printf(" the app root directory (single-app mode only)\n"); printf(" --app-start-command COMMAND\n"); printf(" The command string with which to start the app\n"); printf(" (single-app mode only)\n"); printf(" --spawn-method NAME Spawn method to use. Can either be 'smart' or\n"); printf(" 'direct'. Default: %s\n", DEFAULT_SPAWN_METHOD); printf(" --load-shell-envvars Load shell startup files before loading application\n"); printf(" --preload-bundler Tell Ruby to load bundler gem before loading application\n"); printf(" --concurrency-model The concurrency model to use for the app, either\n"); printf(" 'process' or 'thread' (Enterprise only).\n"); printf(" Default: " DEFAULT_CONCURRENCY_MODEL "\n"); printf(" --app-thread-count The number of application threads to use when using\n"); printf(" the 'thread' concurrency model (Enterprise only).\n"); printf(" Default: %d\n", DEFAULT_APP_THREAD_COUNT); printf("\n"); printf(" --multi-app Enable multi-app mode\n"); printf("\n"); printf(" --force-friendly-error-pages\n"); printf(" Force friendly error pages to be always on\n"); printf(" --disable-friendly-error-pages\n"); printf(" Force friendly error pages to be always off\n"); printf("\n"); printf(" --ruby PATH Default Ruby interpreter to use.\n"); printf(" --nodejs PATH Default NodeJs interpreter to use.\n"); printf(" --python PATH Default Python interpreter to use.\n"); printf(" --meteor-app-settings PATH\n"); printf(" File with settings for a Meteor (non-bundled) app.\n"); printf(" (passed to Meteor using --settings)\n"); printf(" --app-file-descriptor-ulimit NUMBER\n"); printf(" Set custom file descriptor ulimit for the app\n"); printf(" --debugger Enable Ruby debugger support (Enterprise only)\n"); printf("\n"); printf(" --rolling-restarts Enable rolling restarts (Enterprise only)\n"); printf(" --resist-deployment-errors\n"); printf(" Enable deployment error resistance (Enterprise only)\n"); printf("\n"); printf("Process management options (optional):\n"); printf(" --max-pool-size N Maximum number of application processes.\n"); printf(" Default: %d\n", DEFAULT_MAX_POOL_SIZE); printf(" --pool-idle-time SECS\n"); printf(" Maximum number of seconds an application process\n"); printf(" may be idle. Default: %d\n", DEFAULT_POOL_IDLE_TIME); printf(" --max-preloader-idle-time SECS\n"); printf(" Maximum time that preloader processes may be\n"); printf(" be idle. A value of 0 means that preloader\n"); printf(" processes never timeout. Default: %d\n", DEFAULT_MAX_PRELOADER_IDLE_TIME); printf(" --force-max-concurrent-requests-per-process NUMBER\n"); printf(" Force " SHORT_PROGRAM_NAME " to believe that an application\n"); printf(" process can handle the given number of concurrent\n"); printf(" requests per process\n"); printf(" --min-instances N Minimum number of application processes. Default: 1\n"); printf(" --memory-limit MB Restart application processes that go over the\n"); printf(" given memory limit (Enterprise only)\n"); printf("\n"); printf("Request handling options (optional):\n"); printf(" --max-requests Restart application processes that have handled\n"); printf(" the specified maximum number of requests\n"); printf(" --max-request-time Abort requests that take too much time (Enterprise\n"); printf(" only)\n"); printf(" --max-request-queue-size NUMBER\n"); printf(" Specify request queue size. Default: %d\n", DEFAULT_MAX_REQUEST_QUEUE_SIZE); printf(" --sticky-sessions Enable sticky sessions\n"); printf(" --sticky-sessions-cookie-name NAME\n"); printf(" Cookie name to use for sticky sessions.\n"); printf(" Default: " DEFAULT_STICKY_SESSIONS_COOKIE_NAME "\n"); printf(" --sticky-sessions-cookie-attributes 'NAME1=VALUE1; NAME2'\n"); printf(" The attributes to use for the sticky session cookie.\n"); printf(" Default: " DEFAULT_STICKY_SESSIONS_COOKIE_ATTRIBUTES "\n"); printf(" --vary-turbocache-by-cookie NAME\n"); printf(" Vary the turbocache by the cookie of the given name\n"); printf(" --disable-turbocaching\n"); printf(" Disable turbocaching\n"); printf(" --no-abort-websockets-on-process-shutdown\n"); printf(" Do not abort WebSocket connections on process\n"); printf(" shutdown or restart\n"); printf("\n"); printf("Other options (optional):\n"); printf(" --log-file PATH Log to the given file.\n"); printf(" --log-level LEVEL Logging level. Default: %d\n", DEFAULT_LOG_LEVEL); printf(" --fd-log-file PATH Log file descriptor activity to the given file.\n"); printf(" --stat-throttle-rate SECONDS\n"); printf(" Throttle filesystem restart.txt checks to at most\n"); printf(" once per given seconds. Default: %d\n", DEFAULT_STAT_THROTTLE_RATE); printf(" --no-show-version-in-header\n"); printf(" Do not show " PROGRAM_NAME " version number in\n"); printf(" HTTP headers.\n"); printf(" --data-buffer-dir PATH\n"); printf(" Directory to store data buffers in. Default:\n"); printf(" %s\n", getSystemTempDir()); printf(" --no-graceful-exit When exiting, exit immediately instead of waiting\n"); printf(" for all connections to terminate\n"); printf(" --benchmark MODE Enable benchmark mode. Available modes:\n"); printf(" after_accept,before_checkout,after_checkout,\n"); printf(" response_begin\n"); printf(" --disable-selfchecks Disable various self-checks. This improves\n"); printf(" performance, but might delay finding bugs in\n"); printf(" " PROGRAM_NAME "\n"); printf(" --threads NUMBER Number of threads to use for request handling.\n"); printf(" Default: number of CPU cores (%d)\n", boost::thread::hardware_concurrency()); printf(" --cpu-affine Enable per-thread CPU affinity (Linux only)\n"); printf(" --core-file-descriptor-ulimit NUMBER\n"); printf(" Set custom file descriptor ulimit for the core\n"); printf(" --admin-panel-url URL\n"); printf(" Connect to an admin panel through this service\n"); printf(" connector URL\n"); printf(" --ctl NAME=VALUE Set low-level config option directly\n"); printf(" -h, --help Show this help\n"); printf("\n"); printf("API account privilege levels (ordered from most to least privileges):\n"); printf(" readonly Read-only access\n"); printf(" full Full access (default)\n"); } inline bool parseCoreOption(int argc, const char *argv[], int &i, Json::Value &updates) { OptionParser p(coreUsage); if (p.isValueFlag(argc, i, argv[i], '\0', "--passenger-root")) { updates["passenger_root"] = argv[i + 1]; i += 2; } else if (p.isValueFlag(argc, i, argv[i], 'l', "--listen")) { if (getSocketAddressType(argv[i + 1]) != SAT_UNKNOWN) { Json::Value &addresses = updates["controller_addresses"]; if (addresses.size() == SERVER_KIT_MAX_SERVER_ENDPOINTS) { fprintf(stderr, "ERROR: you may specify up to %u --listen addresses.\n", SERVER_KIT_MAX_SERVER_ENDPOINTS); exit(1); } addresses.append(argv[i + 1]); i += 2; } else { fprintf(stderr, "ERROR: invalid address format for --listen. The address " "must be formatted as tcp://IP:PORT for TCP sockets, or unix:PATH " "for Unix domain sockets.\n"); exit(1); } } else if (p.isValueFlag(argc, i, argv[i], '\0', "--api-listen")) { if (getSocketAddressType(argv[i + 1]) != SAT_UNKNOWN) { Json::Value &addresses = updates["api_server_addresses"]; if (addresses.size() == SERVER_KIT_MAX_SERVER_ENDPOINTS) { fprintf(stderr, "ERROR: you may specify up to %u --api-listen addresses.\n", SERVER_KIT_MAX_SERVER_ENDPOINTS); exit(1); } addresses.append(argv[i + 1]); i += 2; } else { fprintf(stderr, "ERROR: invalid address format for --api-listen. The address " "must be formatted as tcp://IP:PORT for TCP sockets, or unix:PATH " "for Unix domain sockets.\n"); exit(1); } } else if (p.isValueFlag(argc, i, argv[i], '\0', "--pid-file")) { updates["pid_file"] = argv[i + 1]; i += 2; } else if (p.isValueFlag(argc, i, argv[i], '\0', "--authorize")) { vector<string> args; split(argv[i + 1], ':', args); if (args.size() < 2 || args.size() > 3) { fprintf(stderr, "ERROR: invalid format for --authorize. The syntax " "is \"[LEVEL:]USERNAME:PASSWORDFILE\".\n"); exit(1); } updates["api_server_authorizations"].append(argv[i + 1]); i += 2; } else if (p.isValueFlag(argc, i, argv[i], '\0', "--socket-backlog")) { updates["controller_socket_backlog"] = argv[i + 1]; i += 2; } else if (p.isFlag(argv[i], '\0', "--no-user-switching")) { updates["user_switching"] = false; i++; } else if (p.isValueFlag(argc, i, argv[i], '\0', "--default-user")) { updates["default_user"] = argv[i + 1]; i += 2; } else if (p.isValueFlag(argc, i, argv[i], '\0', "--default-group")) { updates["default_group"] = argv[i + 1]; i += 2; } else if (p.isFlag(argv[i], '\0', "--disable-security-update-check")) { updates["security_update_checker_disabled"] = true; i++; } else if (p.isValueFlag(argc, i, argv[i], '\0', "--security-update-check-proxy")) { updates["security_update_checker_proxy_url"] = argv[i + 1]; i += 2; } else if (p.isFlag(argv[i], '\0', "--disable-anonymous-telemetry")) { updates["telemetry_collector_disabled"] = true; i++; } else if (p.isValueFlag(argc, i, argv[i], '\0', "--anonymous-telemetry-proxy")) { updates["telemetry_collector_proxy_url"] = argv[i + 1]; i += 2; } else if (p.isValueFlag(argc, i, argv[i], '\0', "--max-pool-size")) { updates["max_pool_size"] = atoi(argv[i + 1]); i += 2; } else if (p.isValueFlag(argc, i, argv[i], '\0', "--pool-idle-time")) { updates["pool_idle_time"] = atoi(argv[i + 1]); i += 2; } else if (p.isValueFlag(argc, i, argv[i], '\0', "--max-preloader-idle-time")) { updates["default_max_preloader_idle_time"] = atoi(argv[i + 1]); i += 2; } else if (p.isValueFlag(argc, i, argv[i], '\0', "--force-max-concurrent-requests-per-process")) { updates["default_force_max_concurrent_requests_per_process"] = atoi(argv[i + 1]); i += 2; } else if (p.isValueFlag(argc, i, argv[i], '\0', "--min-instances")) { updates["default_min_instances"] = atoi(argv[i + 1]); i += 2; } else if (p.isValueFlag(argc, i, argv[i], 'e', "--environment")) { updates["default_environment"] = argv[i + 1]; i += 2; } else if (p.isValueFlag(argc, i, argv[i], '\0', "--app-type")) { updates["single_app_mode_app_type"] = argv[i + 1]; i += 2; } else if (p.isValueFlag(argc, i, argv[i], '\0', "--startup-file")) { updates["single_app_mode_startup_file"] = argv[i + 1]; i += 2; } else if (p.isValueFlag(argc, i, argv[i], '\0', "--app-start-command")) { updates["single_app_mode_app_start_command"] = argv[i + 1]; i += 2; } else if (p.isValueFlag(argc, i, argv[i], '\0', "--spawn-method")) { updates["default_spawn_method"] = argv[i + 1]; i += 2; } else if (p.isFlag(argv[i], '\0', "--load-shell-envvars")) { updates["default_load_shell_envvars"] = true; i++; } else if (p.isFlag(argv[i], '\0', "--preload-bundler")) { updates["default_preload_bundler"] = true; i++; } else if (p.isFlag(argv[i], '\0', "--multi-app")) { updates["multi_app"] = true; i++; } else if (p.isFlag(argv[i], '\0', "--force-friendly-error-pages")) { updates["default_friendly_error_pages"] = true; i++; } else if (p.isFlag(argv[i], '\0', "--disable-friendly-error-pages")) { updates["default_friendly_error_pages"] = false; i++; } else if (p.isValueFlag(argc, i, argv[i], '\0', "--max-requests")) { updates["default_max_requests"] = atoi(argv[i + 1]); i += 2; } else if (p.isValueFlag(argc, i, argv[i], '\0', "--max-request-queue-size")) { updates["default_max_request_queue_size"] = atoi(argv[i + 1]); i += 2; } else if (p.isFlag(argv[i], '\0', "--sticky-sessions")) { updates["default_sticky_sessions"] = true; i++; } else if (p.isValueFlag(argc, i, argv[i], '\0', "--sticky-sessions-cookie-name")) { updates["default_sticky_sessions_cookie_name"] = argv[i + 1]; i += 2; } else if (p.isValueFlag(argc, i, argv[i], '\0', "--vary-turbocache-by-cookie")) { updates["vary_turbocache_by_cookie"] = argv[i + 1]; i += 2; } else if (p.isFlag(argv[i], '\0', "--disable-turbocaching")) { updates["turbocaching"] = false; i++; } else if (p.isFlag(argv[i], '\0', "--no-abort-websockets-on-process-shutdown")) { updates["default_abort_websockets_on_process_shutdown"] = false; i++; } else if (p.isValueFlag(argc, i, argv[i], '\0', "--ruby")) { updates["default_ruby"] = argv[i + 1]; i += 2; } else if (p.isValueFlag(argc, i, argv[i], '\0', "--nodejs")) { updates["default_nodejs"] = argv[i + 1]; i += 2; } else if (p.isValueFlag(argc, i, argv[i], '\0', "--python")) { updates["default_python"] = argv[i + 1]; i += 2; } else if (p.isValueFlag(argc, i, argv[i], '\0', "--meteor-app-settings")) { updates["default_meteor_app_settings"] = argv[i + 1]; i += 2; } else if (p.isValueFlag(argc, i, argv[i], '\0', "--app-file-descriptor-ulimit")) { updates["default_app_file_descriptor_ulimit"] = atoi(argv[i + 1]); i += 2; } else if (p.isValueFlag(argc, i, argv[i], '\0', "--log-level")) { updates["log_level"] = atoi(argv[i + 1]); i += 2; } else if (p.isValueFlag(argc, i, argv[i], '\0', "--log-file")) { updates["log_target"] = argv[i + 1]; i += 2; } else if (p.isValueFlag(argc, i, argv[i], '\0', "--fd-log-file")) { updates["file_descriptor_log_target"] = argv[i + 1]; i += 2; } else if (p.isValueFlag(argc, i, argv[i], '\0', "--stat-throttle-rate")) { updates["stat_throttle_rate"] = atoi(argv[i + 1]); i += 2; } else if (p.isFlag(argv[i], '\0', "--no-show-version-in-header")) { updates["show_version_in_header"] = false; i++; } else if (p.isValueFlag(argc, i, argv[i], '\0', "--data-buffer-dir")) { updates["controller_file_buffered_channel_buffer_dir"] = atoi(argv[i + 1]); i += 2; } else if (p.isFlag(argv[i], '\0', "--no-graceful-exit")) { updates["graceful_exit"] = false; i++; } else if (p.isValueFlag(argc, i, argv[i], '\0', "--benchmark")) { updates["benchmark_mode"] = argv[i + 1]; i += 2; } else if (p.isFlag(argv[i], '\0', "--disable-selfchecks")) { updates["pool_selfchecks"] = false; i++; } else if (p.isValueFlag(argc, i, argv[i], '\0', "--threads")) { updates["controller_threads"] = atoi(argv[i + 1]); i += 2; } else if (p.isFlag(argv[i], '\0', "--cpu-affine")) { updates["controller_cpu_affine"] = true; i++; } else if (p.isValueFlag(argc, i, argv[i], '\0', "--core-file-descriptor-ulimit")) { updates["file_descriptor_ulimit"] = atoi(argv[i + 1]); i += 2; } else if (p.isValueFlag(argc, i, argv[i], '\0', "--admin-panel-url")) { updates["admin_panel_url"] = argv[i + 1]; i += 2; } else if (p.isValueFlag(argc, i, argv[i], '\0', "--ctl")) { const char *sep = strchr(argv[i + 1], '='); if (sep == NULL) { fprintf(stderr, "ERROR: invalid --ctl format: %s\n", argv[i + 1]); exit(1); } string name(argv[i + 1], sep - argv[i + 1]); string value(sep + 1); updates[name] = autocastValueToJson(value); i += 2; } else if (!startsWith(argv[i], "-")) { if (!updates.isMember("single_app_mode_app_root")) { updates["single_app_mode_app_root"] = argv[i]; i++; } else { fprintf(stderr, "ERROR: you may not pass multiple application directories. " "Please type '%s core --help' for usage.\n", argv[0]); exit(1); } } else { return false; } return true; } } // namespace Passenger #endif /* _PASSENGER_CORE_OPTION_PARSER_H_ */ ResponseCache.h 0000644 00000045401 14756456557 0007473 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2014-2018 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef _PASSENGER_RESPONSE_CACHE_H_ #define _PASSENGER_RESPONSE_CACHE_H_ #include <boost/cstdint.hpp> #include <time.h> #include <cassert> #include <cstring> #include <DataStructures/HashedStaticString.h> #include <ServerKit/http_parser.h> #include <ServerKit/CookieUtils.h> #include <StaticString.h> #include <StrIntTools/DateParsing.h> #include <StrIntTools/StrIntUtils.h> namespace Passenger { /** * Relevant RFCs: * https://tools.ietf.org/html/rfc7234 HTTP 1.1 Caching * https://tools.ietf.org/html/rfc2109 HTTP State Management Mechanism */ template<typename Request> class ResponseCache { public: static const unsigned int MAX_ENTRIES = 8; // Fits in exactly 2 cache lines static const unsigned int MAX_KEY_LENGTH = 256; static const unsigned int MAX_HEADER_SIZE = 4096; static const unsigned int MAX_BODY_SIZE = 1024 * 32; static const unsigned int DEFAULT_HEURISTIC_FRESHNESS = 10; static const unsigned int MIN_HEURISTIC_FRESHNESS = 1; struct Header { bool valid; unsigned short keySize; boost::uint32_t hash; time_t date; Header() : valid(false), keySize(0), hash(0), date(0) { } }; struct Body { unsigned short httpHeaderSize; unsigned short httpBodySize; time_t expiryDate; char key[MAX_KEY_LENGTH]; char httpHeaderData[MAX_HEADER_SIZE]; // This data is dechunked. char httpBodyData[MAX_BODY_SIZE]; Body() : httpHeaderSize(0), httpBodySize(0), expiryDate(0) { key[0] = httpHeaderData[0] = httpBodyData[0] = '\0'; } }; struct Entry { unsigned int index; Header *header; Body *body; enum { NOT_FOUND, NOT_FRESH } cacheMissReason; Entry() : index(0), header(NULL), body(NULL) { } Entry(unsigned int i, Header *h, Body *b) : index(i), header(h), body(b) { } OXT_FORCE_INLINE bool valid() const { return header != NULL; } const char *getCacheMissReasonString() const { switch (cacheMissReason) { case NOT_FOUND: return "NOT_FOUND"; case NOT_FRESH: return "NOT_FRESH"; default: return "UNKNOWN"; } } }; private: HashedStaticString HOST; HashedStaticString CACHE_CONTROL; HashedStaticString PRAGMA_CONST; HashedStaticString AUTHORIZATION; HashedStaticString VARY; HashedStaticString WWW_AUTHENTICATE; HashedStaticString X_SENDFILE; HashedStaticString X_ACCEL_REDIRECT; HashedStaticString EXPIRES; HashedStaticString LAST_MODIFIED; HashedStaticString LOCATION; HashedStaticString CONTENT_LOCATION; HashedStaticString COOKIE; HashedStaticString PASSENGER_VARY_TURBOCACHE_BY_COOKIE; unsigned int fetches, hits, stores, storeSuccesses; Header headers[MAX_ENTRIES]; Body bodies[MAX_ENTRIES]; unsigned int calculateKeyLength(const LString * restrict host, const LString * restrict varyCookie, const StaticString &path) { unsigned int size = 1 // protocol flag + ((host != NULL) ? host->size : 0) + 1 // '\n' + path.size() + ((varyCookie != NULL) ? (varyCookie->size + 1) : 0); if (size > MAX_KEY_LENGTH) { return 0; } else { return size; } } void generateKey(bool https, const StaticString &path, const LString * restrict host, const LString * restrict varyCookie, char * restrict output, unsigned int size) { char *pos = output; const char *end = output + size; const LString::Part *part; if (https) { pos = appendData(pos, end, "S", 1); } else { pos = appendData(pos, end, "H", 1); } if (host != NULL) { part = host->start; while (part != NULL) { pos = appendData(pos, end, part->data, part->size); part = part->next; } } pos = appendData(pos, end, "\n", 1); pos = appendData(pos, end, path); if (varyCookie != NULL) { pos = appendData(pos, end, "\n", 1); part = varyCookie->start; while (part != NULL) { pos = appendData(pos, end, part->data, part->size); part = part->next; } } } bool statusCodeIsCacheableByDefault(unsigned int code) const { if (code / 100 == 2) { return code == 200 || code == 203 || code == 204; } else { switch (code / 100) { case 3: return code == 300 || code == 301; case 4: return code == 404 || code == 405 || code == 410 || code == 414; case 5: return code == 501; default: return false; } } } Entry lookup(const HashedStaticString &cacheKey) { for (unsigned int i = 0; i < MAX_ENTRIES; i++) { if (headers[i].valid && headers[i].hash == cacheKey.hash() && cacheKey == StaticString(bodies[i].key, headers[i].keySize)) { return Entry(i, &headers[i], &bodies[i]); } } return Entry(); } Entry lookupInvalidOrOldest() { int oldest = -1; for (unsigned int i = 0; i < MAX_ENTRIES; i++) { if (!headers[i].valid) { return Entry(i, &headers[i], &bodies[i]); } else if (oldest == -1 || headers[i].date < headers[oldest].date) { oldest = i; } } return Entry(oldest, &headers[oldest], &bodies[oldest]); } OXT_FORCE_INLINE void erase(unsigned int index) { headers[index].valid = false; } time_t parseDate(psg_pool_t *pool, const LString *date, ev_tstamp now) const { if (date == NULL || date->size == 0) { return (time_t) now; } struct tm tm; int zone; // Try to parse it as an IMF-fixdate. // We don't support any other formats. It's too much hassle. date = psg_lstr_make_contiguous(date, pool); if (parseImfFixdate(date->start->data, date->start->data + date->size, tm, zone)) { return parsedDateToTimestamp(tm, zone); } else { return (time_t) -1; } } time_t determineExpiryDate(const Request *req, time_t responseDate, ev_tstamp now) const { const LString *value = req->appResponse.expiresHeader; if (value != NULL) { struct tm tm; int zone; if (parseImfFixdate(value->start->data, value->start->data + value->size, tm, zone)) { return parsedDateToTimestamp(tm, zone); } else { return (time_t) -1; } } value = req->appResponse.cacheControl; if (value != NULL) { StaticString cacheControl(value->start->data, value->size); string::size_type pos = cacheControl.find(P_STATIC_STRING("max-age")); if (pos != string::npos && cacheControl.size() > pos + 1) { unsigned int maxAge = stringToUint(cacheControl.substr( pos + (sizeof("max-age") - 1) + 1)); if (maxAge == 0) { // Parse error or max-age=0 return (time_t) - 1; } else { return (time_t) now + maxAge; } } } value = req->appResponse.lastModifiedHeader; if (value != NULL) { struct tm tm; int zone; if (parseImfFixdate(value->start->data, value->start->data + value->size, tm, zone)) { time_t lastModified = parsedDateToTimestamp(tm, zone); if (lastModified < now) { time_t diff = (time_t) now - lastModified; return time_t(now + std::max<double>(diff * 0.1, MIN_HEURISTIC_FRESHNESS)); } } else { return (time_t) now + 1; } } return now + DEFAULT_HEURISTIC_FRESHNESS; } bool isFresh(const Entry &entry, ev_tstamp now) const { return entry.body->expiryDate > now; } StaticString extractHostNameWithPortFromParsedUrl(struct http_parser_url &url, const LString *value) const { assert(url.field_set & (1 << UF_HOST)); if (url.field_set & (1 << UF_PORT)) { unsigned int portEnd = url.field_data[UF_PORT].off + url.field_data[UF_PORT].len; return StaticString(value->start->data + url.field_data[UF_HOST].off, portEnd - url.field_data[UF_HOST].off); } else { return StaticString(value->start->data + url.field_data[UF_HOST].off, url.field_data[UF_HOST].len); } } void invalidateLocation(Request *req, const HashedStaticString &header) { const LString *value = req->appResponse.headers.lookup(header); if (value == NULL || value->size == 0) { return; } StaticString path; bool https; value = psg_lstr_make_contiguous(value, req->pool); if (psg_lstr_first_byte(value) != '/') { // Maybe it is a full URL. Parse the host name. struct http_parser_url url; int ret = http_parser_parse_url(value->start->data, value->size, 0, &url); if (ret != 0) { // Invalid URL. return; } if (!(url.field_set & (1 << UF_HOST))) { // Invalid URL. return; } StaticString host = extractHostNameWithPortFromParsedUrl(url, value); if (host.size() != req->host->size) { // The host names don't match. return; } char *lowercaseHost = (char *) psg_pnalloc(req->pool, host.size()); convertLowerCase((const unsigned char *) host.data(), (unsigned char *) lowercaseHost, host.size()); host = StaticString(lowercaseHost, host.size()); char *lowercaseReqHost = (char *) psg_pnalloc(req->pool, req->host->size); convertLowerCase((const unsigned char *) req->host->start->data, (unsigned char *) lowercaseReqHost, req->host->size); if (memcmp(host.data(), lowercaseReqHost, req->host->size) != 0) { // The host names don't match. return; } if (url.field_set & (1 << UF_PATH)) { path = StaticString(value->start->data + url.field_data[UF_PATH].off, url.field_data[UF_PATH].len); } else { path = P_STATIC_STRING("/"); } if (url.field_set & (1 << UF_SCHEMA)) { StaticString schema(value->start->data + url.field_data[UF_SCHEMA].off, url.field_data[UF_SCHEMA].len); https = schema == "https"; } else { https = req->https; } } else { path = StaticString(value->start->data, value->size); https = req->https; } unsigned int keySize = calculateKeyLength(req->host, req->varyCookie, path); if (keySize == 0) { return; } char *key = (char *) psg_pnalloc(req->pool, keySize); generateKey(https, path, req->host, req->varyCookie, key, keySize); Entry entry(lookup(StaticString(key, keySize))); if (entry.valid()) { entry.header->valid = false; } } public: ResponseCache() : CACHE_CONTROL("cache-control"), PRAGMA_CONST("pragma"), AUTHORIZATION("authorization"), VARY("vary"), WWW_AUTHENTICATE("www-authenticate"), X_SENDFILE("x-sendfile"), X_ACCEL_REDIRECT("x-accel-redirect"), EXPIRES("expires"), LAST_MODIFIED("last-modified"), LOCATION("location"), CONTENT_LOCATION("content-location"), COOKIE("cookie"), PASSENGER_VARY_TURBOCACHE_BY_COOKIE("!~PASSENGER_VARY_TURBOCACHE_COOKIE"), fetches(0), hits(0), stores(0), storeSuccesses(0) { } OXT_FORCE_INLINE unsigned int getFetches() const { return fetches; } OXT_FORCE_INLINE unsigned int getHits() const { return hits; } OXT_FORCE_INLINE double getHitRatio() const { return hits / (double) fetches; } OXT_FORCE_INLINE unsigned int getStores() const { return fetches; } OXT_FORCE_INLINE unsigned int getStoreSuccesses() const { return storeSuccesses; } OXT_FORCE_INLINE double getStoreSuccessRatio() const { return storeSuccesses / (double) stores; } // For decreasing the store success ratio without calling store(). OXT_FORCE_INLINE void incStores() { stores++; } void resetStatistics() { fetches = 0; hits = 0; stores = 0; storeSuccesses = 0; } void clear() { for (unsigned int i = 0; i < MAX_ENTRIES; i++) { headers[i].valid = false; } } /** * Prepares the request for caching operations (fetching and storing). * Returns whether caching operations are available for this request. * * @post result == !req->cacheKey.empty() */ template<typename Controller> bool prepareRequest(Controller *controller, Request *req) { if (req->upgraded() || req->host == NULL) { return false; } LString *varyCookieName = req->secureHeaders.lookup(PASSENGER_VARY_TURBOCACHE_BY_COOKIE); if (varyCookieName == NULL && !req->config->defaultVaryTurbocacheByCookie.empty()) { varyCookieName = (LString *) psg_palloc(req->pool, sizeof(LString)); psg_lstr_init(varyCookieName); psg_lstr_append(varyCookieName, req->pool, req->config->defaultVaryTurbocacheByCookie.data(), req->config->defaultVaryTurbocacheByCookie.size()); } if (varyCookieName != NULL) { LString *cookieHeader = req->headers.lookup(COOKIE); if (cookieHeader != NULL) { req->varyCookie = ServerKit::findCookie(req->pool, cookieHeader, varyCookieName); } } unsigned int size = calculateKeyLength(req->host, req->varyCookie, StaticString(req->path.start->data, req->path.size)); if (size == 0) { req->cacheKey = HashedStaticString(); return false; } req->cacheControl = req->headers.lookup(CACHE_CONTROL); if (req->cacheControl == NULL) { // hasPragmaHeader is only used by requestAllowsFetching(), // so if there is no Cache-Control header then it's not // necessary to check for the Pragma header. req->hasPragmaHeader = req->headers.lookup(PRAGMA_CONST) != NULL; } char *key = (char *) psg_pnalloc(req->pool, size); generateKey(req->https, StaticString(req->path.start->data, req->path.size), req->host, req->varyCookie, key, size); req->cacheKey = HashedStaticString(key, size); return true; } // @pre prepareRequest() returned true bool requestAllowsFetching(Request *req) const { return (req->method == HTTP_GET || req->method == HTTP_HEAD) && req->cacheControl == NULL && !req->hasPragmaHeader; } // @pre requestAllowsFetching() Entry fetch(Request *req, ev_tstamp now) { fetches++; if (OXT_UNLIKELY(fetches == 0)) { // Value rolled over fetches = 1; hits = 0; } Entry entry(lookup(req->cacheKey)); if (entry.valid()) { hits++; if (isFresh(entry, now)) { return entry; } else { erase(entry.index); Entry result; result.cacheMissReason = Entry::NOT_FRESH; return result; } } else { entry.cacheMissReason = Entry::NOT_FOUND; return entry; } } // @pre prepareRequest() returned true OXT_FORCE_INLINE bool requestAllowsStoring(Request *req) const { return req->method != HTTP_HEAD && requestAllowsFetching(req); } // @pre prepareRequest() returned true bool prepareRequestForStoring(Request *req) { if (!statusCodeIsCacheableByDefault(req->appResponse.statusCode)) { return false; } ServerKit::HeaderTable &respHeaders = req->appResponse.headers; req->appResponse.cacheControl = respHeaders.lookup(CACHE_CONTROL); if (req->appResponse.cacheControl != NULL && req->appResponse.cacheControl->size > 0) { req->appResponse.cacheControl = psg_lstr_make_contiguous( req->appResponse.cacheControl, req->pool); StaticString cacheControl = StaticString( req->appResponse.cacheControl->start->data, req->appResponse.cacheControl->size); if (cacheControl.find(P_STATIC_STRING("no-store")) != string::npos || cacheControl.find(P_STATIC_STRING("private")) != string::npos || cacheControl.find(P_STATIC_STRING("no-cache")) != string::npos) { return false; } } if (req->headers.lookup(AUTHORIZATION) != NULL || respHeaders.lookup(VARY) != NULL || respHeaders.lookup(WWW_AUTHENTICATE) != NULL || respHeaders.lookup(X_SENDFILE) != NULL || respHeaders.lookup(X_ACCEL_REDIRECT) != NULL) { return false; } req->appResponse.expiresHeader = respHeaders.lookup(EXPIRES); if (req->appResponse.expiresHeader == NULL) { // lastModifiedHeader is only used in determineExpiryDate(), // and only if expiresHeader is not present, and Cache-Control // does not contain max-age. req->appResponse.lastModifiedHeader = respHeaders.lookup(LAST_MODIFIED); if (req->appResponse.lastModifiedHeader != NULL) { req->appResponse.lastModifiedHeader = psg_lstr_make_contiguous(req->appResponse.lastModifiedHeader, req->pool); } } else { req->appResponse.expiresHeader = psg_lstr_make_contiguous(req->appResponse.expiresHeader, req->pool); } return req->appResponse.cacheControl != NULL || req->appResponse.expiresHeader != NULL; } // @pre requestAllowsStoring() // @pre prepareRequestForStoring() Entry store(Request *req, ev_tstamp now, unsigned int headerSize, unsigned int bodySize) { stores++; if (headerSize > MAX_HEADER_SIZE || bodySize > MAX_BODY_SIZE) { return Entry(); } time_t responseDate = parseDate(req->pool, req->appResponse.date, now); if (responseDate == (time_t) -1) { return Entry(); } time_t expiryDate = determineExpiryDate(req, responseDate, now); if (expiryDate == (time_t) -1) { return Entry(); } const HashedStaticString &cacheKey = req->cacheKey; Entry entry(lookup(cacheKey)); if (!entry.valid()) { entry = lookupInvalidOrOldest(); entry.header->valid = true; entry.header->hash = cacheKey.hash(); entry.header->keySize = cacheKey.size(); memcpy(entry.body->key, cacheKey.data(), cacheKey.size()); } entry.header->date = responseDate; entry.body->expiryDate = expiryDate; entry.body->httpHeaderSize = headerSize; entry.body->httpBodySize = bodySize; storeSuccesses++; return entry; } // @pre prepareRequest() returned true // @pre !requestAllowsStoring() || !prepareRequestForStoring() bool requestAllowsInvalidating(Request *req) const { return req->method != HTTP_GET; } // @pre requestAllowsInvalidating() void invalidate(Request *req) { Entry entry(lookup(req->cacheKey)); if (entry.valid()) { entry.header->valid = false; } invalidateLocation(req, LOCATION); invalidateLocation(req, CONTENT_LOCATION); } string inspect() const { stringstream stream; for (unsigned int i = 0; i < MAX_ENTRIES; i++) { time_t expiryDate = bodies[i].expiryDate; stream << " #" << i << ": valid=" << headers[i].valid << ", hash=" << headers[i].hash << ", expiryDate=" << expiryDate << ", keySize=" << headers[i].keySize << ", key=\"" << cEscapeString(StaticString(bodies[i].key, headers[i].keySize)) << "\"\n"; } return stream.str(); } }; } // namespace Passenger #endif /* _PASSENGER_RESPONSE_CACHE_H_ */ ApiServer.h 0000644 00000063154 14756456557 0006656 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2013-2018 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef _PASSENGER_CORE_API_SERVER_H_ #define _PASSENGER_CORE_API_SERVER_H_ #include <boost/config.hpp> #include <boost/scoped_ptr.hpp> #include <boost/regex.hpp> #include <oxt/thread.hpp> #include <string> #include <cstring> #include <exception> #include <sys/types.h> #include <jsoncpp/json.h> #include <modp_b64.h> #include <Core/Controller.h> #include <Core/ConfigChange.h> #include <Core/ApplicationPool/Pool.h> #include <Shared/ApiServerUtils.h> #include <Shared/ApiAccountUtils.h> #include <ServerKit/HttpServer.h> #include <DataStructures/LString.h> #include <Exceptions.h> #include <StaticString.h> #include <LoggingKit/LoggingKit.h> #include <LoggingKit/Context.h> #include <Constants.h> #include <IOTools/BufferedIO.h> #include <IOTools/MessageIO.h> #include <StrIntTools/StrIntUtils.h> namespace Passenger { namespace Core { namespace ApiServer { using namespace std; /* * BEGIN ConfigKit schema: Passenger::Core::ApiServer::Schema * (do not edit: following text is automatically generated * by 'rake configkit_schemas_inline_comments') * * accept_burst_count unsigned integer - default(32) * authorizations array - default("[FILTERED]"),secret * client_freelist_limit unsigned integer - default(0) * instance_dir string - - * min_spare_clients unsigned integer - default(0) * request_freelist_limit unsigned integer - default(1024) * start_reading_after_accept boolean - default(true) * watchdog_fd_passing_password string - secret * * END */ class Schema: public ServerKit::HttpServerSchema { private: static Json::Value normalizeAuthorizations(const Json::Value &effectiveValues) { Json::Value updates; updates["authorizations"] = ApiAccountUtils::normalizeApiAccountsJson( effectiveValues["authorizations"]); return updates; } public: Schema() : ServerKit::HttpServerSchema(false) { using namespace ConfigKit; add("instance_dir", STRING_TYPE, OPTIONAL); add("watchdog_fd_passing_password", STRING_TYPE, OPTIONAL | SECRET); add("authorizations", ARRAY_TYPE, OPTIONAL | SECRET, Json::arrayValue); addValidator(boost::bind(ApiAccountUtils::validateAuthorizationsField, "authorizations", boost::placeholders::_1, boost::placeholders::_2)); addNormalizer(normalizeAuthorizations); finalize(); } }; struct ConfigChangeRequest { ServerKit::HttpServerConfigChangeRequest forParent; boost::scoped_ptr<ApiAccountUtils::ApiAccountDatabase> apiAccountDatabase; }; class Request: public ServerKit::BaseHttpRequest { public: string body; Json::Value jsonBody; Authorization authorization; unsigned int controllerStatesGathered; vector<Json::Value> controllerStates; DEFINE_SERVER_KIT_BASE_HTTP_REQUEST_FOOTER(Passenger::Core::ApiServer::Request); }; class ApiServer: public ServerKit::HttpServer<ApiServer, ServerKit::HttpClient<Request> > { public: typedef ServerKit::HttpServer<ApiServer, ServerKit::HttpClient<Request> > ParentClass; typedef ServerKit::HttpClient<Request> Client; typedef ServerKit::HeaderTable HeaderTable; typedef Passenger::Core::ApiServer::ConfigChangeRequest ConfigChangeRequest; private: ApiAccountUtils::ApiAccountDatabase apiAccountDatabase; boost::regex serverConnectionPath; bool regex_match(const StaticString &str, const boost::regex &e) const { return boost::regex_match(str.data(), str.data() + str.size(), e); } int extractThreadNumberFromClientName(const string &clientName) const { boost::smatch results; boost::regex re("^([0-9]+)-.*"); if (!boost::regex_match(clientName, results, re)) { return -1; } if (results.size() != 2) { return -1; } return stringToUint(results.str(1)); } static void disconnectClient(Controller *controller, string clientName) { controller->disconnect(clientName); } void route(Client *client, Request *req, const StaticString &path) { if (path == P_STATIC_STRING("/server.json")) { processServerStatus(client, req); } else if (regex_match(path, serverConnectionPath)) { processServerConnectionOperation(client, req); } else if (path == P_STATIC_STRING("/pool.xml")) { processPoolStatusXml(client, req); } else if (path == P_STATIC_STRING("/pool.json")) { processPoolStatusJson(client, req); } else if (path == P_STATIC_STRING("/pool.txt")) { processPoolStatusTxt(client, req); } else if (path == P_STATIC_STRING("/pool/restart_app_group.json")) { processPoolRestartAppGroup(client, req); } else if (path == P_STATIC_STRING("/pool/detach_process.json")) { processPoolDetachProcess(client, req); } else if (path == P_STATIC_STRING("/backtraces.txt")) { apiServerProcessBacktraces(this, client, req); } else if (path == P_STATIC_STRING("/ping.json")) { apiServerProcessPing(this, client, req); } else if (path == P_STATIC_STRING("/info.json") // The "/version.json" path is deprecated || path == P_STATIC_STRING("/version.json")) { apiServerProcessInfo(this, client, req); } else if (path == P_STATIC_STRING("/shutdown.json")) { apiServerProcessShutdown(this, client, req); } else if (path == P_STATIC_STRING("/gc.json")) { processGc(client, req); } else if (path == P_STATIC_STRING("/config.json")) { processConfig(client, req); } else if (path == P_STATIC_STRING("/reinherit_logs.json")) { apiServerProcessReinheritLogs(this, client, req, config["instance_dir"].asString(), config["watchdog_fd_passing_password"].asString()); } else if (path == P_STATIC_STRING("/reopen_logs.json")) { apiServerProcessReopenLogs(this, client, req); } else { apiServerRespondWith404(this, client, req); } } void processServerConnectionOperation(Client *client, Request *req) { if (!authorizeAdminOperation(this, client, req)) { apiServerRespondWith401(this, client, req); } else if (req->method == HTTP_DELETE) { StaticString path = req->getPathWithoutQueryString(); boost::smatch results; boost::regex_match(path.toString(), results, serverConnectionPath); if (results.size() != 2) { endAsBadRequest(&client, &req, "Invalid URI"); return; } int threadNumber = extractThreadNumberFromClientName(results.str(1)); if (threadNumber < 1 || (unsigned int) threadNumber > controllers.size()) { HeaderTable headers; headers.insert(req->pool, "Content-Type", "application/json"); writeSimpleResponse(client, 400, &headers, "{ \"status\": \"error\", \"reason\": \"Invalid thread number\" }"); if (!req->ended()) { endRequest(&client, &req); } return; } controllers[threadNumber - 1]->getContext()->libev->runLater(boost::bind( disconnectClient, controllers[threadNumber - 1], results.str(1))); HeaderTable headers; headers.insert(req->pool, "Content-Type", "application/json"); writeSimpleResponse(client, 200, &headers, "{ \"status\": \"ok\" }"); if (!req->ended()) { endRequest(&client, &req); } } else { apiServerRespondWith405(this, client, req); } } void gatherControllerState(Client *client, Request *req, Controller *controller, unsigned int i) { Json::Value state = controller->inspectStateAsJson(); getContext()->libev->runLater(boost::bind(&ApiServer::controllerStateGathered, this, client, req, i, state)); } void controllerStateGathered(Client *client, Request *req, unsigned int i, Json::Value state) { if (req->ended()) { unrefRequest(req, __FILE__, __LINE__); return; } req->controllerStatesGathered++; req->controllerStates[i] = state; if (req->controllerStatesGathered == controllers.size()) { HeaderTable headers; headers.insert(req->pool, "Content-Type", "application/json"); Json::Value response; response["threads"] = (Json::UInt) controllers.size(); for (unsigned int i = 0; i < controllers.size(); i++) { string key = "thread" + toString(i + 1); response[key] = req->controllerStates[i]; } writeSimpleResponse(client, 200, &headers, psg_pstrdup(req->pool, response.toStyledString())); if (!req->ended()) { Request *req2 = req; endRequest(&client, &req2); } } unrefRequest(req, __FILE__, __LINE__); } void processServerStatus(Client *client, Request *req) { if (authorizeStateInspectionOperation(this, client, req)) { req->controllerStates.resize(controllers.size()); for (unsigned int i = 0; i < controllers.size(); i++) { refRequest(req, __FILE__, __LINE__); controllers[i]->getContext()->libev->runLater(boost::bind( &ApiServer::gatherControllerState, this, client, req, controllers[i], i)); } } else { apiServerRespondWith401(this, client, req); } } void processPoolStatusXml(Client *client, Request *req) { Authorization auth(authorize(this, client, req)); if (auth.canReadPool) { ApplicationPool2::Pool::ToXmlOptions options( parseQueryString(req->getQueryString())); options.uid = auth.uid; options.apiKey = auth.apiKey; HeaderTable headers; headers.insert(req->pool, "Content-Type", "text/xml"); writeSimpleResponse(client, 200, &headers, psg_pstrdup(req->pool, appPool->toXml(options))); if (!req->ended()) { endRequest(&client, &req); } } else { HeaderTable headers; headers.insert(req->pool, "Cache-Control", "no-cache, no-store, must-revalidate"); headers.insert(req->pool, "WWW-Authenticate", "Basic realm=\"api\""); if (clientOnUnixDomainSocket(client) && appPool->getGroupCount() == 0) { // Allow admin tools that connected through the Unix domain socket // to know that this authorization error is caused by the fact // that the pool is empty. headers.insert(req->pool, "Pool-Empty", "true"); } writeSimpleResponse(client, 401, &headers, "Unauthorized"); if (!req->ended()) { endRequest(&client, &req); } } } void processPoolStatusJson(Client *client, Request *req) { Authorization auth(authorize(this, client, req)); if (auth.canReadPool) { ApplicationPool2::Pool::ToJsonOptions options( parseQueryString(req->getQueryString())); options.uid = auth.uid; options.apiKey = auth.apiKey; HeaderTable headers; headers.insert(req->pool, "Content-Type", "application/json"); writeSimpleResponse(client, 200, &headers, psg_pstrdup(req->pool, stringifyJson(appPool->inspectConfigInAdminPanelFormat(options)))); if (!req->ended()) { endRequest(&client, &req); } } else { HeaderTable headers; headers.insert(req->pool, "Cache-Control", "no-cache, no-store, must-revalidate"); headers.insert(req->pool, "WWW-Authenticate", "Basic realm=\"api\""); if (clientOnUnixDomainSocket(client) && appPool->getGroupCount() == 0) { // Allow admin tools that connected through the Unix domain socket // to know that this authorization error is caused by the fact // that the pool is empty. headers.insert(req->pool, "Pool-Empty", "true"); } writeSimpleResponse(client, 401, &headers, "Unauthorized"); if (!req->ended()) { endRequest(&client, &req); } } } void processPoolStatusTxt(Client *client, Request *req) { Authorization auth(authorize(this, client, req)); if (auth.canReadPool) { ApplicationPool2::Pool::InspectOptions options( parseQueryString(req->getQueryString())); options.uid = auth.uid; options.apiKey = auth.apiKey; HeaderTable headers; headers.insert(req->pool, "Content-Type", "text/plain"); writeSimpleResponse(client, 200, &headers, psg_pstrdup(req->pool, appPool->inspect(options))); if (!req->ended()) { endRequest(&client, &req); } } else { HeaderTable headers; headers.insert(req->pool, "Cache-Control", "no-cache, no-store, must-revalidate"); headers.insert(req->pool, "WWW-Authenticate", "Basic realm=\"api\""); if (clientOnUnixDomainSocket(client) && appPool->getGroupCount() == 0) { // Allow admin tools that connected through the Unix domain socket // to know that this authorization error is caused by the fact // that the pool is empty. headers.insert(req->pool, "Pool-Empty", "true"); } writeSimpleResponse(client, 401, &headers, "Unauthorized"); if (!req->ended()) { endRequest(&client, &req); } } } void processPoolRestartAppGroup(Client *client, Request *req) { Authorization auth(authorize(this, client, req)); if (!auth.canModifyPool) { apiServerRespondWith401(this, client, req); } else if (req->method != HTTP_POST) { apiServerRespondWith405(this, client, req); } else if (!req->hasBody()) { endAsBadRequest(&client, &req, "Body required"); } else if (requestBodyExceedsLimit(client, req)) { apiServerRespondWith413(this, client, req); } else { req->authorization = auth; // Continues in processPoolRestartAppGroupBody(). } } void processPoolRestartAppGroupBody(Client *client, Request *req) { if (!req->jsonBody.isMember("name")) { endAsBadRequest(&client, &req, "Name required"); return; } ApplicationPool2::Pool::RestartOptions options; options.uid = req->authorization.uid; options.apiKey = req->authorization.apiKey; if (req->jsonBody.isMember("restart_method")) { string restartMethodString = req->jsonBody["restart_method"].asString(); if (restartMethodString == "blocking") { options.method = RM_BLOCKING; } else if (restartMethodString == "rolling") { options.method = RM_ROLLING; } else { endAsBadRequest(&client, &req, "Unsupported restart method"); return; } } bool result; const char *response; try { result = appPool->restartGroupByName(req->jsonBody["name"].asString(), options); } catch (const SecurityException &) { apiServerRespondWith401(this, client, req); return; } if (result) { response = "{ \"restarted\": true }"; } else { response = "{ \"restarted\": false }"; } HeaderTable headers; headers.insert(req->pool, "Content-Type", "application/json"); headers.insert(req->pool, "Cache-Control", "no-cache, no-store, must-revalidate"); writeSimpleResponse(client, 200, &headers, response); if (!req->ended()) { endRequest(&client, &req); } } void processPoolDetachProcess(Client *client, Request *req) { Authorization auth(authorize(this, client, req)); if (!auth.canModifyPool) { apiServerRespondWith401(this, client, req); } else if (req->method != HTTP_POST) { apiServerRespondWith405(this, client, req); } else if (!req->hasBody()) { endAsBadRequest(&client, &req, "Body required"); } else if (requestBodyExceedsLimit(client, req)) { apiServerRespondWith413(this, client, req); } else { req->authorization = auth; // Continues in processPoolDetachProcessBody(). } } void processPoolDetachProcessBody(Client *client, Request *req) { if (req->jsonBody.isMember("pid")) { pid_t pid = (pid_t) req->jsonBody["pid"].asUInt(); ApplicationPool2::Pool::AuthenticationOptions options; options.uid = req->authorization.uid; options.apiKey = req->authorization.apiKey; bool result; try { result = appPool->detachProcess(pid, options); } catch (const SecurityException &) { apiServerRespondWith401(this, client, req); return; } const char *response; if (result) { response = "{ \"detached\": true }"; } else { response = "{ \"detached\": false }"; } HeaderTable headers; headers.insert(req->pool, "Content-Type", "application/json"); headers.insert(req->pool, "Cache-Control", "no-cache, no-store, must-revalidate"); writeSimpleResponse(client, 200, &headers, response); if (!req->ended()) { endRequest(&client, &req); } } else { endAsBadRequest(&client, &req, "PID required"); } } static void garbageCollect(Controller *controller) { ServerKit::Context *ctx = controller->getContext(); unsigned int count; count = mbuf_pool_compact(&ctx->mbuf_pool); SKS_NOTICE_FROM_STATIC(controller, "Freed " << count << " mbufs"); controller->compact(LoggingKit::NOTICE); } void processGc(Client *client, Request *req) { if (req->method != HTTP_PUT) { apiServerRespondWith405(this, client, req); } else if (authorizeAdminOperation(this, client, req)) { HeaderTable headers; headers.insert(req->pool, "Content-Type", "application/json"); for (unsigned int i = 0; i < controllers.size(); i++) { controllers[i]->getContext()->libev->runLater(boost::bind( garbageCollect, controllers[i])); } writeSimpleResponse(client, 200, &headers, "{ \"status\": \"ok\" }"); if (!req->ended()) { endRequest(&client, &req); } } else { apiServerRespondWith401(this, client, req); } } void processConfig(Client *client, Request *req) { if (req->method == HTTP_GET) { if (!authorizeStateInspectionOperation(this, client, req)) { apiServerRespondWith401(this, client, req); return; } HeaderTable headers; headers.insert(req->pool, "Content-Type", "application/json"); writeSimpleResponse(client, 200, &headers, psg_pstrdup(req->pool, Core::inspectConfig().toStyledString())); if (!req->ended()) { endRequest(&client, &req); } } else if (req->method == HTTP_PUT) { if (!authorizeAdminOperation(this, client, req)) { apiServerRespondWith401(this, client, req); } else if (!req->hasBody()) { endAsBadRequest(&client, &req, "Body required"); } // Continue in processConfigBody() } else { apiServerRespondWith405(this, client, req); } } void processConfigBody(Client *client, Request *req) { Core::ConfigChangeRequest *changeReq = Core::createConfigChangeRequest(); refRequest(req, __FILE__, __LINE__); Core::asyncPrepareConfigChange(req->jsonBody, changeReq, boost::bind(&ApiServer::processConfigBody_prepareDone, this, client, req, boost::placeholders::_1, boost::placeholders::_2)); } void processConfigBody_prepareDone(Client *client, Request *req, const vector<ConfigKit::Error> &errors, Core::ConfigChangeRequest *changeReq) { getContext()->libev->runLater(boost::bind(&ApiServer::processConfigBody_prepareDoneInEventLoop, this, client, req, errors, changeReq)); } void processConfigBody_prepareDoneInEventLoop(Client *client, Request *req, const vector<ConfigKit::Error> &errors, Core::ConfigChangeRequest *changeReq) { if (req->ended()) { Core::freeConfigChangeRequest(changeReq); unrefRequest(req, __FILE__, __LINE__); return; } if (errors.empty()) { Core::asyncCommitConfigChange(changeReq, boost::bind(&ApiServer::processConfigBody_commitDone, this, client, req, boost::placeholders::_1)); } else { unsigned int bufsize = 2048; char *message = (char *) psg_pnalloc(req->pool, bufsize); snprintf(message, bufsize, "{ \"status\": \"error\", " "\"message\": \"Error reconfiguring: %s\" }", ConfigKit::toString(errors).c_str()); HeaderTable headers; headers.insert(req->pool, "Content-Type", "application/json"); headers.insert(req->pool, "Cache-Control", "no-cache, no-store, must-revalidate"); writeSimpleResponse(client, 500, &headers, message); if (!req->ended()) { Request *req2 = req; endRequest(&client, &req2); } Core::freeConfigChangeRequest(changeReq); unrefRequest(req, __FILE__, __LINE__); } } void processConfigBody_commitDone(Client *client, Request *req, Core::ConfigChangeRequest *changeReq) { getContext()->libev->runLater(boost::bind(&ApiServer::processConfigBody_commitDoneInEventLoop, this, client, req, changeReq)); } void processConfigBody_commitDoneInEventLoop(Client *client, Request *req, Core::ConfigChangeRequest *changeReq) { if (!req->ended()) { HeaderTable headers; headers.insert(req->pool, "Content-Type", "application/json"); headers.insert(req->pool, "Cache-Control", "no-cache, no-store, must-revalidate"); writeSimpleResponse(client, 200, &headers, "{ \"status\": \"ok\" }"); if (!req->ended()) { Request *req2 = req; endRequest(&client, &req2); } } Core::freeConfigChangeRequest(changeReq); unrefRequest(req, __FILE__, __LINE__); } bool requestBodyExceedsLimit(Client *client, Request *req, unsigned int limit = 1024 * 128) { return (req->bodyType == Request::RBT_CONTENT_LENGTH && req->aux.bodyInfo.contentLength > limit) || (req->bodyType == Request::RBT_CHUNKED && req->body.size() > limit); } protected: virtual void onRequestBegin(Client *client, Request *req) { TRACE_POINT(); StaticString path = req->getPathWithoutQueryString(); P_INFO("API request: " << http_method_str(req->method) << " " << StaticString(req->path.start->data, req->path.size)); try { route(client, req, path); } catch (const oxt::tracable_exception &e) { SKC_ERROR(client, "Exception: " << e.what() << "\n" << e.backtrace()); if (!req->ended()) { req->wantKeepAlive = false; endRequest(&client, &req); } } } virtual ServerKit::Channel::Result onRequestBody(Client *client, Request *req, const MemoryKit::mbuf &buffer, int errcode) { TRACE_POINT(); if (buffer.size() > 0) { // Data req->body.append(buffer.start, buffer.size()); if (requestBodyExceedsLimit(client, req)) { apiServerRespondWith413(this, client, req); } } else if (errcode == 0) { // EOF Json::Reader reader; if (reader.parse(req->body, req->jsonBody)) { StaticString path = req->getPathWithoutQueryString(); try { if (path == P_STATIC_STRING("/pool/restart_app_group.json")) { processPoolRestartAppGroupBody(client, req); } else if (path == P_STATIC_STRING("/pool/detach_process.json")) { processPoolDetachProcessBody(client, req); } else if (path == P_STATIC_STRING("/config.json")) { processConfigBody(client, req); } else { P_BUG("Unknown path for body processing: " << path); } } catch (const oxt::tracable_exception &e) { SKC_ERROR(client, "Exception: " << e.what() << "\n" << e.backtrace()); if (!req->ended()) { req->wantKeepAlive = false; endRequest(&client, &req); } } } else { apiServerRespondWith422(this, client, req, reader.getFormattedErrorMessages()); } } else { // Error disconnect(&client); } return ServerKit::Channel::Result(buffer.size(), false); } virtual void reinitializeRequest(Client *client, Request *req) { ParentClass::reinitializeRequest(client, req); req->controllerStatesGathered = 0; } virtual void deinitializeRequest(Client *client, Request *req) { req->body.clear(); if (!req->jsonBody.isNull()) { req->jsonBody = Json::Value(); } req->authorization = Authorization(); req->controllerStates.clear(); ParentClass::deinitializeRequest(client, req); } public: typedef ApiAccountUtils::ApiAccount ApiAccount; // Dependencies vector<Controller *> controllers; ApplicationPool2::PoolPtr appPool; EventFd *exitEvent; ApiServer(ServerKit::Context *context, const Schema &schema, const Json::Value &initialConfig, const ConfigKit::Translator &translator = ConfigKit::DummyTranslator()) : ParentClass(context, schema, initialConfig, translator), serverConnectionPath("^/server/(.+)\\.json$"), exitEvent(NULL) { apiAccountDatabase = ApiAccountUtils::ApiAccountDatabase( config["authorizations"]); } virtual void initialize() { if (appPool == NULL) { throw RuntimeException("appPool must be non-NULL"); } if (exitEvent == NULL) { throw RuntimeException("exitEvent must be non-NULL"); } ParentClass::initialize(); } virtual StaticString getServerName() const { return P_STATIC_STRING("ApiServer"); } virtual unsigned int getClientName(const Client *client, char *buf, size_t size) const { char *pos = buf; const char *end = buf + size - 1; pos = appendData(pos, end, "Adm.", 1); pos += uintToString(client->number, pos, end - pos); *pos = '\0'; return pos - buf; } const ApiAccountUtils::ApiAccountDatabase &getApiAccountDatabase() const { return apiAccountDatabase; } bool authorizeByUid(uid_t uid) const { return appPool->authorizeByUid(uid); } bool authorizeByApiKey(const ApplicationPool2::ApiKey &apiKey) const { return appPool->authorizeByApiKey(apiKey); } bool prepareConfigChange(const Json::Value &updates, vector<ConfigKit::Error> &errors, ConfigChangeRequest &req) { if (ParentClass::prepareConfigChange(updates, errors, req.forParent)) { req.apiAccountDatabase.reset(new ApiAccountUtils::ApiAccountDatabase( req.forParent.forParent.config->get("authorizations"))); } return errors.empty(); } void commitConfigChange(ConfigChangeRequest &req) BOOST_NOEXCEPT_OR_NOTHROW { ParentClass::commitConfigChange(req.forParent); apiAccountDatabase.swap(*req.apiAccountDatabase); } }; } // namespace ApiServer } // namespace Core } // namespace Passenger #endif /* _PASSENGER_CORE_API_SERVER_H_ */ ApplicationPool/Implementation.cpp 0000644 00000020353 14756456557 0013365 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2011-2018 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include <typeinfo> #include <algorithm> #include <utility> #include <cstdio> #include <sstream> #include <limits.h> #include <unistd.h> #include <boost/make_shared.hpp> #include <boost/ref.hpp> #include <boost/cstdint.hpp> #include <boost/date_time/posix_time/posix_time_types.hpp> #include <oxt/backtrace.hpp> #include <Exceptions.h> #include <Hooks.h> #include <IOTools/MessageSerialization.h> #include <Utils.h> #include <IOTools/IOUtils.h> #include <Utils/ScopeGuard.h> #include <IOTools/MessageIO.h> #include <JsonTools/JsonUtils.h> #include <Core/ApplicationPool/Pool.h> #include <Core/ApplicationPool/Group.h> #include <Core/ApplicationPool/Pool/InitializationAndShutdown.cpp> #include <Core/ApplicationPool/Pool/AnalyticsCollection.cpp> #include <Core/ApplicationPool/Pool/GarbageCollection.cpp> #include <Core/ApplicationPool/Pool/GeneralUtils.cpp> #include <Core/ApplicationPool/Pool/GroupUtils.cpp> #include <Core/ApplicationPool/Pool/ProcessUtils.cpp> #include <Core/ApplicationPool/Pool/StateInspection.cpp> #include <Core/ApplicationPool/Pool/Miscellaneous.cpp> #include <Core/ApplicationPool/Group/InitializationAndShutdown.cpp> #include <Core/ApplicationPool/Group/LifetimeAndBasics.cpp> #include <Core/ApplicationPool/Group/SessionManagement.cpp> #include <Core/ApplicationPool/Group/SpawningAndRestarting.cpp> #include <Core/ApplicationPool/Group/ProcessListManagement.cpp> #include <Core/ApplicationPool/Group/OutOfBandWork.cpp> #include <Core/ApplicationPool/Group/Miscellaneous.cpp> #include <Core/ApplicationPool/Group/InternalUtils.cpp> #include <Core/ApplicationPool/Group/StateInspection.cpp> #include <Core/ApplicationPool/Group/Verification.cpp> #include <Core/ApplicationPool/Process.cpp> #include <Core/SpawningKit/ErrorRenderer.h> namespace Passenger { namespace ApplicationPool2 { using namespace std; using namespace boost; using namespace oxt; #define TRY_COPY_EXCEPTION(klass) \ do { \ const klass *ep = dynamic_cast<const klass *>(&e); \ if (ep != NULL) { \ return boost::make_shared<klass>(*ep); \ } \ } while (false) ExceptionPtr copyException(const tracable_exception &e) { TRY_COPY_EXCEPTION(FileSystemException); TRY_COPY_EXCEPTION(TimeRetrievalException); TRY_COPY_EXCEPTION(SystemException); TRY_COPY_EXCEPTION(FileNotFoundException); TRY_COPY_EXCEPTION(EOFException); TRY_COPY_EXCEPTION(IOException); TRY_COPY_EXCEPTION(ConfigurationException); TRY_COPY_EXCEPTION(RequestQueueFullException); TRY_COPY_EXCEPTION(GetAbortedException); TRY_COPY_EXCEPTION(SpawningKit::SpawnException); TRY_COPY_EXCEPTION(InvalidModeStringException); TRY_COPY_EXCEPTION(ArgumentException); TRY_COPY_EXCEPTION(RuntimeException); TRY_COPY_EXCEPTION(TimeoutException); TRY_COPY_EXCEPTION(NonExistentUserException); TRY_COPY_EXCEPTION(NonExistentGroupException); TRY_COPY_EXCEPTION(SecurityException); TRY_COPY_EXCEPTION(SyntaxError); TRY_COPY_EXCEPTION(boost::thread_interrupted); return boost::make_shared<tracable_exception>(e); } #define TRY_RETHROW_EXCEPTION(klass) \ do { \ const klass *ep = dynamic_cast<const klass *>(&*e); \ if (ep != NULL) { \ throw klass(*ep); \ } \ } while (false) void rethrowException(const ExceptionPtr &e) { TRY_RETHROW_EXCEPTION(FileSystemException); TRY_RETHROW_EXCEPTION(TimeRetrievalException); TRY_RETHROW_EXCEPTION(SystemException); TRY_RETHROW_EXCEPTION(FileNotFoundException); TRY_RETHROW_EXCEPTION(EOFException); TRY_RETHROW_EXCEPTION(IOException); TRY_RETHROW_EXCEPTION(ConfigurationException); TRY_RETHROW_EXCEPTION(SpawningKit::SpawnException); TRY_RETHROW_EXCEPTION(RequestQueueFullException); TRY_RETHROW_EXCEPTION(GetAbortedException); TRY_RETHROW_EXCEPTION(InvalidModeStringException); TRY_RETHROW_EXCEPTION(ArgumentException); TRY_RETHROW_EXCEPTION(RuntimeException); TRY_RETHROW_EXCEPTION(TimeoutException); TRY_RETHROW_EXCEPTION(NonExistentUserException); TRY_RETHROW_EXCEPTION(NonExistentGroupException); TRY_RETHROW_EXCEPTION(SecurityException); TRY_RETHROW_EXCEPTION(SyntaxError); TRY_RETHROW_EXCEPTION(boost::lock_error); TRY_RETHROW_EXCEPTION(boost::thread_resource_error); TRY_RETHROW_EXCEPTION(boost::unsupported_thread_option); TRY_RETHROW_EXCEPTION(boost::invalid_thread_argument); TRY_RETHROW_EXCEPTION(boost::thread_permission_error); TRY_RETHROW_EXCEPTION(boost::thread_interrupted); TRY_RETHROW_EXCEPTION(boost::thread_exception); TRY_RETHROW_EXCEPTION(boost::condition_error); throw tracable_exception(*e); } void processAndLogNewSpawnException(SpawningKit::SpawnException &e, const Options &options, const Context *context) { TRACE_POINT(); SpawningKit::ErrorRenderer renderer(*context->getSpawningKitContext()); string errorId; char filename[PATH_MAX]; stringstream stream; string errorPage; UPDATE_TRACE_POINT(); if (errorId.empty()) { errorId = context->getRandomGenerator()->generateHexString(4); } e.setId(errorId); try { int fd = -1; UPDATE_TRACE_POINT(); errorPage = renderer.renderWithDetails(e); #if (defined(__linux__) && (__GLIBC__ > 2 || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 11))) || defined(__APPLE__) || defined(__FreeBSD__) snprintf(filename, PATH_MAX, "%s/passenger-error-XXXXXX.html", getSystemTempDir()); fd = mkstemps(filename, sizeof(".html") - 1); #else snprintf(filename, PATH_MAX, "%s/passenger-error.XXXXXX", getSystemTempDir()); fd = mkstemp(filename); #endif FdGuard guard(fd, NULL, 0, true); if (fd == -1) { int e = errno; throw SystemException("Cannot generate a temporary filename", e); } UPDATE_TRACE_POINT(); writeExact(fd, errorPage); } catch (const SystemException &e2) { filename[0] = '\0'; P_ERROR("Cannot render an error page: " << e2.what() << "\n" << e2.backtrace()); } UPDATE_TRACE_POINT(); stream << "Could not spawn process for application " << options.appRoot << ": " << e.what() << "\n" << " Error ID: " << errorId << "\n"; if (filename[0] != '\0') { stream << " Error details saved to: " << filename << "\n"; } P_ERROR(stream.str()); ScopedLock l(context->agentConfigSyncher); if (!context->agentConfig.isNull()) { HookScriptOptions hOptions; hOptions.name = "spawn_failed"; hOptions.spec = context->agentConfig.get("hook_spawn_failed", Json::Value()).asString(); hOptions.agentConfig = context->agentConfig; l.unlock(); hOptions.environment.push_back(make_pair("PASSENGER_APP_ROOT", options.appRoot)); hOptions.environment.push_back(make_pair("PASSENGER_APP_GROUP_NAME", options.getAppGroupName())); hOptions.environment.push_back(make_pair("PASSENGER_ERROR_MESSAGE", e.what())); hOptions.environment.push_back(make_pair("PASSENGER_ERROR_ID", errorId)); oxt::thread(boost::bind(runHookScripts, hOptions), "Hook: spawn_failed", 256 * 1024); } } void recreateString(psg_pool_t *pool, StaticString &str) { str = psg_pstrdup(pool, str); } void Session::requestOOBW() { ProcessPtr process = getProcess()->shared_from_this(); assert(process->isAlive()); process->getGroup()->requestOOBW(process); } } // namespace ApplicationPool2 } // namespace Passenger ApplicationPool/Common.h 0000644 00000016616 14756456557 0011304 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2011-2018 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef _PASSENGER_APPLICATION_POOL2_COMMON_H_ #define _PASSENGER_APPLICATION_POOL2_COMMON_H_ #include <boost/thread.hpp> #include <boost/shared_ptr.hpp> #include <boost/intrusive_ptr.hpp> #include <boost/function.hpp> #include <oxt/tracable_exception.hpp> #include <ResourceLocator.h> #include <RandomGenerator.h> #include <StaticString.h> #include <MemoryKit/palloc.h> #include <DataStructures/StringKeyTable.h> #include <Core/ApplicationPool/Options.h> #include <Core/ApplicationPool/Context.h> #include <Core/SpawningKit/Config.h> namespace tut { struct ApplicationPool2_PoolTest; } namespace Passenger { namespace ApplicationPool2 { using namespace std; using namespace boost; using namespace oxt; class Pool; class Group; class Process; class Socket; class AbstractSession; class Session; /** * The result of a Group::spawn() call. */ enum SpawnResult { // The spawn request has been honored. One or more processes are now being spawned. SR_OK, // A previous spawn request is still in progress, so this spawn request has been // ignored. Having said that, the desired result (increasing the number of processes // by one, within imposed constraints) will still be achieved. SR_IN_PROGRESS, // A non-rolling restart is currently in progress, so the spawn request cannot // be honored. SR_ERR_RESTARTING, // Unable to spawn a new process: the upper bound of the group process limits have // already been reached. // The group limit is checked before checking whether the pool is at full capacity, // so if you get this result then it is possible that the pool is also at full // capacity at the same time. SR_ERR_GROUP_UPPER_LIMITS_REACHED, // Unable to spawn a new process: the pool is at full capacity. Pool capacity is // checked after checking the group upper bound limits, so if you get this result // then it is guaranteed that the group upper bound limits have not been reached. SR_ERR_POOL_AT_FULL_CAPACITY }; /** * The result of a Group::attach() call. */ enum AttachResult { // Attaching succeeded. AR_OK, // Attaching failed: the upper bound of the group process limits have // already been reached. // The group limit is checked before checking whether the pool is at full capacity, // so if you get this result then it is possible that the pool is also at full // capacity at the same time. AR_GROUP_UPPER_LIMITS_REACHED, // Attaching failed: the pool is at full capacity. Pool capacity is // checked after checking the group upper bound limits, so if you get this result // then it is guaranteed that the group upper bound limits have not been reached. AR_POOL_AT_FULL_CAPACITY, // Attaching failed: another group is waiting for capacity, while this group is // not waiting for capacity. You should throw away the current process and let the // other group spawn, e.g. by calling `pool->possiblySpawnMoreProcessesForExistingGroups()`. // This is checked after checking for the group upper bound limits and the pool // capacity, so if you get this result then there is guaranteed to be capacity // in the current group and in the pool. AR_ANOTHER_GROUP_IS_WAITING_FOR_CAPACITY }; /** * The result of a Pool::disableProcess/Group::disable() call. Some values are only * returned by the functions, some values are only passed to the Group::disable() * callback, some values appear in all cases. */ enum DisableResult { // The process has been successfully disabled. // Returned by functions and passed to the callback. DR_SUCCESS, // The disabling of the process was canceled before completion. // The process still exists. // Only passed to the callback. DR_CANCELED, // Nothing happened: the requested process does not exist (anymore) // or was already disabled. // Returned by functions and passed to the callback. DR_NOOP, // The disabling of the process failed: an error occurred. // Returned by functions and passed to the callback. DR_ERROR, // Indicates that the process cannot be disabled immediately // and that the callback will be called later. // Only returned by functions. DR_DEFERRED }; /** * Determines the behavior of Pool::restartGroupsByName() and Group::restart(). * Specifically, determines whether to perform a rolling restart or not. */ enum RestartMethod { // Whether a rolling restart is performed, is determined by whether rolling restart // was enabled in the web server configuration (i.e. whether group->options.rollingRestart // is already true). RM_DEFAULT, // Perform a blocking restart. group->options.rollingRestart will not be changed. RM_BLOCKING, // Perform a rolling restart. group->options.rollingRestart will not be changed. RM_ROLLING }; typedef boost::shared_ptr<Pool> PoolPtr; typedef boost::shared_ptr<Group> GroupPtr; typedef boost::intrusive_ptr<Process> ProcessPtr; typedef boost::intrusive_ptr<AbstractSession> AbstractSessionPtr; typedef boost::intrusive_ptr<Session> SessionPtr; typedef boost::shared_ptr<tracable_exception> ExceptionPtr; typedef StringKeyTable<GroupPtr> GroupMap; typedef boost::function<void (const ProcessPtr &process, DisableResult result)> DisableCallback; typedef boost::function<void ()> Callback; struct GetCallback { void (*func)(const AbstractSessionPtr &session, const ExceptionPtr &e, void *userData); mutable void *userData; void operator()(const AbstractSessionPtr &session, const ExceptionPtr &e) const { func(session, e, userData); } static void call(GetCallback cb, const AbstractSessionPtr &session, const ExceptionPtr &e) { cb(session, e); } }; struct GetWaiter { Options options; GetCallback callback; GetWaiter(const Options &o, const GetCallback &cb) : options(o), callback(cb) { options.persist(o); } }; struct Ticket { boost::mutex syncher; boost::condition_variable cond; SessionPtr session; ExceptionPtr exception; }; ExceptionPtr copyException(const tracable_exception &e); void rethrowException(const ExceptionPtr &e); void processAndLogNewSpawnException(SpawningKit::SpawnException &e, const Options &options, const Context *context); void recreateString(psg_pool_t *pool, StaticString &str); } // namespace ApplicationPool2 } // namespace Passenger #endif /* _PASSENGER_APPLICATION_POOL2_COMMON_H_ */ ApplicationPool/BasicGroupInfo.h 0000644 00000005256 14756456557 0012724 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2014-2017 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef _PASSENGER_APPLICATION_POOL2_BASIC_GROUP_INFO_H_ #define _PASSENGER_APPLICATION_POOL2_BASIC_GROUP_INFO_H_ #include <string> #include <cstddef> #include <Core/ApplicationPool/Context.h> #include <Shared/ApplicationPoolApiKey.h> namespace Passenger { namespace ApplicationPool2 { class Group; /** * Contains basic Group information. This information is set during the * initialization of a Group and never changed afterwards. This struct * encapsulates that information. It is contained inside `Group` as a const * object. Because of the immutable nature of the information, multithreaded * access is safe. * * Since Process and Session sometimes need to look up this basic group * information, this struct also serves to ensure that Process and Session do * not have a direct dependency on Group, but on BasicGroupInfo instead. */ class BasicGroupInfo { public: Context *context; /** * A back pointer to the Group that this BasicGroupInfo is contained in. * May be NULL in unit tests. */ Group *group; /** * This name uniquely identifies this Group within its Pool. It can * also be used as the display name. */ std::string name; /** * This Group's unique API key. */ ApiKey apiKey; BasicGroupInfo() : context(NULL), group(NULL) { } }; } // namespace ApplicationPool2 } // namespace Passenger #endif /* _PASSENGER_APPLICATION_POOL2_BASIC_GROUP_INFO_H_ */ ApplicationPool/Session.h 0000644 00000015102 14756456557 0011464 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2011-2017 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef _PASSENGER_APPLICATION_POOL_SESSION_H_ #define _PASSENGER_APPLICATION_POOL_SESSION_H_ #include <sys/types.h> #include <boost/atomic.hpp> #include <oxt/macros.hpp> #include <oxt/system_calls.hpp> #include <oxt/backtrace.hpp> #include <Utils/ScopeGuard.h> #include <Utils/Lock.h> #include <Core/ApplicationPool/Context.h> #include <Core/ApplicationPool/BasicProcessInfo.h> #include <Core/ApplicationPool/BasicGroupInfo.h> #include <Core/ApplicationPool/Socket.h> #include <Core/ApplicationPool/AbstractSession.h> #include <Shared/ApplicationPoolApiKey.h> namespace Passenger { namespace ApplicationPool2 { using namespace oxt; /** * Represents a communication session with a process. A communication session * within Phusion Passenger is usually a single request + response but the API * allows arbitrary I/O. See Process's class overview for normal usage of Session. * * A Session object is created from a Process object. * * This class can be used outside the ApplicationPool lock, because the methods in this * class only return immutable data and only modify data inside the Session object. * However, it is not thread-safe, and so should only be accessed through 1 thread. * * You MUST destroy all Session objects before destroying the Context that * it was allocated from. Outside unit tests, Context lives in Pool, so * so in that case you must not destroy Pool before destroying all Session * objects. */ class Session: public AbstractSession { public: typedef void (*Callback)(Session *session); private: /** * Pointer to the Context that this Session was allocated from. Always * non-NULL. Allows the Session to free itself from the memory pool * inside the Context. */ Context * const context; /** * Backpointers to Socket that this Session was made from, as well as the immutable info * of the Group and Process that this Session belongs to. * * These are non-NULL if and only if the Session hasn't been closed. * This works because Group waits until all sessions are closed * before destroying a Process. */ const BasicProcessInfo *processInfo; Socket *socket; Connection connection; mutable boost::atomic<int> refcount; bool closed; void deinitiate(bool success, bool wantKeepAlive) { connection.fail = !success; connection.wantKeepAlive = wantKeepAlive; socket->checkinConnection(connection); connection.fd = -1; } void callOnInitiateFailure() { if (OXT_LIKELY(onInitiateFailure != NULL)) { onInitiateFailure(this); } } void callOnClose() { if (OXT_LIKELY(onClose != NULL)) { onClose(this); } closed = true; } void destroySelf() const { this->~Session(); LockGuard l(context->memoryManagementSyncher); context->sessionObjectPool.free(const_cast<Session *>(this)); } public: Callback onInitiateFailure; Callback onClose; Session(Context *_context, const BasicProcessInfo *_processInfo, Socket *_socket) : context(_context), processInfo(_processInfo), socket(_socket), refcount(1), closed(false), onInitiateFailure(NULL), onClose(NULL) { } ~Session() { TRACE_POINT(); // If user doesn't close() explicitly, we penalize performance. if (OXT_LIKELY(initiated())) { deinitiate(false, false); } if (OXT_LIKELY(!closed)) { callOnClose(); } } Group *getGroup() const { assert(!closed); return processInfo->groupInfo->group; } Process *getProcess() const { assert(!closed); return processInfo->process; } virtual const ApiKey &getApiKey() const { assert(!closed); return processInfo->groupInfo->apiKey; } virtual pid_t getPid() const { assert(!closed); return processInfo->pid; } virtual StaticString getGupid() const { assert(!closed); return StaticString(processInfo->gupid, processInfo->gupidSize); } virtual unsigned int getStickySessionId() const { assert(!closed); return processInfo->stickySessionId; } Socket *getSocket() const { assert(!closed); return socket; } virtual StaticString getProtocol() const { return getSocket()->protocol; } virtual void initiate(bool blocking = true) { assert(!closed); ScopeGuard g(boost::bind(&Session::callOnInitiateFailure, this)); Connection connection = socket->checkoutConnection(); connection.fail = true; if (connection.blocking && !blocking) { FdGuard g2(connection.fd, NULL, 0); setNonBlocking(connection.fd); g2.clear(); connection.blocking = false; } g.clear(); this->connection = connection; } bool initiated() const { return connection.fd != -1; } virtual int fd() const { assert(!closed); return connection.fd; } /** * This Session object becomes fully unusable after closing. */ virtual void close(bool success, bool wantKeepAlive = false) { if (OXT_LIKELY(initiated())) { deinitiate(success, wantKeepAlive); } if (OXT_LIKELY(!closed)) { callOnClose(); } processInfo = NULL; socket = NULL; } virtual bool isClosed() const { return closed; } virtual void requestOOBW(); virtual void ref() const { refcount.fetch_add(1, boost::memory_order_relaxed); } virtual void unref() const { if (refcount.fetch_sub(1, boost::memory_order_release) == 1) { boost::atomic_thread_fence(boost::memory_order_acquire); destroySelf(); } } }; } // namespace ApplicationPool2 } // namespace Passenger #endif /* _PASSENGER_APPLICATION_POOL2_SESSION_H_ */ ApplicationPool/Group/Verification.cpp 0000644 00000012077 14756456557 0014122 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2011-2017 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include <Core/ApplicationPool/Group.h> /************************************************************************* * * Correctness verification functions for ApplicationPool2::Group * *************************************************************************/ namespace Passenger { namespace ApplicationPool2 { using namespace std; using namespace boost; /**************************** * * Private methods * ****************************/ bool Group::selfCheckingEnabled() const { return pool->selfchecking; } void Group::verifyInvariants() const { // !a || b: logical equivalent of a IMPLIES b. #ifndef NDEBUG if (!selfCheckingEnabled()) { return; } LifeStatus lifeStatus = (LifeStatus) this->lifeStatus.load(boost::memory_order_relaxed); assert(enabledCount >= 0); assert(disablingCount >= 0); assert(disabledCount >= 0); assert(nEnabledProcessesTotallyBusy >= 0); assert(!( enabledCount == 0 && disablingCount > 0 ) || ( processesBeingSpawned > 0) ); assert(!( !m_spawning ) || ( enabledCount > 0 || disablingCount == 0 )); assert((lifeStatus == ALIVE) == (spawner != NULL)); // Verify getWaitlist invariants. assert(!( !getWaitlist.empty() ) || ( enabledProcesses.empty() || verifyNoRequestsOnGetWaitlistAreRoutable() )); assert(!( enabledProcesses.empty() && !m_spawning && !restarting() && !poolAtFullCapacity() ) || ( getWaitlist.empty() )); assert(!( !getWaitlist.empty() ) || ( !enabledProcesses.empty() || m_spawning || restarting() || poolAtFullCapacity() )); // Verify disableWaitlist invariants. assert((int) disableWaitlist.size() >= disablingCount); // Verify processesBeingSpawned, m_spawning and m_restarting. assert(!( processesBeingSpawned > 0 ) || ( m_spawning )); assert(!( m_restarting ) || ( processesBeingSpawned == 0 )); // Verify lifeStatus. if (lifeStatus != ALIVE) { assert(enabledCount == 0); assert(disablingCount == 0); assert(disabledCount == 0); assert(nEnabledProcessesTotallyBusy == 0); } // Verify list sizes. assert((int) enabledProcesses.size() == enabledCount); assert((int) disablingProcesses.size() == disablingCount); assert((int) disabledProcesses.size() == disabledCount); assert(nEnabledProcessesTotallyBusy <= enabledCount); #endif } void Group::verifyExpensiveInvariants() const { #ifndef NDEBUG // !a || b: logical equivalent of a IMPLIES b. if (!selfCheckingEnabled()) { return; } ProcessList::const_iterator it, end; end = enabledProcesses.end(); for (it = enabledProcesses.begin(); it != end; it++) { const ProcessPtr &process = *it; assert(process->enabled == Process::ENABLED); assert(process->isAlive()); assert(process->oobwStatus == Process::OOBW_NOT_ACTIVE || process->oobwStatus == Process::OOBW_REQUESTED); } end = disablingProcesses.end(); for (it = disablingProcesses.begin(); it != end; it++) { const ProcessPtr &process = *it; assert(process->enabled == Process::DISABLING); assert(process->isAlive()); assert(process->oobwStatus == Process::OOBW_NOT_ACTIVE || process->oobwStatus == Process::OOBW_IN_PROGRESS); } end = disabledProcesses.end(); for (it = disabledProcesses.begin(); it != end; it++) { const ProcessPtr &process = *it; assert(process->enabled == Process::DISABLED); assert(process->isAlive()); assert(process->oobwStatus == Process::OOBW_NOT_ACTIVE || process->oobwStatus == Process::OOBW_IN_PROGRESS); } foreach (const ProcessPtr &process, detachedProcesses) { assert(process->enabled == Process::DETACHED); } #endif } #ifndef NDEBUG bool Group::verifyNoRequestsOnGetWaitlistAreRoutable() const { deque<GetWaiter>::const_iterator it, end = getWaitlist.end(); for (it = getWaitlist.begin(); it != end; it++) { if (route(it->options).process != NULL) { return false; } } return true; } #endif } // namespace ApplicationPool2 } // namespace Passenger ApplicationPool/Group/InitializationAndShutdown.cpp 0000644 00000013173 14756456557 0016644 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2011-2017 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include <Core/ApplicationPool/Group.h> /************************************************************************* * * Initialization and shutdown functions for ApplicationPool2::Group * *************************************************************************/ namespace Passenger { namespace ApplicationPool2 { using namespace std; using namespace boost; /**************************** * * Private methods * ****************************/ ApiKey Group::generateApiKey(const Pool *pool) { char value[ApiKey::SIZE]; pool->getRandomGenerator()->generateAsciiString(value, ApiKey::SIZE); return ApiKey(StaticString(value, ApiKey::SIZE)); } string Group::generateUuid(const Pool *pool) { return pool->getRandomGenerator()->generateAsciiString(20); } bool Group::shutdownCanFinish() const { LifeStatus lifeStatus = (LifeStatus) this->lifeStatus.load(boost::memory_order_seq_cst); return lifeStatus == SHUTTING_DOWN && enabledCount == 0 && disablingCount == 0 && disabledCount == 0 && detachedProcesses.empty(); } /** One of the post lock actions can potentially perform a long-running * operation, so running them in a thread is advised. */ void Group::finishShutdown(boost::container::vector<Callback> &postLockActions) { TRACE_POINT(); #ifndef NDEBUG LifeStatus lifeStatus = (LifeStatus) this->lifeStatus.load(boost::memory_order_relaxed); P_ASSERT_EQ(lifeStatus, SHUTTING_DOWN); #endif P_DEBUG("Finishing shutdown of group " << info.name); if (shutdownCallback) { postLockActions.push_back(shutdownCallback); shutdownCallback = Callback(); } postLockActions.push_back(boost::bind(interruptAndJoinAllThreads, shared_from_this())); this->lifeStatus.store(SHUT_DOWN, boost::memory_order_seq_cst); selfPointer.reset(); } /**************************** * * Public methods * ****************************/ Group::Group(Pool *_pool, const Options &_options) : pool(_pool), uuid(generateUuid(_pool)) { info.context = _pool->getContext(); info.group = this; info.name = _options.getAppGroupName().toString(); info.apiKey = generateApiKey(_pool); resetOptions(_options); enabledCount = 0; disablingCount = 0; disabledCount = 0; nEnabledProcessesTotallyBusy = 0; spawner = getContext()->spawningKitFactory->create(options); restartsInitiated = 0; processesBeingSpawned = 0; m_spawning = false; m_restarting = false; lifeStatus.store(ALIVE, boost::memory_order_relaxed); lastRestartFileMtime = 0; lastRestartFileCheckTime = 0; alwaysRestartFileExists = false; if (options.restartDir.empty()) { restartFile = options.appRoot + "/tmp/restart.txt"; alwaysRestartFile = options.appRoot + "/tmp/always_restart.txt"; } else if (options.restartDir[0] == '/') { restartFile = options.restartDir + "/restart.txt"; alwaysRestartFile = options.restartDir + "/always_restart.txt"; } else { restartFile = options.appRoot + "/" + options.restartDir + "/restart.txt"; alwaysRestartFile = options.appRoot + "/" + options.restartDir + "/always_restart.txt"; } detachedProcessesCheckerActive = false; } Group::~Group() { LifeStatus lifeStatus = getLifeStatus(); if (OXT_UNLIKELY(lifeStatus == ALIVE)) { P_BUG("You must call Group::shutdown() before destroying a Group."); } assert(lifeStatus == SHUT_DOWN); assert(!detachedProcessesCheckerActive); assert(getWaitlist.empty()); } bool Group::initialize() { nullProcess = createNullProcessObject(); return true; } /** * Must be called before destroying a Group. You can optionally provide a * callback so that you are notified when shutdown has finished. * * The caller is responsible for migrating waiters on the getWaitlist. * * One of the post lock actions can potentially perform a long-running * operation, so running them in a thread is advised. */ void Group::shutdown(const Callback &callback, boost::container::vector<Callback> &postLockActions) { assert(isAlive()); assert(getWaitlist.empty()); P_DEBUG("Begin shutting down group " << info.name); shutdownCallback = callback; detachAll(postLockActions); startCheckingDetachedProcesses(true); interruptableThreads.interrupt_all(); postLockActions.push_back(boost::bind(doCleanupSpawner, spawner)); spawner.reset(); selfPointer = shared_from_this(); assert(disableWaitlist.empty()); lifeStatus.store(SHUTTING_DOWN, boost::memory_order_seq_cst); } } // namespace ApplicationPool2 } // namespace Passenger ApplicationPool/Group/ProcessListManagement.cpp 0000644 00000050704 14756456557 0015746 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2011-2017 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include <Core/ApplicationPool/Group.h> /************************************************************************* * * Process list management functions for ApplicationPool2::Group * *************************************************************************/ namespace Passenger { namespace ApplicationPool2 { using namespace std; using namespace boost; /**************************** * * Private methods * ****************************/ Process * Group::findProcessWithStickySessionId(unsigned int id) const { ProcessList::const_iterator it, end = enabledProcesses.end(); for (it = enabledProcesses.begin(); it != end; it++) { Process *process = it->get(); if (process->getStickySessionId() == id) { return process; } } return NULL; } Process * Group::findProcessWithStickySessionIdOrLowestBusyness(unsigned int id) const { int leastBusyProcessIndex = -1; int lowestBusyness = 0; unsigned int i, size = enabledProcessBusynessLevels.size(); const int *enabledProcessBusynessLevels = &this->enabledProcessBusynessLevels[0]; for (i = 0; i < size; i++) { Process *process = enabledProcesses[i].get(); if (process->getStickySessionId() == id) { return process; } else if (leastBusyProcessIndex == -1 || enabledProcessBusynessLevels[i] < lowestBusyness) { leastBusyProcessIndex = i; lowestBusyness = enabledProcessBusynessLevels[i]; } } if (leastBusyProcessIndex == -1) { return NULL; } else { return enabledProcesses[leastBusyProcessIndex].get(); } } Process * Group::findProcessWithLowestBusyness(const ProcessList &processes) const { if (processes.empty()) { return NULL; } int lowestBusyness = -1; Process *leastBusyProcess = NULL; ProcessList::const_iterator it; ProcessList::const_iterator end = processes.end(); for (it = processes.begin(); it != end; it++) { Process *process = (*it).get(); int busyness = process->busyness(); if (lowestBusyness == -1 || lowestBusyness > busyness) { lowestBusyness = busyness; leastBusyProcess = process; } } return leastBusyProcess; } /** * Cache-optimized version of findProcessWithLowestBusyness() for the common case. */ Process * Group::findEnabledProcessWithLowestBusyness() const { if (enabledProcesses.empty()) { return NULL; } int leastBusyProcessIndex = -1; int lowestBusyness = 0; unsigned int i, size = enabledProcessBusynessLevels.size(); const int *enabledProcessBusynessLevels = &this->enabledProcessBusynessLevels[0]; for (i = 0; i < size; i++) { if (leastBusyProcessIndex == -1 || enabledProcessBusynessLevels[i] < lowestBusyness) { leastBusyProcessIndex = i; lowestBusyness = enabledProcessBusynessLevels[i]; } } return enabledProcesses[leastBusyProcessIndex].get(); } /** * Adds a process to the given list (enabledProcess, disablingProcesses, disabledProcesses) * and sets the process->enabled flag accordingly. * The process must currently not be in any list. This function does not fix * getWaitlist invariants or other stuff. */ void Group::addProcessToList(const ProcessPtr &process, ProcessList &destination) { destination.push_back(process); process->setIndex(destination.size() - 1); if (&destination == &enabledProcesses) { process->enabled = Process::ENABLED; enabledCount++; enabledProcessBusynessLevels.push_back(process->busyness()); if (process->isTotallyBusy()) { nEnabledProcessesTotallyBusy++; } } else if (&destination == &disablingProcesses) { process->enabled = Process::DISABLING; disablingCount++; } else if (&destination == &disabledProcesses) { assert(process->sessions == 0); process->enabled = Process::DISABLED; disabledCount++; } else if (&destination == &detachedProcesses) { assert(process->isAlive()); process->enabled = Process::DETACHED; if (!this->options.abortWebsocketsOnProcessShutdown && this->options.appType == P_STATIC_STRING("nodejs")) { // When Passenger is not allowed to abort websockets the application needs a way to know graceful shutdown // is in progress. The solution for the most common use (Node.js) is to send a SIGINT. This is the general // termination signal for Node; later versions of pm2 also use it (with a 1.6 sec grace period, Passenger just waits) kill(process->getPid(), SIGINT); } callAbortLongRunningConnectionsCallback(process); } else { P_BUG("Unknown destination list"); } } /** * Removes a process to the given list (enabledProcess, disablingProcesses, disabledProcesses). * This function does not fix getWaitlist invariants or other stuff. */ void Group::removeProcessFromList(const ProcessPtr &process, ProcessList &source) { ProcessPtr p = process; // Keep an extra reference count just in case. source.erase(source.begin() + process->getIndex()); process->setIndex(-1); switch (process->enabled) { case Process::ENABLED: assert(&source == &enabledProcesses); enabledCount--; if (process->isTotallyBusy()) { nEnabledProcessesTotallyBusy--; } break; case Process::DISABLING: assert(&source == &disablingProcesses); disablingCount--; break; case Process::DISABLED: assert(&source == &disabledProcesses); disabledCount--; break; case Process::DETACHED: assert(&source == &detachedProcesses); break; default: P_BUG("Unknown 'enabled' state " << (int) process->enabled); } // Rebuild indices ProcessList::iterator it, end = source.end(); unsigned int i = 0; for (it = source.begin(); it != end; it++, i++) { const ProcessPtr &process = *it; process->setIndex(i); } // Rebuild enabledProcessBusynessLevels if (&source == &enabledProcesses) { enabledProcessBusynessLevels.clear(); for (it = source.begin(); it != end; it++, i++) { const ProcessPtr &process = *it; enabledProcessBusynessLevels.push_back(process->busyness()); } enabledProcessBusynessLevels.shrink_to_fit(); } } void Group::removeFromDisableWaitlist(const ProcessPtr &p, DisableResult result, boost::container::vector<Callback> &postLockActions) { deque<DisableWaiter>::const_iterator it, end = disableWaitlist.end(); deque<DisableWaiter> newList; for (it = disableWaitlist.begin(); it != end; it++) { const DisableWaiter &waiter = *it; const ProcessPtr process = waiter.process; if (process == p) { postLockActions.push_back(boost::bind(waiter.callback, p, result)); } else { newList.push_back(waiter); } } disableWaitlist = newList; } void Group::clearDisableWaitlist(DisableResult result, boost::container::vector<Callback> &postLockActions) { // This function may be called after processes in the disableWaitlist // have been disabled or enabled, so do not assume any value for // waiter.process->enabled in this function. postLockActions.reserve(postLockActions.size() + disableWaitlist.size()); while (!disableWaitlist.empty()) { const DisableWaiter &waiter = disableWaitlist.front(); postLockActions.push_back(boost::bind(waiter.callback, waiter.process, result)); disableWaitlist.pop_front(); } } void Group::enableAllDisablingProcesses(boost::container::vector<Callback> &postLockActions) { P_DEBUG("Enabling all DISABLING processes with result DR_ERROR"); deque<DisableWaiter>::iterator it, end = disableWaitlist.end(); for (it = disableWaitlist.begin(); it != end; it++) { const DisableWaiter &waiter = *it; const ProcessPtr process = waiter.process; // A process can appear multiple times in disableWaitlist. assert(process->enabled == Process::DISABLING || process->enabled == Process::ENABLED); if (process->enabled == Process::DISABLING) { removeProcessFromList(process, disablingProcesses); addProcessToList(process, enabledProcesses); P_DEBUG("Enabled process " << process->inspect()); } } clearDisableWaitlist(DR_ERROR, postLockActions); } /** * The `immediately` parameter only has effect if the detached processes checker * thread is active. It means that, if the thread is currently sleeping, it should * wake up immediately and perform work. */ void Group::startCheckingDetachedProcesses(bool immediately) { if (!detachedProcessesCheckerActive) { P_DEBUG("Starting detached processes checker"); getPool()->nonInterruptableThreads.create_thread( boost::bind(&Group::detachedProcessesCheckerMain, this, shared_from_this()), "Detached processes checker: " + getName(), POOL_HELPER_THREAD_STACK_SIZE ); detachedProcessesCheckerActive = true; } else if (detachedProcessesCheckerActive && immediately) { detachedProcessesCheckerCond.notify_all(); } } void Group::detachedProcessesCheckerMain(GroupPtr self) { TRACE_POINT(); Pool *pool = getPool(); Pool::DebugSupportPtr debug = pool->debugSupport; if (debug != NULL && debug->detachedProcessesChecker) { debug->debugger->send("About to start detached processes checker"); debug->messages->recv("Proceed with starting detached processes checker"); } boost::unique_lock<boost::mutex> lock(pool->syncher); while (true) { assert(detachedProcessesCheckerActive); if (getLifeStatus() == SHUT_DOWN || boost::this_thread::interruption_requested()) { UPDATE_TRACE_POINT(); P_DEBUG("Stopping detached processes checker"); detachedProcessesCheckerActive = false; break; } UPDATE_TRACE_POINT(); if (!detachedProcesses.empty()) { P_TRACE(2, "Checking whether any of the " << detachedProcesses.size() << " detached processes have exited..."); ProcessList::iterator it, end = detachedProcesses.end(); ProcessList processesToRemove; for (it = detachedProcesses.begin(); it != end; it++) { const ProcessPtr process = *it; switch (process->getLifeStatus()) { case Process::ALIVE: if (process->canTriggerShutdown()) { P_DEBUG("Detached process " << process->inspect() << " has 0 active sessions now. Triggering shutdown."); process->triggerShutdown(); assert(process->getLifeStatus() == Process::SHUTDOWN_TRIGGERED); } break; case Process::SHUTDOWN_TRIGGERED: if (process->canCleanup()) { P_DEBUG("Detached process " << process->inspect() << " has shut down. Cleaning up associated resources."); process->cleanup(); assert(process->getLifeStatus() == Process::DEAD); processesToRemove.push_back(process); } else if (process->shutdownTimeoutExpired()) { P_WARN("Detached process " << process->inspect() << " didn't shut down within " PROCESS_SHUTDOWN_TIMEOUT_DISPLAY ". Forcefully killing it with SIGKILL."); kill(process->getPid(), SIGKILL); } break; default: P_BUG("Unknown 'lifeStatus' state " << (int) process->getLifeStatus()); } } UPDATE_TRACE_POINT(); end = processesToRemove.end(); for (it = processesToRemove.begin(); it != end; it++) { removeProcessFromList(*it, detachedProcesses); } } UPDATE_TRACE_POINT(); if (detachedProcesses.empty()) { UPDATE_TRACE_POINT(); P_DEBUG("Stopping detached processes checker"); detachedProcessesCheckerActive = false; boost::container::vector<Callback> actions; if (shutdownCanFinish()) { UPDATE_TRACE_POINT(); finishShutdown(actions); } verifyInvariants(); verifyExpensiveInvariants(); lock.unlock(); UPDATE_TRACE_POINT(); runAllActions(actions); break; } else { UPDATE_TRACE_POINT(); verifyInvariants(); verifyExpensiveInvariants(); } // Not all processes can be shut down yet. Sleep for a while unless // someone wakes us up. UPDATE_TRACE_POINT(); detachedProcessesCheckerCond.timed_wait(lock, posix_time::milliseconds(100)); } } /**************************** * * Public methods * ****************************/ /** * Attaches the given process to this Group and mark it as enabled. This * function doesn't touch `getWaitlist` so be sure to fix its invariants * afterwards if necessary, e.g. by calling `assignSessionsToGetWaiters()`. */ AttachResult Group::attach(const ProcessPtr &process, boost::container::vector<Callback> &postLockActions) { TRACE_POINT(); assert(process->getGroup() == NULL || process->getGroup() == this); assert(process->isAlive()); assert(isAlive()); if (processUpperLimitsReached()) { return AR_GROUP_UPPER_LIMITS_REACHED; } else if (poolAtFullCapacity()) { return AR_POOL_AT_FULL_CAPACITY; } else if (!isWaitingForCapacity() && anotherGroupIsWaitingForCapacity()) { return AR_ANOTHER_GROUP_IS_WAITING_FOR_CAPACITY; } process->initializeStickySessionId(generateStickySessionId()); if (options.forceMaxConcurrentRequestsPerProcess != -1) { process->forceMaxConcurrency(options.forceMaxConcurrentRequestsPerProcess); } P_DEBUG("Attaching process " << process->inspect()); addProcessToList(process, enabledProcesses); /* Now that there are enough resources, relevant processes in * 'disableWaitlist' can be disabled. */ deque<DisableWaiter>::const_iterator it, end = disableWaitlist.end(); deque<DisableWaiter> newDisableWaitlist; for (it = disableWaitlist.begin(); it != end; it++) { const DisableWaiter &waiter = *it; const ProcessPtr process2 = waiter.process; // The same process can appear multiple times in disableWaitlist. assert(process2->enabled == Process::DISABLING || process2->enabled == Process::DISABLED); if (process2->sessions == 0) { if (process2->enabled == Process::DISABLING) { P_DEBUG("Disabling DISABLING process " << process2->inspect() << "; disable command succeeded immediately"); removeProcessFromList(process2, disablingProcesses); addProcessToList(process2, disabledProcesses); } else { P_DEBUG("Disabling (already disabled) DISABLING process " << process2->inspect() << "; disable command succeeded immediately"); } postLockActions.push_back(boost::bind(waiter.callback, process2, DR_SUCCESS)); } else { newDisableWaitlist.push_back(waiter); } } disableWaitlist = newDisableWaitlist; // Update GC sleep timer. wakeUpGarbageCollector(); postLockActions.push_back(boost::bind(&Group::runAttachHooks, this, process)); return AR_OK; } /** * Detaches the given process from this Group. This function doesn't touch * getWaitlist so be sure to fix its invariants afterwards if necessary. * `pool->detachProcessUnlocked()` does that so you should usually use * that method over this one. */ void Group::detach(const ProcessPtr &process, boost::container::vector<Callback> &postLockActions) { TRACE_POINT(); assert(process->getGroup() == this); assert(process->isAlive()); assert(isAlive()); if (process->enabled == Process::DETACHED) { P_DEBUG("Detaching process " << process->inspect() << ", which was already being detached"); return; } const ProcessPtr p = process; // Keep an extra reference just in case. P_DEBUG("Detaching process " << process->inspect()); if (process->enabled == Process::ENABLED || process->enabled == Process::DISABLING) { assert(enabledCount > 0 || disablingCount > 0); if (process->enabled == Process::ENABLED) { removeProcessFromList(process, enabledProcesses); } else { removeProcessFromList(process, disablingProcesses); removeFromDisableWaitlist(process, DR_NOOP, postLockActions); } } else { assert(process->enabled == Process::DISABLED); assert(!disabledProcesses.empty()); removeProcessFromList(process, disabledProcesses); } addProcessToList(process, detachedProcesses); startCheckingDetachedProcesses(false); postLockActions.push_back(boost::bind(&Group::runDetachHooks, this, process)); } /** * Detaches all processes from this Group. This function doesn't touch * getWaitlist so be sure to fix its invariants afterwards if necessary. */ void Group::detachAll(boost::container::vector<Callback> &postLockActions) { assert(isAlive()); P_DEBUG("Detaching all processes in group " << info.name); foreach (ProcessPtr process, enabledProcesses) { addProcessToList(process, detachedProcesses); } foreach (ProcessPtr process, disablingProcesses) { addProcessToList(process, detachedProcesses); } foreach (ProcessPtr process, disabledProcesses) { addProcessToList(process, detachedProcesses); } enabledProcesses.clear(); disablingProcesses.clear(); disabledProcesses.clear(); enabledProcessBusynessLevels.clear(); enabledCount = 0; disablingCount = 0; disabledCount = 0; nEnabledProcessesTotallyBusy = 0; clearDisableWaitlist(DR_NOOP, postLockActions); startCheckingDetachedProcesses(false); } /** * Marks the given process as enabled. This function doesn't touch getWaitlist * so be sure to fix its invariants afterwards if necessary. */ void Group::enable(const ProcessPtr &process, boost::container::vector<Callback> &postLockActions) { assert(process->getGroup() == this); assert(process->isAlive()); assert(isAlive()); if (process->enabled == Process::DISABLING) { P_DEBUG("Enabling DISABLING process " << process->inspect()); removeProcessFromList(process, disablingProcesses); addProcessToList(process, enabledProcesses); removeFromDisableWaitlist(process, DR_CANCELED, postLockActions); } else if (process->enabled == Process::DISABLED) { P_DEBUG("Enabling DISABLED process " << process->inspect()); removeProcessFromList(process, disabledProcesses); addProcessToList(process, enabledProcesses); } else { P_DEBUG("Enabling ENABLED process " << process->inspect()); } } /** * Marks the given process as disabled. Returns DR_SUCCESS, DR_DEFERRED * or DR_NOOP. If the result is DR_DEFERRED, then the callback will be * called later with the result of this action. */ DisableResult Group::disable(const ProcessPtr &process, const DisableCallback &callback) { assert(process->getGroup() == this); assert(process->isAlive()); assert(isAlive()); if (process->enabled == Process::ENABLED) { P_DEBUG("Disabling ENABLED process " << process->inspect() << "; enabledCount=" << enabledCount << ", process.sessions=" << process->sessions); assert(enabledCount >= 0); if (enabledCount == 1 && !allowSpawn()) { P_WARN("Cannot disable sole enabled process in group " << info.name << " because spawning is not allowed according to the current" << " configuration options"); return DR_ERROR; } else if (enabledCount <= 1 || process->sessions > 0) { removeProcessFromList(process, enabledProcesses); addProcessToList(process, disablingProcesses); disableWaitlist.push_back(DisableWaiter(process, callback)); if (enabledCount == 0) { /* All processes are going to be disabled, so in order * to avoid blocking requests we first spawn a new process * and disable this process after the other one is done * spawning. We do this irrespective of resource limits * because this is an exceptional situation. */ P_DEBUG("Spawning a new process to avoid the disable action from blocking requests"); spawn(); } P_DEBUG("Deferring disable command completion"); return DR_DEFERRED; } else { removeProcessFromList(process, enabledProcesses); addProcessToList(process, disabledProcesses); P_DEBUG("Disable command succeeded immediately"); return DR_SUCCESS; } } else if (process->enabled == Process::DISABLING) { assert(disablingCount > 0); disableWaitlist.push_back(DisableWaiter(process, callback)); P_DEBUG("Disabling DISABLING process " << process->inspect() << info.name << "; command queued, deferring disable command completion"); return DR_DEFERRED; } else { assert(disabledCount > 0); P_DEBUG("Disabling DISABLED process " << process->inspect() << info.name << "; disable command succeeded immediately"); return DR_NOOP; } } } // namespace ApplicationPool2 } // namespace Passenger ApplicationPool/Group/LifetimeAndBasics.cpp 0000644 00000005432 14756456557 0015003 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2011-2017 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include <Core/ApplicationPool/Group.h> /************************************************************************* * * Functions for ApplicationPool2::Group for handling life time, basic info, * backreferences and related objects * *************************************************************************/ namespace Passenger { namespace ApplicationPool2 { using namespace std; using namespace boost; /**************************** * * Public methods * ****************************/ // Thread-safe. bool Group::isAlive() const { return getLifeStatus() == ALIVE; } // Thread-safe. OXT_FORCE_INLINE Group::LifeStatus Group::getLifeStatus() const { return (LifeStatus) lifeStatus.load(boost::memory_order_seq_cst); } StaticString Group::getName() const { return info.name; } const BasicGroupInfo & Group::getInfo() { return info; } const ApiKey & Group::getApiKey() const { return info.apiKey; } /** * Thread-safe. * @pre getLifeState() != SHUT_DOWN * @post result != NULL */ OXT_FORCE_INLINE Pool * Group::getPool() const { return pool; } Context * Group::getContext() const { return info.context; } psg_pool_t * Group::getPallocPool() const { return getPool()->palloc; } const ResourceLocator & Group::getResourceLocator() const { return *getPool()->getSpawningKitContext()->resourceLocator; } const WrapperRegistry::Registry & Group::getWrapperRegistry() const { return *getPool()->getSpawningKitContext()->wrapperRegistry; } } // namespace ApplicationPool2 } // namespace Passenger ApplicationPool/Group/InternalUtils.cpp 0000644 00000024133 14756456557 0014271 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2011-2018 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include <Core/ApplicationPool/Group.h> /************************************************************************* * * Internal utility functions for ApplicationPool2::Group * *************************************************************************/ namespace Passenger { namespace ApplicationPool2 { using namespace std; using namespace boost; /**************************** * * Private methods * ****************************/ void Group::runAllActions(const boost::container::vector<Callback> &actions) { boost::container::vector<Callback>::const_iterator it, end = actions.end(); for (it = actions.begin(); it != end; it++) { (*it)(); } } void Group::interruptAndJoinAllThreads(GroupPtr self) { self->interruptableThreads.interrupt_and_join_all(); } void Group::doCleanupSpawner(SpawningKit::SpawnerPtr spawner) { spawner->cleanup(); } /** * Persists options into this Group. Called at creation time and at restart time. * Values will be persisted into `destination`. Or if it's NULL, into `this->options`. */ void Group::resetOptions(const Options &newOptions, Options *destination) { if (destination == NULL) { destination = &this->options; } *destination = newOptions; destination->persist(newOptions); destination->clearPerRequestFields(); destination->apiKey = getApiKey().toStaticString(); destination->groupUuid = uuid; } /** * Merges some of the new options from the latest get() request into this Group. */ void Group::mergeOptions(const Options &other) { options.maxRequests = other.maxRequests; options.minProcesses = other.minProcesses; options.statThrottleRate = other.statThrottleRate; options.maxPreloaderIdleTime = other.maxPreloaderIdleTime; } /* Given a hook name like "queue_full_error", we return HookScriptOptions filled in with this name and a spec * (user settings that can be queried from agentsOptions using the external hook name that is prefixed with "hook_") * * @return false if the user parameters (agentsOptions) are not available (e.g. during ApplicationPool2_PoolTest) */ bool Group::prepareHookScriptOptions(HookScriptOptions &hsOptions, const char *name) { Context *context = getPool()->getContext(); LockGuard l(context->agentConfigSyncher); if (context->agentConfig.isNull()) { return false; } hsOptions.name = name; string hookName = string("hook_") + name; hsOptions.spec = context->agentConfig.get(hookName, Json::Value()).asString(); return true; } // 'process' is not a reference so that bind(runAttachHooks, ...) causes the shared // pointer reference to increment. void Group::runAttachHooks(const ProcessPtr process) const { getPool()->runHookScripts("attached_process", boost::bind(&Group::setupAttachOrDetachHook, this, process, boost::placeholders::_1)); } void Group::runDetachHooks(const ProcessPtr process) const { getPool()->runHookScripts("detached_process", boost::bind(&Group::setupAttachOrDetachHook, this, process, boost::placeholders::_1)); } void Group::setupAttachOrDetachHook(const ProcessPtr process, HookScriptOptions &options) const { options.environment.push_back(make_pair("PASSENGER_PROCESS_PID", toString(process->getPid()))); options.environment.push_back(make_pair("PASSENGER_APP_ROOT", this->options.appRoot)); } unsigned int Group::generateStickySessionId() { unsigned int result; while (true) { result = (unsigned int) rand(); if (result != 0 && findProcessWithStickySessionId(result) == NULL) { return result; } } // Never reached; shut up compiler warning. return 0; } ProcessPtr Group::createNullProcessObject() { struct Guard { Context *context; Process *process; Guard(Context *c, Process *s) : context(c), process(s) { } ~Guard() { if (process != NULL) { context->processObjectPool.free(process); } } void clear() { process = NULL; } }; Json::Value args; args["pid"] = 0; args["gupid"] = "0"; args["spawner_creation_time"] = 0; args["spawn_start_time"] = 0; args["dummy"] = true; args["sockets"] = Json::Value(Json::arrayValue); Context *context = getContext(); LockGuard l(context->memoryManagementSyncher); Process *process = context->processObjectPool.malloc(); Guard guard(context, process); process = new (process) Process(&info, args); process->shutdownNotRequired(); guard.clear(); return ProcessPtr(process, false); } ProcessPtr Group::createProcessObject(const SpawningKit::Spawner &spawner, const SpawningKit::Result &spawnResult) { struct Guard { Context *context; Process *process; Guard(Context *c, Process *s) : context(c), process(s) { } ~Guard() { if (process != NULL) { context->processObjectPool.free(process); } } void clear() { process = NULL; } }; Json::Value args; args["spawner_creation_time"] = (Json::UInt64) spawner.creationTime; Context *context = getContext(); LockGuard l(context->memoryManagementSyncher); Process *process = context->processObjectPool.malloc(); Guard guard(context, process); process = new (process) Process(&info, spawnResult, args); guard.clear(); return ProcessPtr(process, false); } bool Group::poolAtFullCapacity() const { return getPool()->atFullCapacityUnlocked(); } ProcessPtr Group::poolForceFreeCapacity(const Group *exclude, boost::container::vector<Callback> &postLockActions) { return getPool()->forceFreeCapacity(exclude, postLockActions); } void Group::wakeUpGarbageCollector() { getPool()->garbageCollectionCond.notify_all(); } bool Group::anotherGroupIsWaitingForCapacity() const { return findOtherGroupWaitingForCapacity() != NULL; } Group * Group::findOtherGroupWaitingForCapacity() const { Pool *pool = getPool(); if (pool->groups.size() == 1) { return NULL; } GroupMap::ConstIterator g_it(pool->groups); while (*g_it != NULL) { const GroupPtr &group = g_it.getValue(); if (group.get() != this && group->isWaitingForCapacity()) { return group.get(); } g_it.next(); } return NULL; } bool Group::pushGetWaiter(const Options &newOptions, const GetCallback &callback, boost::container::vector<Callback> &postLockActions) { if (OXT_LIKELY(!testOverflowRequestQueue() && (newOptions.maxRequestQueueSize == 0 || getWaitlist.size() < newOptions.maxRequestQueueSize))) { getWaitlist.push_back(GetWaiter( newOptions.copyAndPersist(), callback)); return true; } else { postLockActions.push_back(boost::bind(GetCallback::call, callback, SessionPtr(), boost::make_shared<RequestQueueFullException>(newOptions.maxRequestQueueSize))); HookScriptOptions hsOptions; if (prepareHookScriptOptions(hsOptions, "queue_full_error")) { // TODO <Feb 17, 2015] DK> should probably rate limit this, since we are already at heavy load postLockActions.push_back(boost::bind(runHookScripts, hsOptions)); } return false; } } template<typename Lock> void Group::assignSessionsToGetWaitersQuickly(Lock &lock) { if (getWaitlist.empty()) { verifyInvariants(); lock.unlock(); return; } boost::container::small_vector<GetAction, 8> actions; unsigned int i = 0; bool done = false; actions.reserve(getWaitlist.size()); while (!done && i < getWaitlist.size()) { const GetWaiter &waiter = getWaitlist[i]; RouteResult result = route(waiter.options); if (result.process != NULL) { GetAction action; action.callback = waiter.callback; action.session = newSession(result.process); getWaitlist.erase(getWaitlist.begin() + i); actions.push_back(action); } else { done = result.finished; if (!result.finished) { i++; } } } verifyInvariants(); lock.unlock(); boost::container::small_vector<GetAction, 50>::const_iterator it, end = actions.end(); for (it = actions.begin(); it != end; it++) { it->callback(it->session, ExceptionPtr()); } } void Group::assignSessionsToGetWaiters(boost::container::vector<Callback> &postLockActions) { unsigned int i = 0; bool done = false; while (!done && i < getWaitlist.size()) { const GetWaiter &waiter = getWaitlist[i]; RouteResult result = route(waiter.options); if (result.process != NULL) { postLockActions.push_back(boost::bind( GetCallback::call, waiter.callback, newSession(result.process), ExceptionPtr())); getWaitlist.erase(getWaitlist.begin() + i); } else { done = result.finished; if (!result.finished) { i++; } } } } bool Group::testOverflowRequestQueue() const { // This has a performance penalty, although I'm not sure whether the penalty is // any greater than a hash table lookup if I were to implement it in Options. Pool::DebugSupportPtr debug = getPool()->debugSupport; if (debug) { return debug->testOverflowRequestQueue; } else { return false; } } void Group::callAbortLongRunningConnectionsCallback(const ProcessPtr &process) { Pool::AbortLongRunningConnectionsCallback callback = getPool()->abortLongRunningConnectionsCallback; if (callback) { callback(process); } } } // namespace ApplicationPool2 } // namespace Passenger ApplicationPool/Group/OutOfBandWork.cpp 0000644 00000024707 14756456557 0014167 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2011-2018 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include <Core/ApplicationPool/Group.h> #include <IOTools/MessageSerialization.h> /************************************************************************* * * Out-of-band work functions for ApplicationPool2::Group * *************************************************************************/ namespace Passenger { namespace ApplicationPool2 { using namespace std; using namespace boost; /**************************** * * Private methods * ****************************/ /** Returns whether it is allowed to perform a new OOBW in this group. */ bool Group::oobwAllowed() const { unsigned int oobwInstances = 0; foreach (const ProcessPtr &process, disablingProcesses) { if (process->oobwStatus == Process::OOBW_IN_PROGRESS) { oobwInstances += 1; } } foreach (const ProcessPtr &process, disabledProcesses) { if (process->oobwStatus == Process::OOBW_IN_PROGRESS) { oobwInstances += 1; } } return oobwInstances < options.maxOutOfBandWorkInstances; } /** Returns whether a new OOBW should be initiated for this process. */ bool Group::shouldInitiateOobw(Process *process) const { return process->oobwStatus == Process::OOBW_REQUESTED && process->enabled != Process::DETACHED && process->isAlive() && oobwAllowed(); } void Group::maybeInitiateOobw(Process *process) { if (shouldInitiateOobw(process)) { // We keep an extra reference to prevent premature destruction. ProcessPtr p = process->shared_from_this(); initiateOobw(p); } } // The 'self' parameter is for keeping the current Group object alive void Group::lockAndMaybeInitiateOobw(const ProcessPtr &process, DisableResult result, GroupPtr self) { TRACE_POINT(); // Standard resource management boilerplate stuff... Pool *pool = getPool(); boost::unique_lock<boost::mutex> lock(pool->syncher); if (OXT_UNLIKELY(!process->isAlive() || !isAlive())) { return; } assert(process->oobwStatus == Process::OOBW_IN_PROGRESS); if (result == DR_SUCCESS) { if (process->enabled == Process::DISABLED) { P_DEBUG("Process " << process->inspect() << " disabled; proceeding " << "with out-of-band work"); process->oobwStatus = Process::OOBW_REQUESTED; if (shouldInitiateOobw(process.get())) { initiateOobw(process); } else { // We do not re-enable the process because it's likely that the // administrator has explicitly changed the state. P_DEBUG("Out-of-band work for process " << process->inspect() << " aborted " "because the process no longer requests out-of-band work"); process->oobwStatus = Process::OOBW_NOT_ACTIVE; } } else { // We do not re-enable the process because it's likely that the // administrator has explicitly changed the state. P_DEBUG("Out-of-band work for process " << process->inspect() << " aborted " "because the process was reenabled after disabling"); process->oobwStatus = Process::OOBW_NOT_ACTIVE; } } else { P_DEBUG("Out-of-band work for process " << process->inspect() << " aborted " "because the process could not be disabled"); process->oobwStatus = Process::OOBW_NOT_ACTIVE; } } void Group::initiateOobw(const ProcessPtr &process) { assert(process->oobwStatus == Process::OOBW_REQUESTED); process->oobwStatus = Process::OOBW_IN_PROGRESS; if (process->enabled == Process::ENABLED || process->enabled == Process::DISABLING) { // We want the process to be disabled. However, disabling a process is potentially // asynchronous, so we pass a callback which will re-aquire the lock and call this // method again. P_DEBUG("Disabling process " << process->inspect() << " in preparation for OOBW"); DisableResult result = disable(process, boost::bind(&Group::lockAndMaybeInitiateOobw, this, boost::placeholders::_1, boost::placeholders::_2, shared_from_this())); switch (result) { case DR_SUCCESS: // Continue code flow. break; case DR_DEFERRED: // lockAndMaybeInitiateOobw() will eventually be called. return; case DR_ERROR: case DR_NOOP: P_DEBUG("Out-of-band work for process " << process->inspect() << " aborted " "because the process could not be disabled"); process->oobwStatus = Process::OOBW_NOT_ACTIVE; return; default: P_BUG("Unexpected disable() result " << result); } } assert(process->enabled == Process::DISABLED); assert(process->sessions == 0); P_DEBUG("Initiating OOBW request for process " << process->inspect()); interruptableThreads.create_thread( boost::bind(&Group::spawnThreadOOBWRequest, this, shared_from_this(), process), "OOBW request thread for process " + process->inspect(), POOL_HELPER_THREAD_STACK_SIZE); } // The 'self' parameter is for keeping the current Group object alive while this thread is running. void Group::spawnThreadOOBWRequest(GroupPtr self, ProcessPtr process) { TRACE_POINT(); boost::this_thread::disable_interruption di; boost::this_thread::disable_syscall_interruption dsi; Socket *socket; Connection connection; Pool *pool = getPool(); Pool::DebugSupportPtr debug = pool->debugSupport; UPDATE_TRACE_POINT(); P_DEBUG("Performing OOBW request for process " << process->inspect()); if (debug != NULL && debug->oobw) { debug->debugger->send("OOBW request about to start"); debug->messages->recv("Proceed with OOBW request"); } UPDATE_TRACE_POINT(); { // Standard resource management boilerplate stuff... boost::unique_lock<boost::mutex> lock(pool->syncher); if (OXT_UNLIKELY(!process->isAlive() || process->enabled == Process::DETACHED || !isAlive())) { return; } if (process->enabled != Process::DISABLED) { UPDATE_TRACE_POINT(); P_INFO("Out-of-Band Work canceled: process " << process->inspect() << " was concurrently re-enabled."); if (debug != NULL && debug->oobw) { debug->debugger->send("OOBW request canceled"); } return; } assert(process->oobwStatus == Process::OOBW_IN_PROGRESS); assert(process->sessions == 0); socket = process->findSocketsAcceptingHttpRequestsAndWithLowestBusyness(); } UPDATE_TRACE_POINT(); unsigned long long timeout = 1000 * 1000 * 60; // 1 min try { boost::this_thread::restore_interruption ri(di); boost::this_thread::restore_syscall_interruption rsi(dsi); // Grab a connection. The connection is marked as fail in order to // ensure it is closed / recycled after this request (otherwise we'd // need to completely read the response). connection = socket->checkoutConnection(); connection.fail = true; ScopeGuard guard(boost::bind(&Socket::checkinConnection, socket, connection)); // This is copied from Core::Controller when it is sending data using the // "session" protocol. char sizeField[sizeof(boost::uint32_t)]; boost::container::small_vector<StaticString, 10> data; data.push_back(StaticString(sizeField, sizeof(boost::uint32_t))); data.push_back(P_STATIC_STRING_WITH_NULL("REQUEST_METHOD")); data.push_back(P_STATIC_STRING_WITH_NULL("OOBW")); data.push_back(P_STATIC_STRING_WITH_NULL("PASSENGER_CONNECT_PASSWORD")); data.push_back(getApiKey().toStaticString()); data.push_back(StaticString("", 1)); boost::uint32_t dataSize = 0; for (unsigned int i = 1; i < data.size(); i++) { dataSize += (boost::uint32_t) data[i].size(); } Uint32Message::generate(sizeField, dataSize); gatheredWrite(connection.fd, &data[0], data.size(), &timeout); // We do not care what the actual response is ... just wait for it. UPDATE_TRACE_POINT(); waitUntilReadable(connection.fd, &timeout); } catch (const SystemException &e) { P_ERROR("*** ERROR: " << e.what() << "\n" << e.backtrace()); } catch (const TimeoutException &e) { P_ERROR("*** ERROR: " << e.what() << "\n" << e.backtrace()); } UPDATE_TRACE_POINT(); boost::container::vector<Callback> actions; { // Standard resource management boilerplate stuff... Pool *pool = getPool(); boost::unique_lock<boost::mutex> lock(pool->syncher); if (OXT_UNLIKELY(!process->isAlive() || !isAlive())) { return; } process->oobwStatus = Process::OOBW_NOT_ACTIVE; if (process->enabled == Process::DISABLED) { enable(process, actions); assignSessionsToGetWaiters(actions); } pool->fullVerifyInvariants(); initiateNextOobwRequest(); } UPDATE_TRACE_POINT(); runAllActions(actions); actions.clear(); UPDATE_TRACE_POINT(); P_DEBUG("Finished OOBW request for process " << process->inspect()); if (debug != NULL && debug->oobw) { debug->debugger->send("OOBW request finished"); } } void Group::initiateNextOobwRequest() { ProcessList::const_iterator it, end = enabledProcesses.end(); for (it = enabledProcesses.begin(); it != end; it++) { const ProcessPtr &process = *it; if (shouldInitiateOobw(process.get())) { // We keep an extra reference to processes to prevent premature destruction. ProcessPtr p = process; initiateOobw(p); return; } } } /**************************** * * Public methods * ****************************/ // Thread-safe, but only call outside the pool lock! void Group::requestOOBW(const ProcessPtr &process) { // Standard resource management boilerplate stuff... Pool *pool = getPool(); boost::unique_lock<boost::mutex> lock(pool->syncher); if (isAlive() && process->isAlive() && process->oobwStatus == Process::OOBW_NOT_ACTIVE) { process->oobwStatus = Process::OOBW_REQUESTED; } } } // namespace ApplicationPool2 } // namespace Passenger ApplicationPool/Group/StateInspection.cpp 0000644 00000027225 14756456557 0014615 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2011-2018 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include <Core/ApplicationPool/Group.h> #include <FileTools/PathManip.h> #include <cassert> #include <modp_b64.h> /************************************************************************* * * Session management functions for ApplicationPool2::Group * *************************************************************************/ namespace Passenger { namespace ApplicationPool2 { using namespace std; using namespace boost; /**************************** * * Public methods * ****************************/ unsigned int Group::getProcessCount() const { return enabledCount + disablingCount + disabledCount; } /** * Returns whether the lower bound of the group-specific process limits * have been satisfied. Note that even if the result is false, the pool limits * may not allow spawning, so you should check `pool->atFullCapacity()` too. */ bool Group::processLowerLimitsSatisfied() const { return capacityUsed() >= options.minProcesses; } /** * Returns whether the upper bound of the group-specific process limits have * been reached, or surpassed. Does not check whether pool limits have been * reached. Use `pool->atFullCapacity()` to check for that. */ bool Group::processUpperLimitsReached() const { // check maxInstances limit as set by Enterprise (OSS maxInstancesPerApp piggybacks on this, // see InitRequest.cpp) return options.maxProcesses != 0 && capacityUsed() >= options.maxProcesses; } /** * Returns whether all enabled processes are totally busy. If so, another * process should be spawned, if allowed by the process limits. * Returns false if there are no enabled processes. */ bool Group::allEnabledProcessesAreTotallyBusy() const { return nEnabledProcessesTotallyBusy == enabledCount && enabledCount > 0; } /** * Returns the number of processes in this group that should be part of the * ApplicationPool process limits calculations. */ unsigned int Group::capacityUsed() const { return enabledCount + disablingCount + disabledCount + processesBeingSpawned; } /** * Checks whether this group is waiting for capacity on the pool to * become available before it can continue processing requests. */ bool Group::isWaitingForCapacity() const { return enabledProcesses.empty() && processesBeingSpawned == 0 && !m_restarting && !getWaitlist.empty(); } bool Group::garbageCollectable(unsigned long long now) const { /* if (now == 0) { now = SystemTime::getUsec(); } return busyness() == 0 && getWaitlist.empty() && disabledProcesses.empty() && options.getMaxPreloaderIdleTime() != 0 && now - spawner->lastUsed() > (unsigned long long) options.getMaxPreloaderIdleTime() * 1000000; */ return false; } void Group::inspectXml(std::ostream &stream, bool includeSecrets) const { ProcessList::const_iterator it; stream << "<name>" << escapeForXml(info.name) << "</name>"; stream << "<component_name>" << escapeForXml(info.name) << "</component_name>"; stream << "<app_root>" << escapeForXml(options.appRoot) << "</app_root>"; stream << "<app_type>" << escapeForXml(options.appType) << "</app_type>"; stream << "<environment>" << escapeForXml(options.environment) << "</environment>"; stream << "<uuid>" << toString(uuid) << "</uuid>"; stream << "<enabled_process_count>" << enabledCount << "</enabled_process_count>"; stream << "<disabling_process_count>" << disablingCount << "</disabling_process_count>"; stream << "<disabled_process_count>" << disabledCount << "</disabled_process_count>"; stream << "<capacity_used>" << capacityUsed() << "</capacity_used>"; stream << "<get_wait_list_size>" << getWaitlist.size() << "</get_wait_list_size>"; stream << "<disable_wait_list_size>" << disableWaitlist.size() << "</disable_wait_list_size>"; stream << "<processes_being_spawned>" << processesBeingSpawned << "</processes_being_spawned>"; if (m_spawning) { stream << "<spawning/>"; } if (restarting()) { stream << "<restarting/>"; } if (includeSecrets) { stream << "<secret>" << escapeForXml(getApiKey().toStaticString()) << "</secret>"; stream << "<api_key>" << escapeForXml(getApiKey().toStaticString()) << "</api_key>"; } LifeStatus lifeStatus = (LifeStatus) this->lifeStatus.load(boost::memory_order_relaxed); switch (lifeStatus) { case ALIVE: stream << "<life_status>ALIVE</life_status>"; break; case SHUTTING_DOWN: stream << "<life_status>SHUTTING_DOWN</life_status>"; break; case SHUT_DOWN: stream << "<life_status>SHUT_DOWN</life_status>"; break; default: P_BUG("Unknown 'lifeStatus' state " << lifeStatus); } SpawningKit::UserSwitchingInfo usInfo(SpawningKit::prepareUserSwitching(options, getWrapperRegistry())); stream << "<user>" << escapeForXml(usInfo.username) << "</user>"; stream << "<uid>" << usInfo.uid << "</uid>"; stream << "<group>" << escapeForXml(usInfo.groupname) << "</group>"; stream << "<gid>" << usInfo.gid << "</gid>"; stream << "<options>"; options.toXml(stream, getResourceLocator(), getWrapperRegistry()); stream << "</options>"; stream << "<processes>"; for (it = enabledProcesses.begin(); it != enabledProcesses.end(); it++) { stream << "<process>"; (*it)->inspectXml(stream, includeSecrets); stream << "</process>"; } for (it = disablingProcesses.begin(); it != disablingProcesses.end(); it++) { stream << "<process>"; (*it)->inspectXml(stream, includeSecrets); stream << "</process>"; } for (it = disabledProcesses.begin(); it != disabledProcesses.end(); it++) { stream << "<process>"; (*it)->inspectXml(stream, includeSecrets); stream << "</process>"; } for (it = detachedProcesses.begin(); it != detachedProcesses.end(); it++) { stream << "<process>"; (*it)->inspectXml(stream, includeSecrets); stream << "</process>"; } stream << "</processes>"; } void Group::inspectPropertiesInAdminPanelFormat(Json::Value &result) const { result["path"] = absolutizePath(options.appRoot); result["startup_file"] = absolutizePath(options.getStartupFile(getWrapperRegistry()), absolutizePath(options.appRoot)); result["start_command"] = options.getStartCommand(getResourceLocator(), getWrapperRegistry()); result["type"] = getWrapperRegistry().lookup(options.appType).language.toString(); SpawningKit::UserSwitchingInfo usInfo(SpawningKit::prepareUserSwitching(options, getWrapperRegistry())); result["user"]["username"] = usInfo.username; result["user"]["uid"] = (Json::Int) usInfo.uid; result["group"]["groupname"] = usInfo.groupname; result["group"]["gid"] = (Json::Int) usInfo.gid; /******************/ } void Group::inspectConfigInAdminPanelFormat(Json::Value &result) const { #define VAL Pool::makeSingleValueJsonConfigFormat #define SVAL Pool::makeSingleStrValueJsonConfigFormat #define NON_EMPTY_SVAL Pool::makeSingleNonEmptyStrValueJsonConfigFormat result["app_root"] = NON_EMPTY_SVAL(absolutizePath(options.appRoot)); result["app_group_name"] = NON_EMPTY_SVAL(info.name); result["default_user"] = NON_EMPTY_SVAL(options.defaultUser); result["default_group"] = NON_EMPTY_SVAL(options.defaultGroup); result["enabled"] = VAL(true, false); result["lve_min_uid"] = VAL(options.lveMinUid, DEFAULT_LVE_MIN_UID); result["type"] = NON_EMPTY_SVAL(options.appType); result["startup_file"] = NON_EMPTY_SVAL(options.startupFile); result["start_command"] = NON_EMPTY_SVAL(replaceAll(options.appStartCommand, P_STATIC_STRING("\t"), P_STATIC_STRING(" "))); result["ruby"] = SVAL(options.ruby, DEFAULT_RUBY); result["python"] = SVAL(options.python, DEFAULT_PYTHON); result["nodejs"] = SVAL(options.nodejs, DEFAULT_NODEJS); result["meteor_app_settings"] = NON_EMPTY_SVAL(options.meteorAppSettings); result["min_processes"] = VAL(options.minProcesses, 1u); result["max_processes"] = VAL(options.maxProcesses, 0u); result["environment"] = SVAL(options.environment); // TODO: default value depends on integration mode result["spawn_method"] = SVAL(options.spawnMethod, DEFAULT_SPAWN_METHOD); result["bind_address"] = SVAL(options.bindAddress, DEFAULT_BIND_ADDRESS); result["start_timeout"] = VAL(options.startTimeout / 1000.0, DEFAULT_START_TIMEOUT / 1000.0); result["max_preloader_idle_time"] = VAL((Json::UInt) options.maxPreloaderIdleTime, (Json::UInt) DEFAULT_MAX_PRELOADER_IDLE_TIME); result["max_out_of_band_work_instances"] = VAL(options.maxOutOfBandWorkInstances, (Json::UInt) 1); result["base_uri"] = SVAL(options.baseURI, P_STATIC_STRING("/")); result["user"] = SVAL(options.user, options.defaultUser); result["group"] = SVAL(options.group, options.defaultGroup); result["user_switching"] = VAL(options.userSwitching); // TODO: default value depends on integration mode and euid result["file_descriptor_ulimit"] = VAL(options.fileDescriptorUlimit, 0u); result["load_shell_envvars"] = VAL(options.loadShellEnvvars); // TODO: default value depends on integration mode result["preload_bundler"] = VAL(options.preloadBundler); result["max_request_queue_size"] = VAL(options.maxRequestQueueSize, (Json::UInt) DEFAULT_MAX_REQUEST_QUEUE_SIZE); result["max_requests"] = VAL((Json::UInt) options.maxRequests, 0u); result["abort_websockets_on_process_shutdown"] = VAL(options.abortWebsocketsOnProcessShutdown); result["force_max_concurrent_requests_per_process"] = VAL(options.forceMaxConcurrentRequestsPerProcess, -1); result["restart_dir"] = NON_EMPTY_SVAL(options.restartDir); result["sticky_sessions_cookie_attributes"] = SVAL(options.stickySessionsCookieAttributes, DEFAULT_STICKY_SESSIONS_COOKIE_ATTRIBUTES); if (!options.environmentVariables.empty()) { DynamicBuffer envvarsData(options.environmentVariables.size() * 3 / 4); size_t envvarsDataSize = modp_b64_decode(envvarsData.data, options.environmentVariables.data(), options.environmentVariables.size()); if (envvarsDataSize == (size_t) -1) { P_WARN("Unable to decode environment variable data"); } else { Json::Value envvars(Json::objectValue); vector<string> envvarsAry; unsigned int i; split(StaticString(envvarsData.data, envvarsDataSize), '\0', envvarsAry); if (!envvarsAry.empty() && envvarsAry.back().empty()) { envvarsAry.pop_back(); } assert(envvars.size() % 2 == 0); for (i = 0; i < envvarsAry.size(); i += 2) { envvars[envvarsAry[i]] = envvarsAry[i + 1]; } result["environment_variables"] = VAL(envvars, Json::objectValue); } } else { result["environment_variables"] = VAL(Json::objectValue, Json::objectValue); } // Missing: sticky_sessions, sticky_session_cookie_name, friendly_error_pages /******************/ #undef VAL #undef SVAL #undef NON_EMPTY_SVAL } } // namespace ApplicationPool2 } // namespace Passenger ApplicationPool/Group/SessionManagement.cpp 0000644 00000025412 14756456557 0015115 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2011-2017 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include <Core/ApplicationPool/Group.h> /************************************************************************* * * Session management functions for ApplicationPool2::Group * *************************************************************************/ namespace Passenger { namespace ApplicationPool2 { using namespace std; using namespace boost; /**************************** * * Private methods * ****************************/ /* Determines which process to route a get() action to. The returned process * is guaranteed to be `canBeRoutedTo()`, i.e. not totally busy. * * A request is routed to an enabled processes, or if there are none, * from a disabling process. The rationale is as follows: * If there are no enabled process, then waiting for one to spawn is too * expensive. The next best thing is to route to disabling processes * until more processes have been spawned. */ Group::RouteResult Group::route(const Options &options) const { if (OXT_LIKELY(enabledCount > 0)) { if (options.stickySessionId == 0) { Process *process = findEnabledProcessWithLowestBusyness(); if (process->canBeRoutedTo()) { return RouteResult(process); } else { return RouteResult(NULL, true); } } else { Process *process = findProcessWithStickySessionIdOrLowestBusyness( options.stickySessionId); if (process != NULL) { if (process->canBeRoutedTo()) { return RouteResult(process); } else { return RouteResult(NULL, false); } } else { return RouteResult(NULL, true); } } } else { Process *process = findProcessWithLowestBusyness(disablingProcesses); if (process->canBeRoutedTo()) { return RouteResult(process); } else { return RouteResult(NULL, true); } } } SessionPtr Group::newSession(Process *process, unsigned long long now) { bool wasTotallyBusy = process->isTotallyBusy(); SessionPtr session = process->newSession(now); session->onInitiateFailure = _onSessionInitiateFailure; session->onClose = _onSessionClose; if (process->enabled == Process::ENABLED) { enabledProcessBusynessLevels[process->getIndex()] = process->busyness(); if (!wasTotallyBusy && process->isTotallyBusy()) { nEnabledProcessesTotallyBusy++; } } return session; } void Group::_onSessionInitiateFailure(Session *session) { Process *process = session->getProcess(); assert(process != NULL); process->getGroup()->onSessionInitiateFailure(process, session); } void Group::_onSessionClose(Session *session) { Process *process = session->getProcess(); assert(process != NULL); process->getGroup()->onSessionClose(process, session); } OXT_FORCE_INLINE void Group::onSessionInitiateFailure(Process *process, Session *session) { boost::container::vector<Callback> actions; TRACE_POINT(); // Standard resource management boilerplate stuff... Pool *pool = getPool(); boost::unique_lock<boost::mutex> lock(pool->syncher); assert(process->isAlive()); assert(isAlive() || getLifeStatus() == SHUTTING_DOWN); UPDATE_TRACE_POINT(); P_DEBUG("Could not initiate a session with process " << process->inspect() << ", detaching from pool if possible"); if (!pool->detachProcessUnlocked(process->shared_from_this(), actions)) { P_DEBUG("Process was already detached"); } pool->fullVerifyInvariants(); lock.unlock(); runAllActions(actions); } OXT_FORCE_INLINE void Group::onSessionClose(Process *process, Session *session) { TRACE_POINT(); // Standard resource management boilerplate stuff... Pool *pool = getPool(); boost::unique_lock<boost::mutex> lock(pool->syncher); assert(process->isAlive()); assert(isAlive() || getLifeStatus() == SHUTTING_DOWN); P_TRACE(2, "Session closed for process " << process->inspect()); verifyInvariants(); UPDATE_TRACE_POINT(); /* Update statistics. */ bool wasTotallyBusy = process->isTotallyBusy(); process->sessionClosed(session); assert(process->getLifeStatus() == Process::ALIVE); assert(process->enabled == Process::ENABLED || process->enabled == Process::DISABLING || process->enabled == Process::DETACHED); if (process->enabled == Process::ENABLED) { enabledProcessBusynessLevels[process->getIndex()] = process->busyness(); if (wasTotallyBusy) { assert(nEnabledProcessesTotallyBusy >= 1); nEnabledProcessesTotallyBusy--; } } /* This group now has a process that's guaranteed to be not * totally busy. */ assert(!process->isTotallyBusy()); bool detachingBecauseOfMaxRequests = false; bool detachingBecauseCapacityNeeded = false; bool shouldDetach = ( detachingBecauseOfMaxRequests = ( options.maxRequests > 0 && process->processed >= options.maxRequests )) || ( detachingBecauseCapacityNeeded = ( process->sessions == 0 && getWaitlist.empty() && ( !pool->getWaitlist.empty() || anotherGroupIsWaitingForCapacity() ) ) ); bool shouldDisable = process->enabled == Process::DISABLING && process->sessions == 0 && enabledCount > 0; if (shouldDetach || shouldDisable) { UPDATE_TRACE_POINT(); boost::container::vector<Callback> actions; if (shouldDetach) { if (detachingBecauseCapacityNeeded) { /* Someone might be trying to get() a session for a different * group that couldn't be spawned because of lack of pool capacity. * If this group isn't under sufficiently load (as apparent by the * checked conditions) then now's a good time to detach * this process or group in order to free capacity. */ P_DEBUG("Process " << process->inspect() << " is no longer totally " "busy; detaching it in order to make room in the pool"); } else { /* This process has processed its maximum number of requests, * so we detach it. */ P_DEBUG("Process " << process->inspect() << " has reached its maximum number of requests (" << options.maxRequests << "); detaching it"); } pool->detachProcessUnlocked(process->shared_from_this(), actions); } else { ProcessPtr processPtr = process->shared_from_this(); removeProcessFromList(processPtr, disablingProcesses); addProcessToList(processPtr, disabledProcesses); removeFromDisableWaitlist(processPtr, DR_SUCCESS, actions); maybeInitiateOobw(process); } pool->fullVerifyInvariants(); lock.unlock(); runAllActions(actions); } else { UPDATE_TRACE_POINT(); // This could change process->enabled. maybeInitiateOobw(process); if (!getWaitlist.empty() && process->enabled == Process::ENABLED) { /* If there are clients on this group waiting for a process to * become available then call them now. */ UPDATE_TRACE_POINT(); // Already calls verifyInvariants(). assignSessionsToGetWaitersQuickly(lock); } } } /**************************** * * Public methods * ****************************/ SessionPtr Group::get(const Options &newOptions, const GetCallback &callback, boost::container::vector<Callback> &postLockActions) { assert(isAlive()); if (OXT_LIKELY(!restarting())) { if (OXT_UNLIKELY(needsRestart(newOptions))) { restart(newOptions); } else { mergeOptions(newOptions); } if (OXT_UNLIKELY(!newOptions.noop && shouldSpawnForGetAction())) { // If we're trying to spawn the first process for this group, and // spawning failed because the pool is at full capacity, then we // try to kill some random idle process in the pool and try again. if (spawn() == SR_ERR_POOL_AT_FULL_CAPACITY && enabledCount == 0) { P_INFO("Unable to spawn the the sole process for group " << info.name << " because the max pool size has been reached. Trying " << "to shutdown another idle process to free capacity..."); if (poolForceFreeCapacity(this, postLockActions) != NULL) { SpawnResult result = spawn(); assert(result == SR_OK); (void) result; } else { P_INFO("There are no processes right now that are eligible " "for shutdown. Will try again later."); } } } } if (OXT_UNLIKELY(newOptions.noop)) { return nullProcess->createSessionObject((Socket *) NULL); } if (OXT_UNLIKELY(enabledCount == 0)) { /* We don't have any processes yet, but they're on the way. * * We have some choices here. If there are disabling processes * then we generally want to use them, except: * - When non-rolling restarting because those disabling processes * are from the old version. * - When all disabling processes are totally busy. * * Whenever a disabling process cannot be used, call the callback * after a process has been spawned or has failed to spawn, or * when a disabling process becomes available. */ assert(m_spawning || restarting() || poolAtFullCapacity()); if (disablingCount > 0 && !restarting()) { Process *process = findProcessWithLowestBusyness(disablingProcesses); assert(process != NULL); if (!process->isTotallyBusy()) { return newSession(process, newOptions.currentTime); } } if (pushGetWaiter(newOptions, callback, postLockActions)) { P_DEBUG("No session checked out yet: group is spawning or restarting"); } return SessionPtr(); } else { RouteResult result = route(newOptions); if (result.process == NULL) { /* Looks like all processes are totally busy. * Wait until a new one has been spawned or until * resources have become free. */ if (pushGetWaiter(newOptions, callback, postLockActions)) { P_DEBUG("No session checked out yet: all processes are at full capacity"); } return SessionPtr(); } else { P_DEBUG("Session checked out from process " << result.process->inspect()); return newSession(result.process, newOptions.currentTime); } } } } // namespace ApplicationPool2 } // namespace Passenger ApplicationPool/Group/SpawningAndRestarting.cpp 0000644 00000034567 14756456557 0015764 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2011-2017 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include <Core/ApplicationPool/Group.h> /************************************************************************* * * Session management functions for ApplicationPool2::Group * *************************************************************************/ namespace Passenger { namespace ApplicationPool2 { using namespace std; using namespace boost; /**************************** * * Private methods * ****************************/ // The 'self' parameter is for keeping the current Group object alive while this thread is running. void Group::spawnThreadMain(GroupPtr self, SpawningKit::SpawnerPtr spawner, Options options, unsigned int restartsInitiated) { spawnThreadRealMain(spawner, options, restartsInitiated); } void Group::spawnThreadRealMain(const SpawningKit::SpawnerPtr &spawner, const Options &options, unsigned int restartsInitiated) { TRACE_POINT(); boost::this_thread::disable_interruption di; boost::this_thread::disable_syscall_interruption dsi; Pool *pool = getPool(); Pool::DebugSupportPtr debug = pool->debugSupport; bool done = false; while (!done) { bool shouldFail = false; if (debug != NULL && debug->spawning) { UPDATE_TRACE_POINT(); boost::this_thread::restore_interruption ri(di); boost::this_thread::restore_syscall_interruption rsi(dsi); boost::this_thread::interruption_point(); string iteration; { LockGuard g(debug->syncher); debug->spawnLoopIteration++; iteration = toString(debug->spawnLoopIteration); } P_DEBUG("Begin spawn loop iteration " << iteration); debug->debugger->send("Begin spawn loop iteration " + iteration); vector<string> cases; cases.push_back("Proceed with spawn loop iteration " + iteration); cases.push_back("Fail spawn loop iteration " + iteration); MessagePtr message = debug->messages->recvAny(cases); shouldFail = message->name == "Fail spawn loop iteration " + iteration; } ProcessPtr process; ExceptionPtr exception; try { UPDATE_TRACE_POINT(); boost::this_thread::restore_interruption ri(di); boost::this_thread::restore_syscall_interruption rsi(dsi); if (shouldFail) { SpawningKit::Journey journey(SpawningKit::SPAWN_DIRECTLY, false); SpawningKit::Config config; journey.setStepErrored(SpawningKit::SPAWNING_KIT_PREPARATION, true); SpawningKit::SpawnException e(SpawningKit::INTERNAL_ERROR, journey, &config); e.setSummary("Simulated failure"); throw e.finalize(); } else { process = createProcessObject(*spawner, spawner->spawn(options)); } } catch (const boost::thread_interrupted &) { break; } catch (SpawningKit::SpawnException &e) { processAndLogNewSpawnException(e, options, pool->getContext()); exception = copyException(e); } catch (const tracable_exception &e) { exception = copyException(e); // Let other (unexpected) exceptions crash the program so // gdb can generate a backtrace. } UPDATE_TRACE_POINT(); ScopeGuard guard(boost::bind(Process::forceTriggerShutdownAndCleanup, process)); boost::unique_lock<boost::mutex> lock(pool->syncher); if (!isAlive()) { if (process != NULL) { P_DEBUG("Group is being shut down so dropping process " << process->inspect() << " which we just spawned and exiting spawn loop"); } else { P_DEBUG("The group is being shut down. A process failed " "to be spawned anyway, so ignoring this error and exiting " "spawn loop"); } // We stop immediately because any previously assumed invariants // may have been violated. break; } else if (restartsInitiated != this->restartsInitiated) { if (process != NULL) { P_DEBUG("A restart was issued for the group, so dropping process " << process->inspect() << " which we just spawned and exiting spawn loop"); } else { P_DEBUG("A restart was issued for the group. A process failed " "to be spawned anyway, so ignoring this error and exiting " "spawn loop"); } // We stop immediately because any previously assumed invariants // may have been violated. break; } verifyInvariants(); assert(m_spawning); assert(processesBeingSpawned > 0); processesBeingSpawned--; assert(processesBeingSpawned == 0); UPDATE_TRACE_POINT(); boost::container::vector<Callback> actions; if (process != NULL) { AttachResult result = attach(process, actions); if (result == AR_OK) { guard.clear(); if (getWaitlist.empty()) { pool->assignSessionsToGetWaiters(actions); } else { assignSessionsToGetWaiters(actions); } P_DEBUG("New process count = " << enabledCount << ", remaining get waiters = " << getWaitlist.size()); } else { done = true; P_DEBUG("Unable to attach spawned process " << process->inspect()); if (result == AR_ANOTHER_GROUP_IS_WAITING_FOR_CAPACITY) { pool->possiblySpawnMoreProcessesForExistingGroups(); } } } else { // TODO: sure this is the best thing? if there are // processes currently alive we should just use them. if (enabledCount == 0) { enableAllDisablingProcesses(actions); } Pool::assignExceptionToGetWaiters(getWaitlist, exception, actions); pool->assignSessionsToGetWaiters(actions); done = true; } done = done || (processLowerLimitsSatisfied() && getWaitlist.empty()) || processUpperLimitsReached() || pool->atFullCapacityUnlocked(); m_spawning = !done; if (done) { P_DEBUG("Spawn loop done"); } else { processesBeingSpawned++; P_DEBUG("Continue spawning"); } UPDATE_TRACE_POINT(); pool->fullVerifyInvariants(); lock.unlock(); UPDATE_TRACE_POINT(); runAllActions(actions); UPDATE_TRACE_POINT(); } if (debug != NULL && debug->spawning) { debug->debugger->send("Spawn loop done"); } } // The 'self' parameter is for keeping the current Group object alive while this thread is running. void Group::finalizeRestart(GroupPtr self, Options oldOptions, Options newOptions, RestartMethod method, SpawningKit::FactoryPtr spawningKitFactory, unsigned int restartsInitiated, boost::container::vector<Callback> postLockActions) { TRACE_POINT(); Pool::runAllActions(postLockActions); postLockActions.clear(); boost::this_thread::disable_interruption di; boost::this_thread::disable_syscall_interruption dsi; // Create a new spawner. Options spawnerOptions = oldOptions; resetOptions(newOptions, &spawnerOptions); SpawningKit::SpawnerPtr newSpawner = spawningKitFactory->create(spawnerOptions); SpawningKit::SpawnerPtr oldSpawner; UPDATE_TRACE_POINT(); Pool *pool = getPool(); Pool::DebugSupportPtr debug = pool->debugSupport; if (debug != NULL && debug->restarting) { boost::this_thread::restore_interruption ri(di); boost::this_thread::restore_syscall_interruption rsi(dsi); boost::this_thread::interruption_point(); debug->debugger->send("About to end restarting"); debug->messages->recv("Finish restarting"); } ScopedLock l(pool->syncher); if (!isAlive()) { P_DEBUG("Group " << getName() << " is shutting down, so aborting restart"); return; } if (restartsInitiated != this->restartsInitiated) { // Before this restart could be finalized, another restart command was given. // The spawner we just created might be out of date now so we abort. P_DEBUG("Restart of group " << getName() << " aborted because a new restart was initiated concurrently"); if (debug != NULL && debug->restarting) { debug->debugger->send("Restarting aborted"); } return; } // Run some sanity checks. pool->fullVerifyInvariants(); assert(m_restarting); UPDATE_TRACE_POINT(); // Atomically swap the new spawner with the old one. resetOptions(newOptions); oldSpawner = spawner; spawner = newSpawner; m_restarting = false; if (shouldSpawn()) { spawn(); } else if (isWaitingForCapacity()) { P_INFO("Group " << getName() << " is waiting for capacity to become available. " "Trying to shutdown another idle process to free capacity..."); if (pool->forceFreeCapacity(this, postLockActions) != NULL) { spawn(); } else { P_INFO("There are no processes right now that are eligible " "for shutdown. Will try again later."); } } verifyInvariants(); l.unlock(); oldSpawner.reset(); Pool::runAllActions(postLockActions); P_DEBUG("Restart of group " << getName() << " done"); if (debug != NULL && debug->restarting) { debug->debugger->send("Restarting done"); } } /**************************** * * Public methods * ****************************/ void Group::restart(const Options &options, RestartMethod method) { boost::container::vector<Callback> actions; assert(isAlive()); P_DEBUG("Restarting group " << getName()); // If there is currently a restarter thread or a spawner thread active, // the following tells them to abort their current work as soon as possible. restartsInitiated++; processesBeingSpawned = 0; m_spawning = false; m_restarting = true; uuid = generateUuid(pool); this->options.groupUuid = uuid; detachAll(actions); getPool()->interruptableThreads.create_thread( boost::bind(&Group::finalizeRestart, this, shared_from_this(), this->options.copyAndPersist().clearPerRequestFields(), options.copyAndPersist().clearPerRequestFields(), method, getContext()->spawningKitFactory, restartsInitiated, actions), "Group restarter: " + getName(), POOL_HELPER_THREAD_STACK_SIZE ); } bool Group::restarting() const { return m_restarting; } bool Group::needsRestart(const Options &options) { if (m_restarting) { return false; } else { time_t now; struct stat buf; if (options.currentTime != 0) { now = options.currentTime / 1000000; } else { now = SystemTime::get(); } if (lastRestartFileCheckTime == 0) { // First time we call needsRestart() for this group. if (syscalls::stat(restartFile.c_str(), &buf) == 0) { lastRestartFileMtime = buf.st_mtime; } else { lastRestartFileMtime = 0; } lastRestartFileCheckTime = now; return false; } else if (lastRestartFileCheckTime <= now - (time_t) options.statThrottleRate) { // Not first time we call needsRestart() for this group. // Stat throttle time has passed. bool restart; lastRestartFileCheckTime = now; if (lastRestartFileMtime > 0) { // restart.txt existed before if (syscalls::stat(restartFile.c_str(), &buf) == -1) { // restart.txt no longer exists lastRestartFileMtime = buf.st_mtime; restart = false; } else if (buf.st_mtime != lastRestartFileMtime) { // restart.txt's mtime has changed lastRestartFileMtime = buf.st_mtime; restart = true; } else { restart = false; } } else { // restart.txt didn't exist before if (syscalls::stat(restartFile.c_str(), &buf) == 0) { // restart.txt now exists lastRestartFileMtime = buf.st_mtime; restart = true; } else { // restart.txt still doesn't exist lastRestartFileMtime = 0; restart = false; } } if (!restart) { alwaysRestartFileExists = restart = syscalls::stat(alwaysRestartFile.c_str(), &buf) == 0; } return restart; } else { // Not first time we call needsRestart() for this group. // Still within stat throttling window. if (alwaysRestartFileExists) { // always_restart.txt existed before alwaysRestartFileExists = syscalls::stat( alwaysRestartFile.c_str(), &buf) == 0; return alwaysRestartFileExists; } else { // Don't check until stat throttling window is over return false; } } } } /** * Attempts to increase the number of processes by one, while respecting the * resource limits. That is, this method will ensure that there are at least * `minProcesses` processes, but no more than `maxProcesses` processes, and no * more than `pool->max` processes in the entire pool. */ SpawnResult Group::spawn() { assert(isAlive()); if (m_spawning) { return SR_IN_PROGRESS; } else if (restarting()) { return SR_ERR_RESTARTING; } else if (processUpperLimitsReached()) { return SR_ERR_GROUP_UPPER_LIMITS_REACHED; } else if (poolAtFullCapacity()) { return SR_ERR_POOL_AT_FULL_CAPACITY; } else { P_DEBUG("Requested spawning of new process for group " << info.name); interruptableThreads.create_thread( boost::bind(&Group::spawnThreadMain, this, shared_from_this(), spawner, options.copyAndPersist().clearPerRequestFields(), restartsInitiated), "Group process spawner: " + info.name, POOL_HELPER_THREAD_STACK_SIZE); m_spawning = true; processesBeingSpawned++; return SR_OK; } } bool Group::spawning() const { return m_spawning; } /** Whether a new process should be spawned for this group. */ bool Group::shouldSpawn() const { return allowSpawn() && ( !processLowerLimitsSatisfied() || allEnabledProcessesAreTotallyBusy() || !getWaitlist.empty() ); } /** Whether a new process should be spawned for this group in the * specific case that another get action is to be performed. */ bool Group::shouldSpawnForGetAction() const { return enabledCount == 0 || shouldSpawn(); } /** * Whether a new process is allowed to be spawned for this group, * i.e. whether the upper processes limits have not been reached. */ bool Group::allowSpawn() const { return isAlive() && !processUpperLimitsReached() && !poolAtFullCapacity(); } } // namespace ApplicationPool2 } // namespace Passenger ApplicationPool/Group/Miscellaneous.cpp 0000644 00000004267 14756456557 0014305 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2011-2017 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include <Core/ApplicationPool/Group.h> /************************************************************************* * * Miscellaneous for ApplicationPool2::Group * *************************************************************************/ namespace Passenger { namespace ApplicationPool2 { using namespace std; using namespace boost; /**************************** * * Public methods * ****************************/ void Group::cleanupSpawner(boost::container::vector<Callback> &postLockActions) { assert(isAlive()); postLockActions.push_back(boost::bind(doCleanupSpawner, spawner)); } bool Group::authorizeByUid(uid_t uid) const { return uid == 0 || SpawningKit::prepareUserSwitching(options, getWrapperRegistry()).uid == uid; } bool Group::authorizeByApiKey(const ApiKey &key) const { return key.isSuper() || key == getApiKey(); } } // namespace ApplicationPool2 } // namespace Passenger ApplicationPool/AbstractSession.h 0000644 00000005225 14756456557 0013155 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2016-2017 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef _PASSENGER_APPLICATION_POOL_ABSTRACT_SESSION_H_ #define _PASSENGER_APPLICATION_POOL_ABSTRACT_SESSION_H_ #include <sys/types.h> #include <boost/atomic.hpp> #include <boost/intrusive_ptr.hpp> #include <StaticString.h> #include <Shared/ApplicationPoolApiKey.h> namespace Passenger { namespace ApplicationPool2 { /** * An abstract base class for Session so that unit tests can work with * a mocked version of it. */ class AbstractSession { public: virtual ~AbstractSession() {} virtual void ref() const = 0; virtual void unref() const = 0; virtual pid_t getPid() const = 0; virtual StaticString getGupid() const = 0; virtual StaticString getProtocol() const = 0; virtual unsigned int getStickySessionId() const = 0; virtual const ApiKey &getApiKey() const = 0; virtual int fd() const = 0; virtual bool isClosed() const = 0; virtual void initiate(bool blocking = true) = 0; virtual void requestOOBW() { /* Do nothing */ } /** * This Session object becomes fully unsable after closing. */ virtual void close(bool success, bool wantKeepAlive = false) = 0; }; inline void intrusive_ptr_add_ref(const AbstractSession *session) { session->ref(); } inline void intrusive_ptr_release(const AbstractSession *session) { session->unref(); } } // namespace ApplicationPool2 } // namespace Passenger #endif /* _PASSENGER_APPLICATION_POOL2_ABSTRACT_SESSION_H_ */ ApplicationPool/Process.cpp 0000644 00000003613 14756456557 0012016 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2011-2017 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include <Core/ApplicationPool/Process.h> #include <Core/ApplicationPool/Group.h> namespace Passenger { namespace ApplicationPool2 { using namespace std; using namespace boost; string Process::getAppGroupName(const BasicGroupInfo *info) const { if (info->group != NULL) { return info->group->options.getAppGroupName().toString(); } else { return string(); } } string Process::getAppLogFile(const BasicGroupInfo *info) const { if (info->group != NULL) { return info->group->options.appLogFile.toString(); } else { return string(); } } } // namespace ApplicationPool2 } // namespace Passenger ApplicationPool/BasicProcessInfo.h 0000644 00000010354 14756456557 0013241 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2014-2018 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef _PASSENGER_APPLICATION_POOL2_BASIC_PROCESS_INFO_H_ #define _PASSENGER_APPLICATION_POOL2_BASIC_PROCESS_INFO_H_ #include <sys/types.h> #include <cstring> #include <jsoncpp/json.h> #include <StaticString.h> #include <Exceptions.h> #include <JsonTools/JsonUtils.h> #include <Core/ApplicationPool/BasicGroupInfo.h> #include <Core/SpawningKit/Result.h> namespace Passenger { namespace ApplicationPool2 { using namespace std; class Process; /** * Contains a subset of the information in Process. This subset consists only * of information that is: * * 1. ...read-only and set during Process constructions. * 2. ...needed by Session. * * This class is contained inside `Process` as a const object. Because the * information is read-only, and because Process outlives all related Session * objects, Session can access it without grabbing the lock on Process. * * This class also serves to ensure that Session does not have a direct * dependency on Process. */ class BasicProcessInfo { public: static const unsigned int GUPID_MAX_SIZE = 20; /** The Process that this BasicProcessInfo is contained in. */ Process *process; /** The basic information of the Group that the associated Process is contained in. */ const BasicGroupInfo *groupInfo; /** * The operating system process ID. */ pid_t pid; /** * An ID that uniquely identifies this Process in the Group, for * use in implementing sticky sessions. Set by Group::attach(). */ unsigned int stickySessionId; /** * UUID for this process, randomly generated and extremely unlikely to ever * appear again in this universe. */ char gupid[GUPID_MAX_SIZE]; unsigned int gupidSize; BasicProcessInfo(Process *_process, const BasicGroupInfo *_groupInfo, const Json::Value &json) : process(_process), groupInfo(_groupInfo), pid(getJsonIntField(json, "pid")) // We initialize this in Process::initializeStickySessionId(), // called from Group::attach(). // We should probably some day refactor this. The reason we do // it the way we do right now is because some day we want to be able // to attach external processes, so the best place to initialize this // information is in Group::attach(). //stickySessionId(getJsonUintField(json, "sticky_session_id", 0)) { StaticString gupid = getJsonStaticStringField(json, "gupid"); assert(gupid.size() <= GUPID_MAX_SIZE); memcpy(this->gupid, gupid.data(), gupid.size()); gupidSize = gupid.size(); } BasicProcessInfo(Process *_process, const BasicGroupInfo *_groupInfo, const SpawningKit::Result &skResult) : process(_process), groupInfo(_groupInfo), pid(skResult.pid) // See above comment about the 'stickySessionId' field { assert(skResult.gupid.size() <= GUPID_MAX_SIZE); memcpy(gupid, skResult.gupid.data(), skResult.gupid.size()); gupidSize = skResult.gupid.size(); } }; } // namespace ApplicationPool2 } // namespace Passenger #endif /* _PASSENGER_APPLICATION_POOL2_BASIC_PROCESS_INFO_H_ */ ApplicationPool/Context.h 0000644 00000006471 14756456557 0011476 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2014-2018 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef _PASSENGER_APPLICATION_POOL2_CONTEXT_H_ #define _PASSENGER_APPLICATION_POOL2_CONTEXT_H_ #include <boost/shared_ptr.hpp> #include <boost/thread.hpp> #include <boost/pool/object_pool.hpp> #include <Exceptions.h> #include <Core/SpawningKit/Factory.h> namespace Passenger { namespace ApplicationPool2 { using namespace boost; class Session; class Process; /** * State shared by Pool, Group, Process and Session. It contains statistics * and counters, memory management objects, configuration objects, etc. * This struct was introduced so that Group, Process and Sessions don't have * to depend on Pool (which introduces circular dependencies). * * The fields are separated in several groups. Each group may have its own mutex. * If it does, then all operations on any of the fields in that group requires * grabbing the mutex unless documented otherwise. */ struct Context { public: /****** Working objects ******/ boost::mutex memoryManagementSyncher; boost::object_pool<Session> sessionObjectPool; boost::object_pool<Process> processObjectPool; mutable boost::mutex agentConfigSyncher; /****** Dependencies ******/ SpawningKit::FactoryPtr spawningKitFactory; Json::Value agentConfig; Context() : sessionObjectPool(64, 1024), processObjectPool(4, 64) { } void finalize() { if (spawningKitFactory == NULL) { throw RuntimeException("spawningKitFactory must be set"); } } /****** Configuration objects ******/ SpawningKit::Context *getSpawningKitContext() const { return spawningKitFactory->getContext(); } const ResourceLocator *getResourceLocator() const { return getSpawningKitContext()->resourceLocator; } const WrapperRegistry::Registry *getWrapperRegistry() const { return getSpawningKitContext()->wrapperRegistry; } const RandomGeneratorPtr &getRandomGenerator() const { return getSpawningKitContext()->randomGenerator; } }; typedef boost::shared_ptr<Context> ContextPtr; } // namespace ApplicationPool2 } // namespace Passenger #endif /* _PASSENGER_APPLICATION_POOL2_CONTEXT_H_ */ ApplicationPool/Options.h 0000644 00000054203 14756456557 0011501 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2010-2018 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef _PASSENGER_APPLICATION_POOL2_OPTIONS_H_ #define _PASSENGER_APPLICATION_POOL2_OPTIONS_H_ #include <string> #include <vector> #include <utility> #include <boost/shared_array.hpp> #include <WrapperRegistry/Registry.h> #include <DataStructures/HashedStaticString.h> #include <Constants.h> #include <ResourceLocator.h> #include <StaticString.h> #include <FileTools/PathManip.h> #include <Utils.h> namespace Passenger { namespace ApplicationPool2 { using namespace std; using namespace boost; /** * This struct encapsulates information for ApplicationPool::get() and for * Spawner::spawn(), such as which application is to be spawned. * * ## Privilege lowering support * * If <em>user</em> is given and isn't the empty string, then the application process * will run as the given username. Otherwise, the owner of the application's startup * file (e.g. config.ru) will be used. * * If <em>group</em> is given and isn't the empty string, then the application process * will run as the given group name. If it's set to the special value * "!STARTUP_FILE!", then the startup file's group will be used. Otherwise, * the primary group of the user that the application process will run as, * will be used as group. * * If the user or group that the application process attempts to switch to * doesn't exist, then <em>defaultUser</em> and <em>defaultGroup</em>, respectively, * will be used. * * Phusion Passenger will attempt to avoid running the application process as * root: if <em>user</em> or <em>group</em> is set to the root user or the root group, * or if the startup file is owned by root, then <em>defaultUser</em> and * <em>defaultGroup</em> will be used instead. * * All this only happen if Phusion Passenger has root privileges. If not, then * these options have no effect. */ class Options { private: shared_array<char> storage; template<typename OptionsClass, typename StaticStringClass> static vector<StaticStringClass *> getStringFields(OptionsClass &options) { vector<StaticStringClass *> result; result.reserve(20); result.push_back(&options.appRoot); result.push_back(&options.appGroupName); result.push_back(&options.appLogFile); result.push_back(&options.appType); result.push_back(&options.appStartCommand); result.push_back(&options.startupFile); result.push_back(&options.processTitle); result.push_back(&options.environment); result.push_back(&options.baseURI); result.push_back(&options.spawnMethod); result.push_back(&options.bindAddress); result.push_back(&options.user); result.push_back(&options.group); result.push_back(&options.defaultUser); result.push_back(&options.defaultGroup); result.push_back(&options.restartDir); result.push_back(&options.preexecChroot); result.push_back(&options.postexecChroot); result.push_back(&options.integrationMode); result.push_back(&options.ruby); result.push_back(&options.python); result.push_back(&options.nodejs); result.push_back(&options.meteorAppSettings); result.push_back(&options.environmentVariables); result.push_back(&options.apiKey); result.push_back(&options.groupUuid); result.push_back(&options.hostName); result.push_back(&options.uri); result.push_back(&options.stickySessionsCookieAttributes); return result; } static inline void appendKeyValue(vector<string> &vec, const char *key, const StaticString &value) { if (!value.empty()) { vec.push_back(key); vec.push_back(value.toString()); } } static inline void appendKeyValue(vector<string> &vec, const char *key, const char *value) { vec.push_back(key); vec.push_back(value); } static inline void appendKeyValue2(vector<string> &vec, const char *key, long value) { vec.push_back(key); vec.push_back(toString(value)); } static inline void appendKeyValue3(vector<string> &vec, const char *key, unsigned long value) { vec.push_back(key); vec.push_back(toString(value)); } static inline void appendKeyValue4(vector<string> &vec, const char *key, bool value) { vec.push_back(key); vec.push_back(value ? "true" : "false"); } public: /*********** Spawn options that should be set by the caller *********** * These are the options that are relevant while spawning an application * process. These options are only used during spawning. */ /** * The root directory of the application to spawn. In case of a Ruby on Rails * application, this is the folder that contains 'app/', 'public/', 'config/', * etc. This must be a valid directory, but the path does not have to be absolute. */ HashedStaticString appRoot; /** * A name used by ApplicationPool to uniquely identify an application. * If one tries to get() from the application pool with name "A", then get() * again with name "B", then the latter will spawn a new application process, * even if both get() requests have the same app root. * * If left empty, then the app root is used as the app group name. */ HashedStaticString appGroupName; /** The application's log file, where Passenger sends the logs from * the application. */ StaticString appLogFile; /** The application's type, used for determining the command to invoke to * spawn an application process as well as determining the startup file's * filename. It can be one of the app type names in AppType.cpp, or the * empty string (default). In case of the latter, 'appStartCommand' and * 'startupFile' (which MUST be set) will dictate the startup command * and the startup file's filename. */ StaticString appType; /** The shell command string for spawning the application process. * Only used during spawning and only if appType.empty(). */ StaticString appStartCommand; /** Filename of the application's startup file. Only actually used for * determining user switching info. Only used during spawning. */ StaticString startupFile; /** The process title to assign to the application process. Only used * during spawning. May be empty in which case no particular process * title is assigned. Only used during spawning. */ StaticString processTitle; /** * Defaults to DEFAULT_LOG_LEVEL. */ int logLevel; /** The maximum amount of time, in milliseconds, that may be spent * on spawning the process or the preloader. */ unsigned int startTimeout; /** * The RAILS_ENV/RACK_ENV environment that should be used. May not be an * empty string. */ StaticString environment; /** * The base URI on which the application runs. If the application is * running on the root URI, then this value must be "/". * * @invariant baseURI != "" */ StaticString baseURI; /** * Spawning method, either "smart" or "direct". */ StaticString spawnMethod; /** * The address that Passenger binds to in order to allow sending HTTP * requests to individual application processes. */ StaticString bindAddress; /** See overview. */ StaticString user; /** See class overview. */ StaticString group; /** See class overview. Defaults to "nobody". */ StaticString defaultUser; /** See class overview. Defaults to the defaultUser's primary group. */ StaticString defaultGroup; /** Minimum user id starting from which entering LVE and CageFS is allowed. */ unsigned int lveMinUid; /** * The directory which contains restart.txt and always_restart.txt. * An empty string means that the default directory should be used. */ StaticString restartDir; StaticString preexecChroot; StaticString postexecChroot; StaticString integrationMode; /** * Path to the Ruby interpreter to use, in case the application to spawn * is a Ruby app. */ StaticString ruby; /** * Path to the Python interpreter to use, in case the application to spawn * is a Python app. */ StaticString python; /** * Path to the Node.js command to use, in case the application to spawn * is a Node.js app. */ StaticString nodejs; /** * When running meteor in non-bundled mode, settings for the application need to be specified * via --settings (instead of through the METEOR_SETTINGS environment variable), */ StaticString meteorAppSettings; /** * Environment variables which should be passed to the spawned application * process. This is a base64-encoded string of key-value pairs, with each * element terminated by a NUL character. For example: * * base64("PATH\0/usr/bin\0RUBY\0/usr/bin/ruby\0") */ StaticString environmentVariables; unsigned int fileDescriptorUlimit; /** * If set to a value that isn't -1, makes Passenger ignore the application's * advertised socket concurrency, and believe that the concurrency should be * the given value. * * Defaults to -1. */ int forceMaxConcurrentRequestsPerProcess; /** Whether debugger support should be enabled. */ bool debugger; /** Whether to load environment variables set in shell startup * files (e.g. ~/.bashrc) during spawning. */ bool loadShellEnvvars; /** Whether to tell Ruby to load bundler during spawning. */ bool preloadBundler; bool userSwitching; /** * Whether Spawner should raise an internal error when spawning. Used * during unit tests. */ bool raiseInternalError; /*********** Per-group pool options that should be set by the caller *********** * These options dictate how Pool will manage processes, routing, etc. within * a single Group. These options are not process-specific, only group-specific. */ /** * The minimum number of processes for the current group that the application * pool's cleaner thread should keep around. */ unsigned int minProcesses; /** * The maximum number of processes that may be spawned * for this app root. This option only has effect if it's lower than * the pool size. * * A value of 0 means unspecified, and has no effect. */ unsigned int maxProcesses; /** The number of seconds that preloader processes may stay alive idling. */ long maxPreloaderIdleTime; /** * The maximum number of processes inside a group that may be performing * out-of-band work at the same time. */ unsigned int maxOutOfBandWorkInstances; /** * The maximum number of requests that may live in the Group.getWaitlist queue. * A value of 0 means unlimited. */ unsigned int maxRequestQueueSize; /** * Whether websocket connections should be aborted on process shutdown * or restart. */ bool abortWebsocketsOnProcessShutdown; /** * The attributes to use for the sticky session cookie. * Values should validate against the regex: ([\w]+(=[\w]+)?; )* */ StaticString stickySessionsCookieAttributes; /*-----------------*/ /*********** Per-request pool options that should be set by the caller *********** * These options also dictate how Pool will manage processes, etc. Unlike the * per-group options, these options are customizable on a per-request basis. * Their effects also don't persist longer than a single request. */ /** Current request host name. */ StaticString hostName; /** Current request URI. */ StaticString uri; /** * A sticky session ID for routing to a specific process. */ unsigned int stickySessionId; /** * A throttling rate for file stats. When set to a non-zero value N, * restart.txt and other files which are usually stat()ted on every * ApplicationPool::get() call will be stat()ed at most every N seconds. */ unsigned long statThrottleRate; /** * The maximum number of requests that the spawned application may process * before exiting. A value of 0 means unlimited. */ unsigned long maxRequests; /** If the current time (in microseconds) has already been queried, set it * here. Pool will use this timestamp instead of querying it again. */ unsigned long long currentTime; /** When true, Pool::get() and Pool::asyncGet() will create the necessary * Group structure just as normally, and will even handle * restarting logic, but will not actually spawn any processes and will not * open a session with an existing process. Instead, a fake Session object * is returned which points to a Process object that isn't stored anywhere * in the Pool structures and isn't mapped to any real OS process. It does * however point to the real Group structure. Useful for unit tests. * False by default. */ bool noop; /*-----------------*/ /*-----------------*/ /*********** Spawn options automatically set by Pool *********** * These options are passed to the Spawner. The Pool::get() caller may not * see these values. */ /** The API key of the pool group that the spawned process is to belong to. */ StaticString apiKey; /** * A UUID that's generated on Group initialization, and changes every time * the Group receives a restart command. Allows Union Station to track app * restarts. */ StaticString groupUuid; /*********************************/ /** * Creates a new Options object with the default values filled in. * One must still set appRoot manually, after having used this constructor. */ Options() : logLevel(DEFAULT_LOG_LEVEL), startTimeout(DEFAULT_START_TIMEOUT), environment(DEFAULT_APP_ENV, sizeof(DEFAULT_APP_ENV) - 1), baseURI("/", 1), spawnMethod(DEFAULT_SPAWN_METHOD, sizeof(DEFAULT_SPAWN_METHOD) - 1), bindAddress(DEFAULT_BIND_ADDRESS, sizeof(DEFAULT_BIND_ADDRESS) - 1), defaultUser(PASSENGER_DEFAULT_USER, sizeof(PASSENGER_DEFAULT_USER) - 1), lveMinUid(DEFAULT_LVE_MIN_UID), integrationMode(DEFAULT_INTEGRATION_MODE, sizeof(DEFAULT_INTEGRATION_MODE) - 1), ruby(DEFAULT_RUBY, sizeof(DEFAULT_RUBY) - 1), python(DEFAULT_PYTHON, sizeof(DEFAULT_PYTHON) - 1), nodejs(DEFAULT_NODEJS, sizeof(DEFAULT_NODEJS) - 1), fileDescriptorUlimit(0), forceMaxConcurrentRequestsPerProcess(-1), debugger(false), loadShellEnvvars(true), preloadBundler(false), userSwitching(true), raiseInternalError(false), minProcesses(1), maxProcesses(0), maxPreloaderIdleTime(-1), maxOutOfBandWorkInstances(1), maxRequestQueueSize(DEFAULT_MAX_REQUEST_QUEUE_SIZE), abortWebsocketsOnProcessShutdown(true), stickySessionsCookieAttributes(DEFAULT_STICKY_SESSIONS_COOKIE_ATTRIBUTES, sizeof(DEFAULT_STICKY_SESSIONS_COOKIE_ATTRIBUTES) - 1), stickySessionId(0), statThrottleRate(DEFAULT_STAT_THROTTLE_RATE), maxRequests(0), currentTime(0), noop(false) /*********************************/ { /*********************************/ } Options copy() const { return *this; } Options copyAndPersist() const { Options cpy(*this); cpy.persist(*this); return cpy; } /** * Assign <em>other</em>'s string fields' values into this Option * object, and store the data in this Option object's internal storage * area. */ Options &persist(const Options &other) { vector<StaticString *> strings = getStringFields<Options, StaticString>(*this); const vector<const StaticString *> otherStrings = getStringFields<const Options, const StaticString>(other); unsigned int i; size_t otherLen = 0; char *end; assert(strings.size() == otherStrings.size()); // Calculate the desired length of the internal storage area. // All strings are NULL-terminated. for (i = 0; i < otherStrings.size(); i++) { otherLen += otherStrings[i]->size() + 1; } shared_array<char> data(new char[otherLen]); end = data.get(); // Copy string fields into the internal storage area. for (i = 0; i < otherStrings.size(); i++) { const char *pos = end; StaticString *str = strings[i]; const StaticString *otherStr = otherStrings[i]; // Copy over the string data. memcpy(end, otherStr->c_str(), otherStr->size()); end += otherStr->size(); *end = '\0'; end++; // Point current object's field to the data in the // internal storage area. *str = StaticString(pos, end - pos - 1); } storage = data; // Fix up HashedStaticStrings' hashes. appRoot.setHash(other.appRoot.hash()); appGroupName.setHash(other.appGroupName.hash()); return *this; } Options &clearPerRequestFields() { hostName = StaticString(); uri = StaticString(); stickySessionId = 0; currentTime = 0; noop = false; return *this; } enum FieldSet { SPAWN_OPTIONS = 1 << 0, PER_GROUP_POOL_OPTIONS = 1 << 1, ALL_OPTIONS = ~0 }; /** * Append information in this Options object to the given string vector, except * for environmentVariables. You can customize what information you want through * the `elements` argument. */ void toVector(vector<string> &vec, const ResourceLocator &resourceLocator, const WrapperRegistry::Registry &wrapperRegistry, int fields = ALL_OPTIONS) const { if (fields & SPAWN_OPTIONS) { appendKeyValue (vec, "app_root", appRoot); appendKeyValue (vec, "app_group_name", getAppGroupName()); appendKeyValue (vec, "app_type", appType); appendKeyValue (vec, "app_log_file", appLogFile); appendKeyValue (vec, "start_command", getStartCommand(resourceLocator, wrapperRegistry)); appendKeyValue (vec, "startup_file", absolutizePath(getStartupFile(wrapperRegistry), absolutizePath(appRoot))); appendKeyValue (vec, "process_title", getProcessTitle(wrapperRegistry)); appendKeyValue2(vec, "log_level", logLevel); appendKeyValue3(vec, "start_timeout", startTimeout); appendKeyValue (vec, "environment", environment); appendKeyValue (vec, "base_uri", baseURI); appendKeyValue (vec, "spawn_method", spawnMethod); appendKeyValue (vec, "bind_address", bindAddress); appendKeyValue (vec, "user", user); appendKeyValue (vec, "group", group); appendKeyValue (vec, "default_user", defaultUser); appendKeyValue (vec, "default_group", defaultGroup); appendKeyValue (vec, "restart_dir", restartDir); appendKeyValue (vec, "preexec_chroot", preexecChroot); appendKeyValue (vec, "postexec_chroot", postexecChroot); appendKeyValue (vec, "integration_mode", integrationMode); appendKeyValue (vec, "ruby", ruby); appendKeyValue (vec, "python", python); appendKeyValue (vec, "nodejs", nodejs); appendKeyValue (vec, "meteor_app_settings", meteorAppSettings); appendKeyValue4(vec, "debugger", debugger); appendKeyValue (vec, "api_key", apiKey); /*********************************/ } if (fields & PER_GROUP_POOL_OPTIONS) { appendKeyValue3(vec, "min_processes", minProcesses); appendKeyValue3(vec, "max_processes", maxProcesses); appendKeyValue2(vec, "max_preloader_idle_time", maxPreloaderIdleTime); appendKeyValue3(vec, "max_out_of_band_work_instances", maxOutOfBandWorkInstances); appendKeyValue (vec, "sticky_sessions_cookie_attributes", stickySessionsCookieAttributes); } /*********************************/ } template<typename Stream> void toXml(Stream &stream, const ResourceLocator &resourceLocator, const WrapperRegistry::Registry &wrapperRegistry, int fields = ALL_OPTIONS) const { vector<string> args; unsigned int i; toVector(args, resourceLocator, wrapperRegistry, fields); for (i = 0; i < args.size(); i += 2) { stream << "<" << args[i] << ">"; stream << escapeForXml(args[i + 1]); stream << "</" << args[i] << ">"; } } /** * Returns the app group name. If there is no explicitly set app group name * then the app root is considered to be the app group name. */ const HashedStaticString &getAppGroupName() const { if (appGroupName.empty()) { return appRoot; } else { return appGroupName; } } string getStartCommand(const ResourceLocator &resourceLocator, const WrapperRegistry::Registry &wrapperRegistry) const { const WrapperRegistry::Entry &entry = wrapperRegistry.lookup(appType); string interpreter; if (entry.language == P_STATIC_STRING("ruby")) { interpreter = escapeShell(ruby); } else if (entry.language == P_STATIC_STRING("python")) { interpreter = escapeShell(python); } else if (entry.language == P_STATIC_STRING("nodejs")) { interpreter = escapeShell(nodejs); } else if (entry.language == P_STATIC_STRING("meteor")) { interpreter = escapeShell(ruby); } else { return appStartCommand; } return interpreter + " " + escapeShell(resourceLocator.getHelperScriptsDir() + "/" + entry.path); } StaticString getStartupFile(const WrapperRegistry::Registry &wrapperRegistry) const { if (startupFile.empty()) { const WrapperRegistry::Entry &entry = wrapperRegistry.lookup(appType); if (entry.isNull() || entry.defaultStartupFiles.empty()) { return StaticString(); } else { return entry.defaultStartupFiles[0]; } } else { return startupFile; } } StaticString getProcessTitle(const WrapperRegistry::Registry ®istry) const { const WrapperRegistry::Entry &entry = registry.lookup(appType); if (entry.isNull()) { return StaticString(); } else { return entry.processTitle; } } unsigned long getMaxPreloaderIdleTime() const { if (maxPreloaderIdleTime == -1) { return DEFAULT_MAX_PRELOADER_IDLE_TIME; } else { return maxPreloaderIdleTime; } } }; } // namespace ApplicationPool2 } // namespace Passenger #endif /* _PASSENGER_APPLICATION_POOL2_OPTIONS_H_ */ ApplicationPool/Process.h 0000644 00000071036 14756456557 0011467 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2011-2018 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef _PASSENGER_APPLICATION_POOL_PROCESS_H_ #define _PASSENGER_APPLICATION_POOL_PROCESS_H_ #include <string> #include <vector> #include <algorithm> #include <boost/intrusive_ptr.hpp> #include <boost/move/core.hpp> #include <boost/container/vector.hpp> #include <oxt/system_calls.hpp> #include <oxt/spin_lock.hpp> #include <oxt/macros.hpp> #include <sys/types.h> #include <cstdio> #include <climits> #include <cassert> #include <cstring> #include <Constants.h> #include <FileDescriptor.h> #include <LoggingKit/LoggingKit.h> #include <SystemTools/ProcessMetricsCollector.h> #include <SystemTools/SystemTime.h> #include <StrIntTools/StrIntUtils.h> #include <Utils/Lock.h> #include <Core/ApplicationPool/Common.h> #include <Core/ApplicationPool/Socket.h> #include <Core/ApplicationPool/Session.h> #include <Core/SpawningKit/PipeWatcher.h> #include <Core/SpawningKit/Result.h> #include <Shared/ApplicationPoolApiKey.h> namespace Passenger { namespace ApplicationPool2 { using namespace std; using namespace boost; typedef boost::container::vector<ProcessPtr> ProcessList; /** * Represents an application process, as spawned by a SpawningKit::Spawner. Every * Process has a PID, a stdin pipe, an output pipe and a list of sockets on which * it listens for connections. A Process object is contained inside a Group. * * The stdin pipe is mapped to the process's STDIN and is used for garbage * collection: closing the STDIN part causes the process to gracefully terminate itself. * * The output pipe is mapped to the process' STDOUT and STDERR. All data coming * from those pipes will be printed. * * Except for the otherwise documented parts, this class is not thread-safe, * so only use within the Pool lock. * * ## Normal usage * * 1. Create a session with newSession(). * 2. Initiate the session by calling initiate() on it. * 3. Perform I/O through session->fd(). * 4. When done, close the session by calling close() on it. * 5. Call process.sessionClosed(). * * ## Life time * * A Process object lives until the containing Group calls `detach(process)`, * which indicates that it wants this Process to shut down. The Process object * is stored in the `detachedProcesses` collection in the Group and is no longer * eligible for receiving requests. Once all requests on this Process have finished, * `triggerShutdown()` will be called, which will send a message to the * OS process telling it to shut down. Once the OS process is gone, `cleanup()` is * called, and the Process object is removed from the collection. * * This means that a Group outlives all its Processes, a Process outlives all * its Sessions, and a Process also outlives the OS process. */ class Process { public: static const unsigned int MAX_SOCKETS_ACCEPTING_HTTP_REQUESTS = 3; private: /************************************************************* * Read-only fields, set once during initialization and never * written to again. Reading is thread-safe. *************************************************************/ BasicProcessInfo info; DynamicBuffer stringBuffer; SocketList sockets; /** * The maximum amount of concurrent sessions this process can handle. * 0 means unlimited. Automatically inferred from the sockets. */ int concurrency; /** * A subset of 'sockets': all sockets that accept HTTP requests * from the Passenger Core controller. */ unsigned int socketsAcceptingHttpRequestsCount; Socket *socketsAcceptingHttpRequests[MAX_SOCKETS_ACCEPTING_HTTP_REQUESTS]; /** Input pipe. See Process class description. */ FileDescriptor inputPipe; /** * Pipe on which this process outputs stdout and stderr data. Mapped to the * process's STDOUT and STDERR. */ FileDescriptor outputPipe; /** * The code revision of the application, inferred through various means. * See Spawner::prepareSpawn() to learn how this is determined. * May be an empty string if no code revision has been inferred. */ StaticString codeRevision; /** * Time at which the Spawner that created this process was created. * Microseconds resolution. */ unsigned long long spawnerCreationTime; /** Time at which we started spawning this process. Microseconds resolution. */ unsigned long long spawnStartTime; /** * Time at which we finished spawning this process, i.e. when this * process was finished initializing. Microseconds resolution. */ unsigned long long spawnEndTime; SpawningKit::Result::Type type; /** * Whether it is required that triggerShutdown() and cleanup() must be called * before destroying this Process. Normally true, except for dummy Process * objects created by Pool::asyncGet() with options.noop == true, because those * processes are never added to Group.enabledProcesses. */ bool requiresShutdown; /************************************************************* * Read-write fields. *************************************************************/ mutable boost::atomic<int> refcount; /** A mutex to protect access to `lifeStatus`. */ mutable oxt::spin_lock lifetimeSyncher; /** The index inside the associated Group's process list. */ unsigned int index; /************************************************************* * Methods *************************************************************/ /****** Initialization and destruction ******/ struct InitializationLog { struct String { unsigned int offset; unsigned int size; }; struct SocketStringOffsets { String address; String protocol; String description; }; vector<SocketStringOffsets> socketStringOffsets; String codeRevision; }; void appendJsonFieldToBuffer(std::string &buffer, const Json::Value &json, const char *key, InitializationLog::String &str, bool required = true) const { StaticString value; if (required) { value = getJsonStaticStringField(json, key); } else { value = getJsonStaticStringField(json, Json::StaticString(key), StaticString()); } str.offset = buffer.size(); str.size = value.size(); buffer.append(value.data(), value.size()); buffer.append(1, '\0'); } void initializeSocketsAndStringFields(const SpawningKit::Result &result) { Json::Value doc, sockets(Json::arrayValue); vector<SpawningKit::Result::Socket>::const_iterator it, end = result.sockets.end(); for (it = result.sockets.begin(); it != end; it++) { sockets.append(it->inspectAsJson()); } doc["sockets"] = sockets; initializeSocketsAndStringFields(doc); } void initializeSocketsAndStringFields(const Json::Value &json) { InitializationLog log; string buffer; // Step 1: append strings to temporary buffer and take note of their // offsets within the temporary buffer. Json::Value sockets = getJsonField(json, "sockets"); // The const_cast here works around a jsoncpp bug. Json::Value::const_iterator it = const_cast<const Json::Value &>(sockets).begin(); Json::Value::const_iterator end = const_cast<const Json::Value &>(sockets).end(); buffer.reserve(1024); for (it = sockets.begin(); it != end; it++) { const Json::Value &socket = *it; InitializationLog::SocketStringOffsets offsets; appendJsonFieldToBuffer(buffer, socket, "address", offsets.address); appendJsonFieldToBuffer(buffer, socket, "protocol", offsets.protocol); appendJsonFieldToBuffer(buffer, socket, "description", offsets.description, false); log.socketStringOffsets.push_back(offsets); } if (json.isMember("code_revision")) { appendJsonFieldToBuffer(buffer, json, "code_revision", log.codeRevision); } // Step 2: allocate the real buffer. this->stringBuffer = DynamicBuffer(buffer.size()); memcpy(this->stringBuffer.data, buffer.data(), buffer.size()); // Step 3: initialize the string fields and point them to // addresses within the real buffer. unsigned int i; const char *base = this->stringBuffer.data; it = const_cast<const Json::Value &>(sockets).begin(); for (i = 0; it != end; it++, i++) { const Json::Value &socket = *it; this->sockets.add( info.pid, StaticString(base + log.socketStringOffsets[i].address.offset, log.socketStringOffsets[i].address.size), StaticString(base + log.socketStringOffsets[i].protocol.offset, log.socketStringOffsets[i].protocol.size), StaticString(base + log.socketStringOffsets[i].description.offset, log.socketStringOffsets[i].description.size), getJsonIntField(socket, "concurrency"), getJsonBoolField(socket, "accept_http_requests") ); } if (json.isMember("code_revision")) { codeRevision = StaticString(base + log.codeRevision.offset, log.codeRevision.size); } } void indexSocketsAcceptingHttpRequests() { SocketList::iterator it; concurrency = 0; memset(socketsAcceptingHttpRequests, 0, sizeof(socketsAcceptingHttpRequests)); for (it = sockets.begin(); it != sockets.end(); it++) { Socket *socket = &(*it); if (!socket->acceptHttpRequests) { continue; } if (socketsAcceptingHttpRequestsCount == MAX_SOCKETS_ACCEPTING_HTTP_REQUESTS) { throw RuntimeException("The process has too many sockets that accept HTTP requests. " "A maximum of " + toString(MAX_SOCKETS_ACCEPTING_HTTP_REQUESTS) + " is allowed"); } socketsAcceptingHttpRequests[socketsAcceptingHttpRequestsCount] = socket; socketsAcceptingHttpRequestsCount++; if (concurrency >= 0) { if (socket->concurrency < 0) { // If one of the sockets has a concurrency of // < 0 (unknown) then we mark this entire Process // as having a concurrency of -1 (unknown). concurrency = -1; } else if (socket->concurrency == 0) { // If one of the sockets has a concurrency of // 0 (unlimited) then we mark this entire Process // as having a concurrency of 0. concurrency = -999; } else { concurrency += socket->concurrency; } } } if (concurrency == -999) { concurrency = 0; } } void destroySelf() const { this->~Process(); LockGuard l(getContext()->memoryManagementSyncher); getContext()->processObjectPool.free(const_cast<Process *>(this)); } /****** Miscellaneous ******/ static bool isZombie(pid_t pid) { string filename = "/proc/" + toString(pid) + "/status"; FILE *f = fopen(filename.c_str(), "r"); if (f == NULL) { // Don't know. return false; } bool result = false; while (!feof(f)) { char buf[512]; const char *line; line = fgets(buf, sizeof(buf), f); if (line == NULL) { break; } if (strcmp(line, "State: Z (zombie)\n") == 0) { // Is a zombie. result = true; break; } } fclose(f); return result; } string getAppGroupName(const BasicGroupInfo *info) const; string getAppLogFile(const BasicGroupInfo *info) const; public: /************************************************************* * Information used by Pool. Do not write to these from * outside the Pool. If you read these make sure the Pool * isn't concurrently modifying. *************************************************************/ /** Last time when a session was opened for this Process. */ unsigned long long lastUsed; /** Number of sessions currently open. * @invariant session >= 0 */ int sessions; /** Number of sessions opened so far. */ unsigned int processed; /** Do not access directly, always use `isAlive()`/`isDead()`/`getLifeStatus()` or * through `lifetimeSyncher`. */ enum LifeStatus { /** Up and operational. */ ALIVE, /** This process has been detached, and the detached processes checker has * verified that there are no active sessions left and has told the process * to shut down. In this state we're supposed to wait until the process * has actually shutdown, after which cleanup() must be called. */ SHUTDOWN_TRIGGERED, /** * The process has exited and cleanup() has been called. In this state, * this object is no longer usable. */ DEAD } lifeStatus; enum EnabledStatus { /** Up and operational. */ ENABLED, /** Process is being disabled. The containing Group is waiting for * all sessions on this Process to finish. It may in some corner * cases still be selected for processing requests. */ DISABLING, /** Process is fully disabled and should not be handling any * requests. It *may* still handle some requests, e.g. by * the Out-of-Band-Work trigger. */ DISABLED, /** * Process has been detached. It will be removed from the Group * as soon we have detected that the OS process has exited. Detached * processes are allowed to finish their requests, but are not * eligible for new requests. */ DETACHED } enabled; enum OobwStatus { /** Process is not using out-of-band work. */ OOBW_NOT_ACTIVE, /** The process has requested out-of-band work. At some point, the code * will see this and set the status to OOBW_IN_PROGRESS. */ OOBW_REQUESTED, /** An out-of-band work is in progress. We need to wait until all * sessions have ended and the process has been disabled before the * out-of-band work can be performed. */ OOBW_IN_PROGRESS, } oobwStatus; /** Caches whether or not the OS process still exists. */ mutable bool m_osProcessExists: 1; bool longRunningConnectionsAborted: 1; /** Time at which shutdown began. */ time_t shutdownStartTime; /** Collected by Pool::collectAnalytics(). */ ProcessMetrics metrics; Process(const BasicGroupInfo *groupInfo, const Json::Value &args) : info(this, groupInfo, args), socketsAcceptingHttpRequestsCount(0), spawnerCreationTime(getJsonUint64Field(args, "spawner_creation_time")), spawnStartTime(getJsonUint64Field(args, "spawn_start_time")), spawnEndTime(SystemTime::getUsec()), type(args["type"] == "dummy" ? SpawningKit::Result::DUMMY : SpawningKit::Result::UNKNOWN), requiresShutdown(false), refcount(1), index(-1), lastUsed(spawnEndTime), sessions(0), processed(0), lifeStatus(ALIVE), enabled(ENABLED), oobwStatus(OOBW_NOT_ACTIVE), m_osProcessExists(true), longRunningConnectionsAborted(false), shutdownStartTime(0) { initializeSocketsAndStringFields(args); indexSocketsAcceptingHttpRequests(); } Process(const BasicGroupInfo *groupInfo, const SpawningKit::Result &skResult, const Json::Value &args) : info(this, groupInfo, skResult), socketsAcceptingHttpRequestsCount(0), spawnerCreationTime(getJsonUint64Field(args, "spawner_creation_time")), spawnStartTime(skResult.spawnStartTime), spawnEndTime(skResult.spawnEndTime), type(skResult.type), requiresShutdown(false), refcount(1), index(-1), lastUsed(spawnEndTime), sessions(0), processed(0), lifeStatus(ALIVE), enabled(ENABLED), oobwStatus(OOBW_NOT_ACTIVE), m_osProcessExists(true), longRunningConnectionsAborted(false), shutdownStartTime(0) { initializeSocketsAndStringFields(skResult); indexSocketsAcceptingHttpRequests(); inputPipe = skResult.stdinFd; outputPipe = skResult.stdoutAndErrFd; if (outputPipe != -1) { SpawningKit::PipeWatcherPtr watcher = boost::make_shared<SpawningKit::PipeWatcher>( outputPipe, "output", getAppGroupName(groupInfo), getAppLogFile(groupInfo), skResult.pid); if (!args["log_file"].isNull()) { watcher->setLogFile(args["log_file"].asString()); } watcher->initialize(); watcher->start(); } } ~Process() { if (OXT_UNLIKELY(requiresShutdown && !isDead())) { P_BUG("You must call Process::triggerShutdown() and Process::cleanup() before actually " "destroying the Process object."); } } void initializeStickySessionId(unsigned int value) { info.stickySessionId = value; } void forceMaxConcurrency(int value) { assert(value >= 0); concurrency = value; for (unsigned i = 0; i < socketsAcceptingHttpRequestsCount; i++) { socketsAcceptingHttpRequests[i]->concurrency = concurrency; } } void shutdownNotRequired() { requiresShutdown = false; } /****** Memory and life time management ******/ void ref() const { refcount.fetch_add(1, boost::memory_order_relaxed); } void unref() const { if (refcount.fetch_sub(1, boost::memory_order_release) == 1) { boost::atomic_thread_fence(boost::memory_order_acquire); destroySelf(); } } ProcessPtr shared_from_this() { return ProcessPtr(this); } static void forceTriggerShutdownAndCleanup(ProcessPtr process) { if (process != NULL) { process->triggerShutdown(); // Pretend like the OS process has exited so // that the canCleanup() precondition is true. process->m_osProcessExists = false; process->cleanup(); } } // Thread-safe. bool isAlive() const { oxt::spin_lock::scoped_lock lock(lifetimeSyncher); return lifeStatus == ALIVE; } // Thread-safe. bool hasTriggeredShutdown() const { oxt::spin_lock::scoped_lock lock(lifetimeSyncher); return lifeStatus == SHUTDOWN_TRIGGERED; } // Thread-safe. bool isDead() const { oxt::spin_lock::scoped_lock lock(lifetimeSyncher); return lifeStatus == DEAD; } // Thread-safe. LifeStatus getLifeStatus() const { oxt::spin_lock::scoped_lock lock(lifetimeSyncher); return lifeStatus; } bool canTriggerShutdown() const { return getLifeStatus() == ALIVE && sessions == 0; } void triggerShutdown() { assert(canTriggerShutdown()); { time_t now = SystemTime::get(); oxt::spin_lock::scoped_lock lock(lifetimeSyncher); assert(lifeStatus == ALIVE); lifeStatus = SHUTDOWN_TRIGGERED; shutdownStartTime = now; } if (inputPipe != -1) { inputPipe.close(); } if (type == SpawningKit::Result::GENERIC) { syscalls::kill(getPid(), SIGTERM); } } bool shutdownTimeoutExpired() const { return SystemTime::get() >= shutdownStartTime + PROCESS_SHUTDOWN_TIMEOUT; } bool canCleanup() const { return getLifeStatus() == SHUTDOWN_TRIGGERED && !osProcessExists(); } void cleanup() { assert(canCleanup()); P_TRACE(2, "Cleaning up process " << inspect()); if (type != SpawningKit::Result::DUMMY) { SocketList::iterator it, end = sockets.end(); for (it = sockets.begin(); it != end; it++) { if (getSocketAddressType(it->address) == SAT_UNIX) { string filename = parseUnixSocketAddress(it->address); syscalls::unlink(filename.c_str()); } it->closeAllConnections(); } } oxt::spin_lock::scoped_lock lock(lifetimeSyncher); lifeStatus = DEAD; } /****** Basic information queries ******/ OXT_FORCE_INLINE Context *getContext() const { return info.groupInfo->context; } Group *getGroup() const { return info.groupInfo->group; } StaticString getGroupName() const { return info.groupInfo->name; } const ApiKey &getApiKey() const { return info.groupInfo->apiKey; } const BasicProcessInfo &getInfo() const { return info; } pid_t getPid() const { return info.pid; } StaticString getGupid() const { return StaticString(info.gupid, info.gupidSize); } unsigned int getStickySessionId() const { return info.stickySessionId; } unsigned long long getSpawnerCreationTime() const { return spawnerCreationTime; } bool isDummy() const { return type == SpawningKit::Result::DUMMY; } /****** Miscellaneous ******/ unsigned int getIndex() const { return index; } void setIndex(unsigned int i) { index = i; } const SocketList &getSockets() const { return sockets; } Socket *findSocketsAcceptingHttpRequestsAndWithLowestBusyness() const { if (OXT_UNLIKELY(socketsAcceptingHttpRequestsCount == 0)) { return NULL; } else if (socketsAcceptingHttpRequestsCount == 1) { return socketsAcceptingHttpRequests[0]; } else { int leastBusySocketIndex = 0; int lowestBusyness = socketsAcceptingHttpRequests[0]->busyness(); for (unsigned i = 1; i < socketsAcceptingHttpRequestsCount; i++) { if (socketsAcceptingHttpRequests[i]->busyness() < lowestBusyness) { leastBusySocketIndex = i; lowestBusyness = socketsAcceptingHttpRequests[i]->busyness(); } } return socketsAcceptingHttpRequests[leastBusySocketIndex]; } } /** Checks whether the OS process exists. * Once it has been detected that it doesn't, that event is remembered * so that we don't accidentally ping any new processes that have the * same PID. */ bool osProcessExists() const { if (type != SpawningKit::Result::DUMMY && m_osProcessExists) { if (syscalls::kill(getPid(), 0) == 0) { /* On some environments, e.g. Heroku, the init process does * not properly reap adopted zombie processes, which can interfere * with our process existance check. To work around this, we * explicitly check whether or not the process has become a zombie. */ m_osProcessExists = !isZombie(getPid()); } else { m_osProcessExists = errno != ESRCH; } return m_osProcessExists; } else { return false; } } /** Kill the OS process with the given signal. */ int kill(int signo) { if (osProcessExists()) { return syscalls::kill(getPid(), signo); } else { return 0; } } int busyness() const { /* Different processes within a Group may have different * 'concurrency' values. We want: * - the process with the smallest busyness to be be picked for routing. * - to give processes with concurrency == 0 or -1 more priority (in general) * over processes with concurrency > 0. * Therefore, in case of processes with concurrency > 0, we describe our * busyness as a percentage of 'concurrency', with the percentage value * in [0..INT_MAX] instead of [0..1]. That way, the busyness value * of processes with concurrency > 0 is usually higher than that of processes * with concurrency == 0 or -1. */ if (concurrency <= 0) { return sessions; } else { return (int) (((long long) sessions * INT_MAX) / (double) concurrency); } } /** * Whether we've reached the maximum number of concurrent sessions for this * process. */ bool isTotallyBusy() const { return concurrency > 0 && sessions >= concurrency; } /** * Whether a get() request can be routed to this process, assuming that * the sticky session ID (if any) matches. This is only not the case * if this process is totally busy. */ bool canBeRoutedTo() const { return !isTotallyBusy(); } /** * Create a new communication session with this process. This will connect to one * of the session sockets or reuse an existing connection. See Session for * more information about sessions. * * If you know the current time (in microseconds), pass it to `now`, which * prevents this function from having to query the time. * * You SHOULD call sessionClosed() when one's done with the session. * Failure to do so will mess up internal statistics but will otherwise * not result in any harmful behavior. */ SessionPtr newSession(unsigned long long now = 0) { Socket *socket = findSocketsAcceptingHttpRequestsAndWithLowestBusyness(); if (socket->isTotallyBusy()) { return SessionPtr(); } else { socket->sessions++; this->sessions++; if (now != 0) { lastUsed = now; } else { lastUsed = SystemTime::getUsec(); } return createSessionObject(socket); } } SessionPtr createSessionObject(Socket *socket) { struct Guard { Context *context; Session *session; Guard(Context *c, Session *s) : context(c), session(s) { } ~Guard() { if (session != NULL) { context->sessionObjectPool.free(session); } } void clear() { session = NULL; } }; Context *context = getContext(); LockGuard l(context->memoryManagementSyncher); Session *session = context->sessionObjectPool.malloc(); Guard guard(context, session); session = new (session) Session(context, &info, socket); guard.clear(); return SessionPtr(session, false); } void sessionClosed(Session *session) { Socket *socket = session->getSocket(); assert(socket->sessions > 0); assert(sessions > 0); socket->sessions--; this->sessions--; processed++; assert(!isTotallyBusy()); } /** * Returns the uptime of this process so far, as a string. */ string uptime() const { return distanceOfTimeInWords(spawnEndTime / 1000000); } string inspect() const { assert(getLifeStatus() != DEAD); stringstream result; result << "(pid=" << getPid() << ", group=" << getGroupName() << ")"; return result.str(); } template<typename Stream> void inspectXml(Stream &stream, bool includeSockets = true) const { stream << "<pid>" << getPid() << "</pid>"; stream << "<sticky_session_id>" << getStickySessionId() << "</sticky_session_id>"; stream << "<gupid>" << getGupid() << "</gupid>"; stream << "<concurrency>" << concurrency << "</concurrency>"; stream << "<sessions>" << sessions << "</sessions>"; stream << "<busyness>" << busyness() << "</busyness>"; stream << "<processed>" << processed << "</processed>"; stream << "<spawner_creation_time>" << spawnerCreationTime << "</spawner_creation_time>"; stream << "<spawn_start_time>" << spawnStartTime << "</spawn_start_time>"; stream << "<spawn_end_time>" << spawnEndTime << "</spawn_end_time>"; stream << "<last_used>" << lastUsed << "</last_used>"; stream << "<last_used_desc>" << distanceOfTimeInWords(lastUsed / 1000000).c_str() << " ago</last_used_desc>"; stream << "<uptime>" << uptime() << "</uptime>"; if (!codeRevision.empty()) { stream << "<code_revision>" << escapeForXml(codeRevision) << "</code_revision>"; } switch (lifeStatus) { case ALIVE: stream << "<life_status>ALIVE</life_status>"; break; case SHUTDOWN_TRIGGERED: stream << "<life_status>SHUTDOWN_TRIGGERED</life_status>"; break; case DEAD: stream << "<life_status>DEAD</life_status>"; break; default: P_BUG("Unknown 'lifeStatus' state " << (int) lifeStatus); } switch (enabled) { case ENABLED: stream << "<enabled>ENABLED</enabled>"; break; case DISABLING: stream << "<enabled>DISABLING</enabled>"; break; case DISABLED: stream << "<enabled>DISABLED</enabled>"; break; case DETACHED: stream << "<enabled>DETACHED</enabled>"; break; default: P_BUG("Unknown 'enabled' state " << (int) enabled); } if (metrics.isValid()) { stream << "<has_metrics>true</has_metrics>"; stream << "<cpu>" << (int) metrics.cpu << "</cpu>"; stream << "<rss>" << metrics.rss << "</rss>"; stream << "<pss>" << metrics.pss << "</pss>"; stream << "<private_dirty>" << metrics.privateDirty << "</private_dirty>"; stream << "<swap>" << metrics.swap << "</swap>"; stream << "<real_memory>" << metrics.realMemory() << "</real_memory>"; stream << "<vmsize>" << metrics.vmsize << "</vmsize>"; stream << "<process_group_id>" << metrics.processGroupId << "</process_group_id>"; stream << "<command>" << escapeForXml(metrics.command) << "</command>"; } if (includeSockets) { SocketList::const_iterator it; stream << "<sockets>"; for (it = sockets.begin(); it != sockets.end(); it++) { const Socket &socket = *it; stream << "<socket>"; stream << "<address>" << escapeForXml(socket.address) << "</address>"; stream << "<protocol>" << escapeForXml(socket.protocol) << "</protocol>"; if (!socket.description.empty()) { stream << "<description>" << escapeForXml(socket.description) << "</description>"; } stream << "<concurrency>" << socket.concurrency << "</concurrency>"; stream << "<accept_http_requests>" << socket.acceptHttpRequests << "</accept_http_requests>"; stream << "<sessions>" << socket.sessions << "</sessions>"; stream << "</socket>"; } stream << "</sockets>"; } } }; inline void intrusive_ptr_add_ref(const Process *process) { process->ref(); } inline void intrusive_ptr_release(const Process *process) { process->unref(); } } // namespace ApplicationPool2 } // namespace Passenger #endif /* _PASSENGER_APPLICATION_POOL2_PROCESS_H_ */ ApplicationPool/TestSession.h 0000644 00000011664 14756456557 0012335 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2016-2018 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef _PASSENGER_APPLICATION_POOL_TEST_SESSION_H_ #define _PASSENGER_APPLICATION_POOL_TEST_SESSION_H_ #include <boost/thread.hpp> #include <string> #include <cassert> #include <IOTools/IOUtils.h> #include <IOTools/BufferedIO.h> #include <Core/ApplicationPool/AbstractSession.h> namespace Passenger { namespace ApplicationPool2 { using namespace std; /** * The TestSession represents a session between the Core to the Application. There is a * connection between the two, which is represented by a SocketPair having a first (Core side) * and second (Application side) FD. These are also referred to as fd and peerFd. */ class TestSession: public AbstractSession { private: mutable boost::mutex syncher; mutable unsigned int refcount; pid_t pid; string gupid; string protocol; ApiKey apiKey; SocketPair connection; BufferedIO peerBufferedIO; unsigned int stickySessionId; mutable bool closed; mutable bool success; mutable bool wantKeepAlive; public: TestSession() : refcount(1), pid(123), gupid("gupid-123"), protocol("session"), stickySessionId(0), closed(false), success(false), wantKeepAlive(false) { } virtual void ref() const { boost::lock_guard<boost::mutex> l(syncher); assert(refcount > 0); refcount++; } virtual void unref() const { boost::lock_guard<boost::mutex> l(syncher); assert(refcount > 0); refcount--; if (refcount == 0) { if (!closed) { closed = true; success = false; wantKeepAlive = false; } } } virtual pid_t getPid() const { boost::lock_guard<boost::mutex> l(syncher); return pid; } void setPid(pid_t p) { boost::lock_guard<boost::mutex> l(syncher); pid = p; } virtual StaticString getGupid() const { boost::lock_guard<boost::mutex> l(syncher); return gupid; } void setGupid(const string &v) { boost::lock_guard<boost::mutex> l(syncher); gupid = v; } virtual StaticString getProtocol() const { boost::lock_guard<boost::mutex> l(syncher); return protocol; } void setProtocol(const string &v) { boost::lock_guard<boost::mutex> l(syncher); protocol = v; } virtual unsigned int getStickySessionId() const { boost::lock_guard<boost::mutex> l(syncher); return stickySessionId; } void setStickySessionId(unsigned int v) { boost::lock_guard<boost::mutex> l(syncher); stickySessionId = v; } virtual const ApiKey &getApiKey() const { return apiKey; } virtual int fd() const { boost::lock_guard<boost::mutex> l(syncher); return connection.first; } virtual int peerFd() const { boost::lock_guard<boost::mutex> l(syncher); return connection.second; } virtual BufferedIO &getPeerBufferedIO() { boost::lock_guard<boost::mutex> l(syncher); return peerBufferedIO; } virtual bool isClosed() const { boost::lock_guard<boost::mutex> l(syncher); return closed; } bool isSuccessful() const { boost::lock_guard<boost::mutex> l(syncher); return success; } bool wantsKeepAlive() const { boost::lock_guard<boost::mutex> l(syncher); return wantKeepAlive; } virtual void initiate(bool blocking = true) { boost::lock_guard<boost::mutex> l(syncher); connection = createUnixSocketPair(__FILE__, __LINE__); peerBufferedIO = BufferedIO(connection.second); if (!blocking) { setNonBlocking(connection.first); } } virtual void close(bool _success, bool _wantKeepAlive = false) { boost::lock_guard<boost::mutex> l(syncher); closed = true; success = _success; wantKeepAlive = _wantKeepAlive; } void closePeerFd() { boost::lock_guard<boost::mutex> l(syncher); connection.second.close(); } }; } // namespace ApplicationPool2 } // namespace Passenger #endif /* _PASSENGER_APPLICATION_POOL2_TEST_SESSION_H_ */ ApplicationPool/Socket.h 0000644 00000020766 14756456557 0011305 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2011-2018 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef _PASSENGER_APPLICATION_POOL_SOCKET_H_ #define _PASSENGER_APPLICATION_POOL_SOCKET_H_ #include <vector> #include <oxt/macros.hpp> #include <boost/thread.hpp> #include <boost/shared_ptr.hpp> #include <boost/weak_ptr.hpp> #include <boost/container/small_vector.hpp> #include <climits> #include <cassert> #include <LoggingKit/LoggingKit.h> #include <StaticString.h> #include <MemoryKit/palloc.h> #include <IOTools/IOUtils.h> #include <Core/ApplicationPool/Common.h> namespace Passenger { namespace ApplicationPool2 { using namespace std; using namespace boost; struct Connection { int fd; bool wantKeepAlive: 1; bool fail: 1; bool blocking: 1; Connection() : fd(-1), wantKeepAlive(false), fail(false), blocking(true) { } void close() { if (fd != -1) { int fd2 = fd; fd = -1; wantKeepAlive = false; safelyClose(fd2); P_LOG_FILE_DESCRIPTOR_CLOSE(fd2); } } }; /** * Not thread-safe except for the connection pooling methods, so only use * within the ApplicationPool lock. */ class Socket { private: boost::mutex connectionPoolLock; vector<Connection> idleConnections; OXT_FORCE_INLINE int connectionPoolLimit() const { return concurrency; } Connection connect() const { Connection connection; P_TRACE(3, "Connecting to " << address); connection.fd = connectToServer(address, __FILE__, __LINE__); connection.fail = true; connection.wantKeepAlive = false; connection.blocking = true; P_LOG_FILE_DESCRIPTOR_PURPOSE(connection.fd, "App " << pid << " connection"); return connection; } public: // Socket properties. Read-only. StaticString address; StaticString protocol; StaticString description; pid_t pid; /** * Special values: * 0 = unlimited concurrency * -1 = unknown */ int concurrency; bool acceptHttpRequests; // Private. In public section as alignment optimization. int totalConnections; int totalIdleConnections; /** Invariant: sessions >= 0 */ int sessions; Socket() : pid(-1), concurrency(-1), acceptHttpRequests(0) { } Socket(pid_t _pid, const StaticString &_address, const StaticString &_protocol, const StaticString &_description, int _concurrency, bool _acceptHttpRequests) : address(_address), protocol(_protocol), description(_description), pid(_pid), concurrency(_concurrency), acceptHttpRequests(_acceptHttpRequests), totalConnections(0), totalIdleConnections(0), sessions(0) { } Socket(const Socket &other) : idleConnections(other.idleConnections), address(other.address), protocol(other.protocol), description(other.description), pid(other.pid), concurrency(other.concurrency), acceptHttpRequests(other.acceptHttpRequests), totalConnections(other.totalConnections), totalIdleConnections(other.totalIdleConnections), sessions(other.sessions) { } Socket &operator=(const Socket &other) { totalConnections = other.totalConnections; totalIdleConnections = other.totalIdleConnections; idleConnections = other.idleConnections; address = other.address; protocol = other.protocol; description = other.description; pid = other.pid; concurrency = other.concurrency; acceptHttpRequests = other.acceptHttpRequests; sessions = other.sessions; return *this; } /** * Connect to this socket or reuse an existing connection. * * One MUST call checkinConnection() when one's done using the Connection. * Failure to do so will result in a resource leak. */ Connection checkoutConnection() { boost::unique_lock<boost::mutex> l(connectionPoolLock); if (!idleConnections.empty()) { P_TRACE(3, "Socket " << address << ": checking out connection from connection pool (" << idleConnections.size() << " -> " << (idleConnections.size() - 1) << " items). Current total number of connections: " << totalConnections); Connection connection = idleConnections.back(); idleConnections.pop_back(); totalIdleConnections--; return connection; } else { Connection connection = connect(); totalConnections++; P_TRACE(3, "Socket " << address << ": there are now " << totalConnections << " total connections"); l.unlock(); return connection; } } void checkinConnection(Connection &connection) { boost::unique_lock<boost::mutex> l(connectionPoolLock); if (connection.fail || !connection.wantKeepAlive || totalIdleConnections >= connectionPoolLimit()) { totalConnections--; assert(totalConnections >= 0); P_TRACE(3, "Socket " << address << ": connection not checked back into " "connection pool. There are now " << totalConnections << " connections in total"); l.unlock(); connection.close(); } else { P_TRACE(3, "Socket " << address << ": checking in connection into connection pool (" << totalIdleConnections << " -> " << (totalIdleConnections + 1) << " items). Current total number of connections: " << totalConnections); totalIdleConnections++; idleConnections.push_back(connection); } } void closeAllConnections() { boost::unique_lock<boost::mutex> l(connectionPoolLock); assert(sessions == 0); assert(totalConnections == totalIdleConnections); vector<Connection>::iterator it, end = idleConnections.end(); for (it = idleConnections.begin(); it != end; it++) { try { it->close(); } catch (const SystemException &e) { P_ERROR("Cannot close a connection with socket " << address << ": " << e.what()); } } idleConnections.clear(); totalConnections = 0; totalIdleConnections = 0; } bool isIdle() const { return sessions == 0; } int busyness() const { /* Different sockets within a Process may have different * 'concurrency' values. We want: * - the socket with the smallest busyness to be be picked for routing. * - to give sockets with concurrency == 0 or -1 more priority (in general) * over sockets with concurrency > 0. * Therefore, in case of sockets with concurrency > 0, we describe our * busyness as a percentage of 'concurrency', with the percentage value * in [0..INT_MAX] instead of [0..1]. That way, the busyness value * of sockets with concurrency > 0 is usually higher than that of sockets * with concurrency == 0 or -1. */ if (concurrency <= 0) { return sessions; } else { return (int) (((long long) sessions * INT_MAX) / (double) concurrency); } } bool isTotallyBusy() const { return concurrency > 0 && sessions >= concurrency; } void recreateStrings(psg_pool_t *newPool) { recreateString(newPool, address); recreateString(newPool, protocol); recreateString(newPool, description); } }; class SocketList: public boost::container::small_vector<Socket, 1> { public: void add(pid_t pid, const StaticString &address, const StaticString &protocol, const StaticString &description, int concurrency, bool acceptHttpRequests) { push_back(Socket(pid, address, protocol, description, concurrency, acceptHttpRequests)); } const Socket *findFirstSocketWithProtocol(const StaticString &protocol) const { const_iterator it, end = this->end(); for (it = begin(); it != end; it++) { if (it->protocol == protocol) { return &(*it); } } return NULL; } }; typedef boost::shared_ptr<SocketList> SocketListPtr; } // namespace ApplicationPool2 } // namespace Passenger #endif /* _PASSENGER_APPLICATION_POOL2_SOCKET_H_ */ ApplicationPool/Pool/GroupUtils.cpp 0000644 00000017171 14756456557 0013432 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2011-2017 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include <Core/ApplicationPool/Pool.h> /************************************************************************* * * Group data structure utility functions for ApplicationPool2::Pool * *************************************************************************/ namespace Passenger { namespace ApplicationPool2 { using namespace std; using namespace boost; /**************************** * * Consider these to be private methods, * they are only marked public for unit testing! * ****************************/ const pair<uid_t, gid_t> Pool::getGroupRunUidAndGids(const StaticString &appGroupName) { LockGuard l(syncher); GroupPtr *group; if (!groups.lookup(appGroupName.c_str(), &group)) { throw RuntimeException("Could not find group: " + appGroupName); } else { SpawningKit::UserSwitchingInfo info = SpawningKit::prepareUserSwitching((*group)->options, *context->getWrapperRegistry()); return pair<uid_t, gid_t>(info.uid,info.gid); } } const GroupPtr Pool::getGroup(const char *name) { GroupPtr *group; if (groups.lookup(name, &group)) { return *group; } else { return GroupPtr(); } } Group * Pool::findMatchingGroup(const Options &options) { GroupPtr *group; if (groups.lookup(options.getAppGroupName(), &group)) { return group->get(); } else { return NULL; } } GroupPtr Pool::createGroup(const Options &options) { GroupPtr group = boost::make_shared<Group>(this, options); group->initialize(); groups.insert(options.getAppGroupName(), group); wakeupGarbageCollector(); return group; } GroupPtr Pool::createGroupAndAsyncGetFromIt(const Options &options, const GetCallback &callback, boost::container::vector<Callback> &postLockActions) { GroupPtr group = createGroup(options); SessionPtr session = group->get(options, callback, postLockActions); /* If !options.noop, then the callback should now have been put on the * wait list, unless something has changed and we forgot to update * some code here... */ if (session != NULL) { assert(options.noop); postLockActions.push_back(boost::bind(GetCallback::call, callback, session, ExceptionPtr())); } return group; } /** * Forcefully destroys and detaches the given Group. After detaching * the Group may have a non-empty getWaitlist so be sure to do * something with it. * * Also, one of the post lock actions can potentially perform a long-running * operation, so running them in a thread is advised. */ void Pool::forceDetachGroup(const GroupPtr &group, const Callback &callback, boost::container::vector<Callback> &postLockActions) { assert(group->getWaitlist.empty()); const GroupPtr p = group; // Prevent premature destruction. bool removed = groups.erase(group->getName()); assert(removed); (void) removed; // Shut up compiler warning. group->shutdown(callback, postLockActions); } void Pool::syncDetachGroupCallback(boost::shared_ptr<DetachGroupWaitTicket> ticket) { LockGuard l(ticket->syncher); ticket->done = true; ticket->cond.notify_one(); } void Pool::waitDetachGroupCallback(boost::shared_ptr<DetachGroupWaitTicket> ticket) { ScopedLock l(ticket->syncher); while (!ticket->done) { ticket->cond.wait(l); } } /**************************** * * Public methods * ****************************/ GroupPtr Pool::findOrCreateGroup(const Options &options) { Options options2 = options; options2.noop = true; Ticket ticket; { LockGuard l(syncher); GroupPtr *group; if (!groups.lookup(options.getAppGroupName(), &group)) { // Forcefully create Group, don't care whether resource limits // actually allow it. createGroup(options); } } return get(options2, &ticket)->getGroup()->shared_from_this(); } GroupPtr Pool::findGroupByApiKey(const StaticString &value, bool lock) const { DynamicScopedLock l(syncher, lock); GroupMap::ConstIterator g_it(groups); while (*g_it != NULL) { const GroupPtr &group = g_it.getValue(); if (group->getApiKey() == value) { return group; } g_it.next(); } return GroupPtr(); } bool Pool::detachGroupByName(const HashedStaticString &name) { TRACE_POINT(); ScopedLock l(syncher); GroupPtr group = groups.lookupCopy(name); if (OXT_LIKELY(group != NULL)) { P_ASSERT_EQ(group->getName(), name); UPDATE_TRACE_POINT(); verifyInvariants(); verifyExpensiveInvariants(); boost::container::vector<Callback> actions; boost::shared_ptr<DetachGroupWaitTicket> ticket = boost::make_shared<DetachGroupWaitTicket>(); ExceptionPtr exception = copyException( GetAbortedException("The containing Group was detached.")); assignExceptionToGetWaiters(group->getWaitlist, exception, actions); forceDetachGroup(group, boost::bind(syncDetachGroupCallback, ticket), actions); possiblySpawnMoreProcessesForExistingGroups(); verifyInvariants(); verifyExpensiveInvariants(); l.unlock(); UPDATE_TRACE_POINT(); runAllActions(actions); actions.clear(); UPDATE_TRACE_POINT(); ScopedLock l2(ticket->syncher); while (!ticket->done) { ticket->cond.wait(l2); } return true; } else { return false; } } bool Pool::detachGroupByApiKey(const StaticString &value) { ScopedLock l(syncher); GroupPtr group = findGroupByApiKey(value, false); if (group != NULL) { string name = group->getName(); group.reset(); l.unlock(); return detachGroupByName(name); } else { return false; } } bool Pool::restartGroupByName(const StaticString &name, const RestartOptions &options) { ScopedLock l(syncher); GroupMap::ConstIterator g_it(groups); while (*g_it != NULL) { const GroupPtr &group = g_it.getValue(); if (name == group->getName()) { if (!group->authorizeByUid(options.uid) && !group->authorizeByApiKey(options.apiKey)) { throw SecurityException("Operation unauthorized"); } if (!group->restarting()) { group->restart(group->options, options.method); } return true; } g_it.next(); } return false; } unsigned int Pool::restartGroupsByAppRoot(const StaticString &appRoot, const RestartOptions &options) { ScopedLock l(syncher); GroupMap::ConstIterator g_it(groups); unsigned int result = 0; while (*g_it != NULL) { const GroupPtr &group = g_it.getValue(); if (appRoot == group->options.appRoot) { if (group->authorizeByUid(options.uid) || group->authorizeByApiKey(options.apiKey)) { result++; group->restart(group->options, options.method); } } g_it.next(); } return result; } } // namespace ApplicationPool2 } // namespace Passenger ApplicationPool/Pool/InitializationAndShutdown.cpp 0000644 00000010261 14756456557 0016454 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2011-2018 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include <Core/ApplicationPool/Pool.h> /************************************************************************* * * Initialization and shutdown-related code for ApplicationPool2::Pool * *************************************************************************/ namespace Passenger { namespace ApplicationPool2 { using namespace std; using namespace boost; Pool::Pool(Context *_context) : context(_context), abortLongRunningConnectionsCallback(NULL) { try { systemMetricsCollector.collect(systemMetrics); } catch (const RuntimeException &e) { P_WARN("Unable to collect system metrics: " << e.what()); } lifeStatus = ALIVE; max = 6; maxIdleTime = 60 * 1000000; selfchecking = true; palloc = psg_create_pool(PSG_DEFAULT_POOL_SIZE); // The following code only serve to instantiate certain inline methods // so that they can be invoked from gdb. (void) GroupPtr().get(); (void) ProcessPtr().get(); (void) SessionPtr().get(); } Pool::~Pool() { if (lifeStatus != SHUT_DOWN) { P_BUG("You must call Pool::destroy() before actually destroying the Pool object!"); } psg_destroy_pool(palloc); } /** Must be called right after construction. */ void Pool::initialize() { LockGuard l(syncher); initializeAnalyticsCollection(); initializeGarbageCollection(); } void Pool::initDebugging() { LockGuard l(syncher); debugSupport = boost::make_shared<DebugSupport>(); } /** * Should be called right after the agent has received * the message to exit gracefully. This will tell processes to * abort any long-running connections, e.g. WebSocket connections, * because the Core::Controller has to wait until all connections are * finished before proceeding with shutdown. */ void Pool::prepareForShutdown() { TRACE_POINT(); ScopedLock lock(syncher); assert(lifeStatus == ALIVE); lifeStatus = PREPARED_FOR_SHUTDOWN; if (abortLongRunningConnectionsCallback) { vector<ProcessPtr> processes = getProcesses(false); foreach (ProcessPtr process, processes) { // Ensure that the process is not immediately respawned. process->getGroup()->options.minProcesses = 0; abortLongRunningConnectionsCallback(process); } } } /** Must be called right before destruction. */ void Pool::destroy() { TRACE_POINT(); ScopedLock lock(syncher); assert(lifeStatus == ALIVE || lifeStatus == PREPARED_FOR_SHUTDOWN); lifeStatus = SHUTTING_DOWN; while (!groups.empty()) { GroupPtr *group; groups.lookupRandom(NULL, &group); string name = group->get()->getName().toString(); lock.unlock(); detachGroupByName(name); lock.lock(); } UPDATE_TRACE_POINT(); lock.unlock(); P_DEBUG("Shutting down ApplicationPool background threads..."); interruptableThreads.interrupt_and_join_all(); nonInterruptableThreads.join_all(); lock.lock(); lifeStatus = SHUT_DOWN; UPDATE_TRACE_POINT(); verifyInvariants(); verifyExpensiveInvariants(); } } // namespace ApplicationPool2 } // namespace Passenger ApplicationPool/Pool/GeneralUtils.cpp 0000644 00000014357 14756456557 0013716 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2011-2017 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include <Core/ApplicationPool/Pool.h> /************************************************************************* * * General utility functions for ApplicationPool2::Pool * *************************************************************************/ namespace Passenger { namespace ApplicationPool2 { using namespace std; using namespace boost; /**************************** * * Private methods * ****************************/ const char * Pool::maybeColorize(const InspectOptions &options, const char *color) { if (options.colorize) { return color; } else { return ""; } } const char * Pool::maybePluralize(unsigned int count, const char *singular, const char *plural) { if (count == 1) { return singular; } else { return plural; } } void Pool::runAllActions(const boost::container::vector<Callback> &actions) { boost::container::vector<Callback>::const_iterator it, end = actions.end(); for (it = actions.begin(); it != end; it++) { (*it)(); } } void Pool::runAllActionsWithCopy(boost::container::vector<Callback> actions) { runAllActions(actions); } bool Pool::runHookScripts(const char *name, const boost::function<void (HookScriptOptions &)> &setup) const { ScopedLock l(context->agentConfigSyncher); if (!context->agentConfig.isNull()) { string hookName = string("hook_") + name; string spec = context->agentConfig.get(hookName, Json::Value()).asString(); if (!spec.empty()) { HookScriptOptions options; options.agentConfig = context->agentConfig; l.unlock(); options.name = name; options.spec = spec; setup(options); return Passenger::runHookScripts(options); } else { return true; } } else { return true; } } void Pool::verifyInvariants() const { // !a || b: logical equivalent of a IMPLIES b. #ifndef NDEBUG if (!selfchecking) { return; } assert(!( !getWaitlist.empty() ) || ( atFullCapacityUnlocked() )); assert(!( !atFullCapacityUnlocked() ) || ( getWaitlist.empty() )); #endif } void Pool::verifyExpensiveInvariants() const { #ifndef NDEBUG if (!selfchecking) { return; } vector<GetWaiter>::const_iterator it, end = getWaitlist.end(); for (it = getWaitlist.begin(); it != end; it++) { const GetWaiter &waiter = *it; const GroupPtr *group; assert(!groups.lookup(waiter.options.getAppGroupName(), &group)); } #endif } void Pool::fullVerifyInvariants() const { TRACE_POINT(); verifyInvariants(); UPDATE_TRACE_POINT(); verifyExpensiveInvariants(); UPDATE_TRACE_POINT(); GroupMap::ConstIterator g_it(groups); while (*g_it != NULL) { const GroupPtr &group = g_it.getValue(); group->verifyInvariants(); group->verifyExpensiveInvariants(); g_it.next(); } } /** * Process all waiters on the getWaitlist. Call when capacity has become free. * This function assigns sessions to them by calling get() on the corresponding * Groups, or by creating more Groups, in so far the new capacity allows. */ void Pool::assignSessionsToGetWaiters(boost::container::vector<Callback> &postLockActions) { bool done = false; vector<GetWaiter>::iterator it, end = getWaitlist.end(); vector<GetWaiter> newWaitlist; for (it = getWaitlist.begin(); it != end && !done; it++) { GetWaiter &waiter = *it; Group *group = findMatchingGroup(waiter.options); if (group != NULL) { SessionPtr session = group->get(waiter.options, waiter.callback, postLockActions); if (session != NULL) { postLockActions.push_back(boost::bind(GetCallback::call, waiter.callback, session, ExceptionPtr())); } /* else: the callback has now been put in * the group's get wait list. */ } else if (!atFullCapacityUnlocked()) { createGroupAndAsyncGetFromIt(waiter.options, waiter.callback, postLockActions); } else { /* Still cannot satisfy this get request. Keep it on the get * wait list and try again later. */ newWaitlist.push_back(waiter); } } std::swap(getWaitlist, newWaitlist); } template<typename Queue> void Pool::assignExceptionToGetWaiters(Queue &getWaitlist, const ExceptionPtr &exception, boost::container::vector<Callback> &postLockActions) { while (!getWaitlist.empty()) { postLockActions.push_back(boost::bind(GetCallback::call, getWaitlist.front().callback, SessionPtr(), exception)); getWaitlist.pop_front(); } } void Pool::syncGetCallback(const AbstractSessionPtr &session, const ExceptionPtr &e, void *userData) { Ticket *ticket = static_cast<Ticket *>(userData); ScopedLock lock(ticket->syncher); if (OXT_LIKELY(session != NULL)) { ticket->session = static_pointer_cast<Session>(session); } else { ticket->exception = e; } ticket->cond.notify_one(); } /**************************** * * Public methods * ****************************/ Context * Pool::getContext() { return context; } SpawningKit::Context * Pool::getSpawningKitContext() const { return context->getSpawningKitContext(); } const RandomGeneratorPtr & Pool::getRandomGenerator() const { return context->getRandomGenerator(); } } // namespace ApplicationPool2 } // namespace Passenger ApplicationPool/Pool/GarbageCollection.cpp 0000644 00000013454 14756456557 0014661 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2011-2017 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include <Core/ApplicationPool/Pool.h> /************************************************************************* * * Garbage collection functions for ApplicationPool2::Pool * *************************************************************************/ namespace Passenger { namespace ApplicationPool2 { using namespace std; using namespace boost; void Pool::initializeGarbageCollection() { interruptableThreads.create_thread( boost::bind(garbageCollect, shared_from_this()), "Pool garbage collector", POOL_HELPER_THREAD_STACK_SIZE ); } void Pool::garbageCollect(PoolPtr self) { TRACE_POINT(); { ScopedLock lock(self->syncher); self->garbageCollectionCond.timed_wait(lock, posix_time::seconds(5)); } while (!boost::this_thread::interruption_requested()) { try { UPDATE_TRACE_POINT(); unsigned long long sleepTime = self->realGarbageCollect(); UPDATE_TRACE_POINT(); ScopedLock lock(self->syncher); self->garbageCollectionCond.timed_wait(lock, posix_time::microseconds(sleepTime)); } catch (const thread_interrupted &) { break; } catch (const tracable_exception &e) { P_WARN("ERROR: " << e.what() << "\n Backtrace:\n" << e.backtrace()); } } } void Pool::maybeUpdateNextGcRuntime(GarbageCollectorState &state, unsigned long candidate) { if (state.nextGcRunTime == 0 || candidate < state.nextGcRunTime) { state.nextGcRunTime = candidate; } } void Pool::checkWhetherProcessCanBeGarbageCollected(GarbageCollectorState &state, const GroupPtr &group, const ProcessPtr &process, ProcessList &output) { assert(maxIdleTime > 0); unsigned long long processGcTime = process->lastUsed + maxIdleTime; if (process->sessions == 0 && state.now >= processGcTime) { if (output.capacity() == 0) { output.reserve(group->enabledCount); } output.push_back(process); } else { maybeUpdateNextGcRuntime(state, processGcTime); } } void Pool::garbageCollectProcessesInGroup(GarbageCollectorState &state, const GroupPtr &group) { ProcessList &processes = group->enabledProcesses; ProcessList processesToGc; ProcessList::iterator p_it, p_end = processes.end(); for (p_it = processes.begin(); p_it != p_end; p_it++) { const ProcessPtr &process = *p_it; checkWhetherProcessCanBeGarbageCollected(state, group, process, processesToGc); } p_it = processesToGc.begin(); p_end = processesToGc.end(); while (p_it != p_end && (unsigned long) group->getProcessCount() > group->options.minProcesses) { ProcessPtr process = *p_it; P_DEBUG("Garbage collect idle process: " << process->inspect() << ", group=" << group->getName()); group->detach(process, state.actions); p_it++; } } void Pool::maybeCleanPreloader(GarbageCollectorState &state, const GroupPtr &group) { if (group->spawner->cleanable() && group->options.getMaxPreloaderIdleTime() != 0) { unsigned long long spawnerGcTime = group->spawner->lastUsed() + group->options.getMaxPreloaderIdleTime() * 1000000; if (state.now >= spawnerGcTime) { P_DEBUG("Garbage collect idle spawner: group=" << group->getName()); group->cleanupSpawner(state.actions); } else { maybeUpdateNextGcRuntime(state, spawnerGcTime); } } } unsigned long long Pool::realGarbageCollect() { TRACE_POINT(); ScopedLock lock(syncher); GroupMap::ConstIterator g_it(groups); GarbageCollectorState state; state.now = SystemTime::getUsec(); state.nextGcRunTime = 0; P_DEBUG("Garbage collection time..."); verifyInvariants(); // For all groups... while (*g_it != NULL) { const GroupPtr group = g_it.getValue(); if (maxIdleTime > 0) { // ...detach processes that have been idle for more than maxIdleTime. garbageCollectProcessesInGroup(state, group); } group->verifyInvariants(); // ...cleanup the spawner if it's been idle for more than preloaderIdleTime. maybeCleanPreloader(state, group); g_it.next(); } verifyInvariants(); lock.unlock(); // Schedule next garbage collection run. unsigned long long sleepTime; if (state.nextGcRunTime == 0 || state.nextGcRunTime <= state.now) { if (maxIdleTime == 0) { sleepTime = 10 * 60 * 1000000; } else { sleepTime = maxIdleTime; } } else { sleepTime = state.nextGcRunTime - state.now; } P_DEBUG("Garbage collection done; next garbage collect in " << std::fixed << std::setprecision(3) << (sleepTime / 1000000.0) << " sec"); UPDATE_TRACE_POINT(); runAllActions(state.actions); UPDATE_TRACE_POINT(); state.actions.clear(); return sleepTime; } void Pool::wakeupGarbageCollector() { garbageCollectionCond.notify_all(); } } // namespace ApplicationPool2 } // namespace Passenger ApplicationPool/Pool/AnalyticsCollection.cpp 0000644 00000013462 14756456557 0015257 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2011-2017 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include <Core/ApplicationPool/Pool.h> /************************************************************************* * * Analytics collection functions for ApplicationPool2::Pool * *************************************************************************/ namespace Passenger { namespace ApplicationPool2 { using namespace std; using namespace boost; void Pool::initializeAnalyticsCollection() { interruptableThreads.create_thread( boost::bind(collectAnalytics, shared_from_this()), "Pool analytics collector", POOL_HELPER_THREAD_STACK_SIZE ); } void Pool::collectAnalytics(PoolPtr self) { TRACE_POINT(); syscalls::usleep(3000000); while (!boost::this_thread::interruption_requested()) { try { UPDATE_TRACE_POINT(); self->realCollectAnalytics(); } catch (const thread_interrupted &) { break; } catch (const tracable_exception &e) { P_WARN("ERROR: " << e.what() << "\n Backtrace:\n" << e.backtrace()); } UPDATE_TRACE_POINT(); unsigned long long currentTime = SystemTime::getUsec(); unsigned long long sleepTime = timeToNextMultipleULL(5000000, currentTime); P_DEBUG("Analytics collection done; next analytics collection in " << std::fixed << std::setprecision(3) << (sleepTime / 1000000.0) << " sec"); try { syscalls::usleep(sleepTime); } catch (const thread_interrupted &) { break; } catch (const tracable_exception &e) { P_WARN("ERROR: " << e.what() << "\n Backtrace:\n" << e.backtrace()); } } } void Pool::collectPids(const ProcessList &processes, vector<pid_t> &pids) { foreach (const ProcessPtr &process, processes) { pids.push_back(process->getPid()); } } void Pool::updateProcessMetrics(const ProcessList &processes, const ProcessMetricMap &allMetrics, vector<ProcessPtr> &processesToDetach) { foreach (const ProcessPtr &process, processes) { ProcessMetricMap::const_iterator metrics_it = allMetrics.find(process->getPid()); if (metrics_it != allMetrics.end()) { process->metrics = metrics_it->second; // If the process is missing from 'allMetrics' then either 'ps' // failed or the process really is gone. We double check by sending // it a signal. } else if (!process->isDummy() && !process->osProcessExists()) { P_WARN("Process " << process->inspect() << " no longer exists! " "Detaching it from the pool."); processesToDetach.push_back(process); } } } void Pool::realCollectAnalytics() { TRACE_POINT(); boost::this_thread::disable_interruption di; boost::this_thread::disable_syscall_interruption dsi; vector<pid_t> pids; unsigned int max; P_DEBUG("Analytics collection time..."); // Collect all the PIDs. { UPDATE_TRACE_POINT(); LockGuard l(syncher); max = this->max; } pids.reserve(max); { UPDATE_TRACE_POINT(); LockGuard l(syncher); GroupMap::ConstIterator g_it(groups); while (*g_it != NULL) { const GroupPtr &group = g_it.getValue(); collectPids(group->enabledProcesses, pids); collectPids(group->disablingProcesses, pids); collectPids(group->disabledProcesses, pids); g_it.next(); } } // Collect process metrics and system and store them in the // data structures. ProcessMetricMap processMetrics; try { UPDATE_TRACE_POINT(); P_DEBUG("Collecting process metrics"); processMetrics = ProcessMetricsCollector().collect(pids); } catch (const ParseException &) { P_WARN("Unable to collect process metrics: cannot parse 'ps' output."); return; } try { UPDATE_TRACE_POINT(); P_DEBUG("Collecting system metrics"); systemMetricsCollector.collect(systemMetrics); } catch (const RuntimeException &e) { P_WARN("Unable to collect system metrics: " << e.what()); return; } { UPDATE_TRACE_POINT(); vector<ProcessPtr> processesToDetach; boost::container::vector<Callback> actions; ScopedLock l(syncher); GroupMap::ConstIterator g_it(groups); UPDATE_TRACE_POINT(); while (*g_it != NULL) { const GroupPtr &group = g_it.getValue(); updateProcessMetrics(group->enabledProcesses, processMetrics, processesToDetach); updateProcessMetrics(group->disablingProcesses, processMetrics, processesToDetach); updateProcessMetrics(group->disabledProcesses, processMetrics, processesToDetach); g_it.next(); } UPDATE_TRACE_POINT(); foreach (const ProcessPtr process, processesToDetach) { detachProcessUnlocked(process, actions); } UPDATE_TRACE_POINT(); processesToDetach.clear(); l.unlock(); UPDATE_TRACE_POINT(); runAllActions(actions); UPDATE_TRACE_POINT(); // Run destructors with updated trace point. actions.clear(); } } } // namespace ApplicationPool2 } // namespace Passenger ApplicationPool/Pool/ProcessUtils.cpp 0000644 00000022067 14756456557 0013754 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2011-2017 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include <Core/ApplicationPool/Pool.h> /************************************************************************* * * Process data structure utility functions for ApplicationPool2::Pool * *************************************************************************/ namespace Passenger { namespace ApplicationPool2 { using namespace std; using namespace boost; /**************************** * * Private methods * ****************************/ ProcessPtr Pool::findOldestIdleProcess(const Group *exclude) const { ProcessPtr oldestIdleProcess; GroupMap::ConstIterator g_it(groups); while (*g_it != NULL) { const GroupPtr &group = g_it.getValue(); if (group.get() == exclude) { g_it.next(); continue; } const ProcessList &processes = group->enabledProcesses; ProcessList::const_iterator p_it, p_end = processes.end(); for (p_it = processes.begin(); p_it != p_end; p_it++) { const ProcessPtr process = *p_it; if (process->busyness() == 0 && (oldestIdleProcess == NULL || process->lastUsed < oldestIdleProcess->lastUsed) ) { oldestIdleProcess = process; } } g_it.next(); } return oldestIdleProcess; } ProcessPtr Pool::findBestProcessToTrash() const { ProcessPtr oldestProcess; GroupMap::ConstIterator g_it(groups); while (*g_it != NULL) { const GroupPtr &group = g_it.getValue(); const ProcessList &processes = group->enabledProcesses; ProcessList::const_iterator p_it, p_end = processes.end(); for (p_it = processes.begin(); p_it != p_end; p_it++) { const ProcessPtr process = *p_it; if (oldestProcess == NULL || process->lastUsed < oldestProcess->lastUsed) { oldestProcess = process; } } g_it.next(); } return oldestProcess; } /** * Calls Group::detach() so be sure to fix up the invariants afterwards. * See the comments for Group::detach() and the code for detachProcessUnlocked(). */ ProcessPtr Pool::forceFreeCapacity(const Group *exclude, boost::container::vector<Callback> &postLockActions) { ProcessPtr process = findOldestIdleProcess(exclude); if (process != NULL) { P_DEBUG("Forcefully detaching process " << process->inspect() << " in order to free capacity in the pool"); Group *group = process->getGroup(); assert(group != NULL); assert(group->getWaitlist.empty()); group->detach(process, postLockActions); } return process; } bool Pool::detachProcessUnlocked(const ProcessPtr &process, boost::container::vector<Callback> &postLockActions) { if (OXT_LIKELY(process->isAlive())) { verifyInvariants(); Group *group = process->getGroup(); group->detach(process, postLockActions); // 'process' may now be a stale pointer so don't use it anymore. assignSessionsToGetWaiters(postLockActions); possiblySpawnMoreProcessesForExistingGroups(); group->verifyInvariants(); verifyInvariants(); verifyExpensiveInvariants(); return true; } else { return false; } } void Pool::syncDisableProcessCallback(const ProcessPtr &process, DisableResult result, boost::shared_ptr<DisableWaitTicket> ticket) { LockGuard l(ticket->syncher); ticket->done = true; ticket->result = result; ticket->cond.notify_one(); } void Pool::possiblySpawnMoreProcessesForExistingGroups() { /* Looks for Groups that are waiting for capacity to become available, * and spawn processes in those groups. */ GroupMap::ConstIterator g_it(groups); while (*g_it != NULL) { const GroupPtr &group = g_it.getValue(); if (group->isWaitingForCapacity()) { P_DEBUG("Group " << group->getName() << " is waiting for capacity"); group->spawn(); if (atFullCapacityUnlocked()) { return; } } g_it.next(); } /* Now look for Groups that haven't maximized their allowed capacity * yet, and spawn processes in those groups. */ g_it = GroupMap::ConstIterator(groups); while (*g_it != NULL) { const GroupPtr &group = g_it.getValue(); if (group->shouldSpawn()) { P_DEBUG("Group " << group->getName() << " requests more processes to be spawned"); group->spawn(); if (atFullCapacityUnlocked()) { return; } } g_it.next(); } } /**************************** * * Public methods * ****************************/ vector<ProcessPtr> Pool::getProcesses(bool lock) const { DynamicScopedLock l(syncher, lock); vector<ProcessPtr> result; GroupMap::ConstIterator g_it(groups); while (*g_it != NULL) { const GroupPtr &group = g_it.getValue(); ProcessList::const_iterator p_it; for (p_it = group->enabledProcesses.begin(); p_it != group->enabledProcesses.end(); p_it++) { result.push_back(*p_it); } for (p_it = group->disablingProcesses.begin(); p_it != group->disablingProcesses.end(); p_it++) { result.push_back(*p_it); } for (p_it = group->disabledProcesses.begin(); p_it != group->disabledProcesses.end(); p_it++) { result.push_back(*p_it); } g_it.next(); } return result; } ProcessPtr Pool::findProcessByGupid(const StaticString &gupid, bool lock) const { vector<ProcessPtr> processes = getProcesses(lock); vector<ProcessPtr>::const_iterator it, end = processes.end(); for (it = processes.begin(); it != end; it++) { const ProcessPtr &process = *it; if (process->getGupid() == gupid) { return process; } } return ProcessPtr(); } ProcessPtr Pool::findProcessByPid(pid_t pid, bool lock) const { vector<ProcessPtr> processes = getProcesses(lock); vector<ProcessPtr>::const_iterator it, end = processes.end(); for (it = processes.begin(); it != end; it++) { const ProcessPtr &process = *it; if (process->getPid() == pid) { return process; } } return ProcessPtr(); } bool Pool::detachProcess(const ProcessPtr &process) { ScopedLock l(syncher); boost::container::vector<Callback> actions; bool result = detachProcessUnlocked(process, actions); fullVerifyInvariants(); l.unlock(); runAllActions(actions); return result; } bool Pool::detachProcess(pid_t pid, const AuthenticationOptions &options) { ScopedLock l(syncher); ProcessPtr process = findProcessByPid(pid, false); if (process != NULL) { const Group *group = process->getGroup(); if (group->authorizeByUid(options.uid) || group->authorizeByApiKey(options.apiKey)) { boost::container::vector<Callback> actions; bool result = detachProcessUnlocked(process, actions); fullVerifyInvariants(); l.unlock(); runAllActions(actions); return result; } else { throw SecurityException("Operation unauthorized"); } } else { return false; } } bool Pool::detachProcess(const string &gupid, const AuthenticationOptions &options) { ScopedLock l(syncher); ProcessPtr process = findProcessByGupid(gupid, false); if (process != NULL) { const Group *group = process->getGroup(); if (group->authorizeByUid(options.uid) || group->authorizeByApiKey(options.apiKey)) { boost::container::vector<Callback> actions; bool result = detachProcessUnlocked(process, actions); fullVerifyInvariants(); l.unlock(); runAllActions(actions); return result; } else { throw SecurityException("Operation unauthorized"); } } else { return false; } } DisableResult Pool::disableProcess(const StaticString &gupid) { ScopedLock l(syncher); ProcessPtr process = findProcessByGupid(gupid, false); if (process != NULL) { Group *group = process->getGroup(); // Must be a boost::shared_ptr to be interruption-safe. boost::shared_ptr<DisableWaitTicket> ticket = boost::make_shared<DisableWaitTicket>(); DisableResult result = group->disable(process, boost::bind(syncDisableProcessCallback, boost::placeholders::_1, boost::placeholders::_2, ticket)); group->verifyInvariants(); group->verifyExpensiveInvariants(); if (result == DR_DEFERRED) { l.unlock(); ScopedLock l2(ticket->syncher); while (!ticket->done) { ticket->cond.wait(l2); } return ticket->result; } else { return result; } } else { return DR_NOOP; } } } // namespace ApplicationPool2 } // namespace Passenger ApplicationPool/Pool/StateInspection.cpp 0000644 00000027676 14756456557 0014444 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2011-2017 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include <Core/ApplicationPool/Pool.h> /************************************************************************* * * State inspection functions for ApplicationPool2::Pool * *************************************************************************/ namespace Passenger { namespace ApplicationPool2 { using namespace std; using namespace boost; /**************************** * * Private methods * ****************************/ unsigned int Pool::capacityUsedUnlocked() const { if (groups.size() == 1) { GroupPtr *group; groups.lookupRandom(NULL, &group); return (*group)->capacityUsed(); } else { GroupMap::ConstIterator g_it(groups); int result = 0; while (*g_it != NULL) { const GroupPtr &group = g_it.getValue(); result += group->capacityUsed(); g_it.next(); } return result; } } bool Pool::atFullCapacityUnlocked() const { return capacityUsedUnlocked() >= max; } void Pool::inspectProcessList(const InspectOptions &options, stringstream &result, const Group *group, const ProcessList &processes) const { ProcessList::const_iterator p_it; for (p_it = processes.begin(); p_it != processes.end(); p_it++) { const ProcessPtr &process = *p_it; char buf[128]; char cpubuf[10]; char membuf[10]; if (process->metrics.isValid()) { snprintf(cpubuf, sizeof(cpubuf), "%d%%", (int) process->metrics.cpu); snprintf(membuf, sizeof(membuf), "%ldM", (unsigned long) (process->metrics.realMemory() / 1024)); } else { snprintf(cpubuf, sizeof(cpubuf), "0%%"); snprintf(membuf, sizeof(membuf), "0M"); } snprintf(buf, sizeof(buf), " * PID: %-5lu Sessions: %-2u Processed: %-5u Uptime: %s\n" " CPU: %-5s Memory : %-5s Last used: %s ago", (unsigned long) process->getPid(), process->sessions, process->processed, process->uptime().c_str(), cpubuf, membuf, distanceOfTimeInWords(process->lastUsed / 1000000).c_str()); result << buf << endl; if (process->enabled == Process::DISABLING) { result << " Disabling..." << endl; } else if (process->enabled == Process::DISABLED) { result << " DISABLED" << endl; } else if (process->enabled == Process::DETACHED) { result << " Shutting down..." << endl; } const Socket *socket; if (options.verbose && (socket = process->getSockets().findFirstSocketWithProtocol("http")) != NULL) { result << " URL : http://" << replaceString(socket->address, "tcp://", "") << endl; result << " Password: " << group->getApiKey().toStaticString() << endl; } } } /**************************** * * Public methods * ****************************/ string Pool::inspect(const InspectOptions &options, bool lock) const { DynamicScopedLock l(syncher, lock); stringstream result; const char *headerColor = maybeColorize(options, ANSI_COLOR_YELLOW ANSI_COLOR_BLUE_BG ANSI_COLOR_BOLD); const char *resetColor = maybeColorize(options, ANSI_COLOR_RESET); if (!authorizeByUid(options.uid, false) && !authorizeByApiKey(options.apiKey, false)) { throw SecurityException("Operation unauthorized"); } result << headerColor << "----------- General information -----------" << resetColor << endl; result << "Max pool size : " << max << endl; result << "App groups : " << groups.size() << endl; result << "Processes : " << getProcessCount(false) << endl; result << "Requests in top-level queue : " << getWaitlist.size() << endl; if (options.verbose) { unsigned int i = 0; foreach (const GetWaiter &waiter, getWaitlist) { result << " " << i << ": " << waiter.options.getAppGroupName() << endl; i++; } } result << endl; result << headerColor << "----------- Application groups -----------" << resetColor << endl; GroupMap::ConstIterator g_it(groups); while (*g_it != NULL) { const GroupPtr &group = g_it.getValue(); if (!group->authorizeByUid(options.uid) && !group->authorizeByApiKey(options.apiKey)) { g_it.next(); continue; } ProcessList::const_iterator p_it; result << group->getName() << ":" << endl; result << " App root: " << group->options.appRoot << endl; if (group->restarting()) { result << " (restarting...)" << endl; } if (group->spawning()) { if (group->processesBeingSpawned == 0) { result << " (spawning...)" << endl; } else { result << " (spawning " << group->processesBeingSpawned << " new " << maybePluralize(group->processesBeingSpawned, "process", "processes") << "...)" << endl; } } result << " Requests in queue: " << group->getWaitlist.size() << endl; inspectProcessList(options, result, group.get(), group->enabledProcesses); inspectProcessList(options, result, group.get(), group->disablingProcesses); inspectProcessList(options, result, group.get(), group->disabledProcesses); inspectProcessList(options, result, group.get(), group->detachedProcesses); result << endl; g_it.next(); } return result.str(); } string Pool::toXml(const ToXmlOptions &options, bool lock) const { DynamicScopedLock l(syncher, lock); stringstream result; GroupMap::ConstIterator g_it(groups); ProcessList::const_iterator p_it; if (!authorizeByUid(options.uid, false) && !authorizeByApiKey(options.apiKey, false)) { throw SecurityException("Operation unauthorized"); } result << "<?xml version=\"1.0\" encoding=\"iso-8859-1\" ?>\n"; result << "<info version=\"3\">"; result << "<passenger_version>" << PASSENGER_VERSION << "</passenger_version>"; result << "<group_count>" << groups.size() << "</group_count>"; result << "<process_count>" << getProcessCount(false) << "</process_count>"; result << "<max>" << max << "</max>"; result << "<capacity_used>" << capacityUsedUnlocked() << "</capacity_used>"; result << "<get_wait_list_size>" << getWaitlist.size() << "</get_wait_list_size>"; if (options.secrets) { vector<GetWaiter>::const_iterator w_it, w_end = getWaitlist.end(); result << "<get_wait_list>"; for (w_it = getWaitlist.begin(); w_it != w_end; w_it++) { const GetWaiter &waiter = *w_it; result << "<item>"; result << "<app_group_name>" << escapeForXml(waiter.options.getAppGroupName()) << "</app_group_name>"; result << "</item>"; } result << "</get_wait_list>"; } result << "<supergroups>"; while (*g_it != NULL) { const GroupPtr &group = g_it.getValue(); if (!group->authorizeByUid(options.uid) && !group->authorizeByApiKey(options.apiKey)) { g_it.next(); continue; } result << "<supergroup>"; result << "<name>" << escapeForXml(group->getName()) << "</name>"; result << "<state>READY</state>"; result << "<get_wait_list_size>0</get_wait_list_size>"; result << "<capacity_used>" << group->capacityUsed() << "</capacity_used>"; if (options.secrets) { result << "<secret>" << escapeForXml(group->getApiKey().toStaticString()) << "</secret>"; } result << "<group default=\"true\">"; group->inspectXml(result, options.secrets); result << "</group>"; result << "</supergroup>"; g_it.next(); } result << "</supergroups>"; result << "</info>"; return result.str(); } Json::Value Pool::inspectPropertiesInAdminPanelFormat(const ToJsonOptions &options) const { ScopedLock l(syncher); Json::Value result(Json::objectValue); GroupMap::ConstIterator g_it(groups); ProcessList::const_iterator p_it; if (!authorizeByUid(options.uid, false) && !authorizeByApiKey(options.apiKey, false)) { throw SecurityException("Operation unauthorized"); } while (*g_it != NULL) { const GroupPtr &group = g_it.getValue(); if (options.hasApplicationIdsFilter) { const bool *tmp; if (!options.applicationIdsFilter.lookup(group->info.name, &tmp)) { g_it.next(); continue; } } if (!group->authorizeByUid(options.uid) && !group->authorizeByApiKey(options.apiKey)) { g_it.next(); continue; } Json::Value groupDoc(Json::objectValue); group->inspectPropertiesInAdminPanelFormat(groupDoc); result[group->info.name] = groupDoc; g_it.next(); } return result; } Json::Value Pool::inspectConfigInAdminPanelFormat(const ToJsonOptions &options) const { ScopedLock l(syncher); Json::Value result(Json::objectValue); GroupMap::ConstIterator g_it(groups); ProcessList::const_iterator p_it; if (!authorizeByUid(options.uid, false) && !authorizeByApiKey(options.apiKey, false)) { throw SecurityException("Operation unauthorized"); } while (*g_it != NULL) { const GroupPtr &group = g_it.getValue(); if (options.hasApplicationIdsFilter) { const bool *tmp; if (!options.applicationIdsFilter.lookup(group->info.name, &tmp)) { g_it.next(); continue; } } if (!group->authorizeByUid(options.uid) && !group->authorizeByApiKey(options.apiKey)) { g_it.next(); continue; } Json::Value groupDoc(Json::objectValue); group->inspectConfigInAdminPanelFormat(groupDoc); result[group->info.name] = groupDoc; g_it.next(); } return result; } Json::Value Pool::makeSingleValueJsonConfigFormat(const Json::Value &val, const Json::Value &defaultValue) { Json::Value ary(Json::arrayValue); if (val != defaultValue) { Json::Value entry; entry["value"] = val; entry["source"]["type"] = "ephemeral"; ary.append(entry); } if (!defaultValue.isNull()) { Json::Value entry; entry["value"] = defaultValue; entry["source"]["type"] = "default"; ary.append(entry); } return ary; } Json::Value Pool::makeSingleStrValueJsonConfigFormat(const StaticString &val) { return makeSingleValueJsonConfigFormat( Json::Value(val.data(), val.data() + val.size())); } Json::Value Pool::makeSingleStrValueJsonConfigFormat(const StaticString &val, const StaticString &defaultValue) { return makeSingleValueJsonConfigFormat( Json::Value(val.data(), val.data() + val.size()), Json::Value(defaultValue.data(), defaultValue.data() + defaultValue.size())); } Json::Value Pool::makeSingleNonEmptyStrValueJsonConfigFormat(const StaticString &val) { if (val.empty()) { return Json::arrayValue; } else { return makeSingleStrValueJsonConfigFormat(val); } } unsigned int Pool::capacityUsed() const { LockGuard l(syncher); return capacityUsedUnlocked(); } bool Pool::atFullCapacity() const { LockGuard l(syncher); return atFullCapacityUnlocked(); } /** * Returns the total number of processes in the pool, including all disabling and * disabled processes, but excluding processes that are shutting down and excluding * processes that are being spawned. */ unsigned int Pool::getProcessCount(bool lock) const { DynamicScopedLock l(syncher, lock); unsigned int result = 0; GroupMap::ConstIterator g_it(groups); while (*g_it != NULL) { const GroupPtr &group = g_it.getValue(); result += group->getProcessCount(); g_it.next(); } return result; } unsigned int Pool::getGroupCount() const { LockGuard l(syncher); return groups.size(); } } // namespace ApplicationPool2 } // namespace Passenger ApplicationPool/Pool/Miscellaneous.cpp 0000644 00000016424 14756456557 0014120 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2011-2017 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include <Core/ApplicationPool/Pool.h> /************************************************************************* * * Miscellaneous functions for ApplicationPool2::Pool * *************************************************************************/ namespace Passenger { namespace ApplicationPool2 { using namespace std; using namespace boost; // 'lockNow == false' may only be used during unit tests. Normally we // should never call the callback while holding the lock. void Pool::asyncGet(const Options &options, const GetCallback &callback, bool lockNow) { DynamicScopedLock lock(syncher, lockNow); assert(lifeStatus == ALIVE || lifeStatus == PREPARED_FOR_SHUTDOWN); verifyInvariants(); P_TRACE(2, "asyncGet(appGroupName=" << options.getAppGroupName() << ")"); boost::container::vector<Callback> actions; Group *existingGroup = findMatchingGroup(options); if (OXT_LIKELY(existingGroup != NULL)) { /* Best case: the app group is already in the pool. Let's use it. */ P_TRACE(2, "Found existing Group"); existingGroup->verifyInvariants(); SessionPtr session = existingGroup->get(options, callback, actions); existingGroup->verifyInvariants(); verifyInvariants(); P_TRACE(2, "asyncGet() finished"); if (lockNow) { lock.unlock(); } if (session != NULL) { callback(session, ExceptionPtr()); } } else if (!atFullCapacityUnlocked()) { /* The app super group isn't in the pool and we have enough free * resources to make a new one. */ P_DEBUG("Spawning new Group"); GroupPtr group = createGroupAndAsyncGetFromIt(options, callback, actions); group->verifyInvariants(); verifyInvariants(); P_DEBUG("asyncGet() finished"); } else { /* Uh oh, the app super group isn't in the pool but we don't * have the resources to make a new one. The sysadmin should * configure the system to let something like this happen * as least as possible, but let's try to handle it as well * as we can. */ ProcessPtr freedProcess = forceFreeCapacity(NULL, actions); if (freedProcess == NULL) { /* No process is eligible for killing. This could happen if, for example, * all (super)groups are currently initializing/restarting/spawning/etc. * We have no choice but to satisfy this get() action later when resources * become available. */ P_DEBUG("Could not free a process; putting request to top-level getWaitlist"); getWaitlist.push_back(GetWaiter( options.copyAndPersist(), callback)); } else { /* Now that a process has been trashed we can create * the missing Group. */ P_DEBUG("Creating new Group"); GroupPtr group = createGroup(options); SessionPtr session = group->get(options, callback, actions); /* The Group is now spawning a process so the callback * should now have been put on the wait list, * unless something has changed and we forgot to update * some code here or if options.noop... */ if (session != NULL) { assert(options.noop); actions.push_back(boost::bind(GetCallback::call, callback, session, ExceptionPtr())); } freedProcess->getGroup()->verifyInvariants(); group->verifyInvariants(); } assert(atFullCapacityUnlocked()); verifyInvariants(); verifyExpensiveInvariants(); P_TRACE(2, "asyncGet() finished"); } if (!actions.empty()) { if (lockNow) { if (lock.owns_lock()) { lock.unlock(); } runAllActions(actions); } else { // This state is not allowed. If we reach // here then it probably indicates a bug in // the test suite. abort(); } } } // TODO: 'ticket' should be a boost::shared_ptr for interruption-safety. SessionPtr Pool::get(const Options &options, Ticket *ticket) { ticket->session.reset(); ticket->exception.reset(); GetCallback callback; callback.func = syncGetCallback; callback.userData = ticket; asyncGet(options, callback); ScopedLock lock(ticket->syncher); while (ticket->session == NULL && ticket->exception == NULL) { ticket->cond.wait(lock); } lock.unlock(); if (OXT_LIKELY(ticket->session != NULL)) { SessionPtr session = ticket->session; ticket->session.reset(); return session; } else { rethrowException(ticket->exception); return SessionPtr(); // Shut up compiler warning. } } void Pool::setMax(unsigned int max) { ScopedLock l(syncher); assert(max > 0); fullVerifyInvariants(); bool bigger = max > this->max; this->max = max; if (bigger) { /* If there are clients waiting for resources * to become free, spawn more processes now that * we have the capacity. * * We favor waiters on the pool over waiters on the * the groups because the latter already have the * resources to eventually complete. Favoring waiters * on the pool should be fairer. */ boost::container::vector<Callback> actions; assignSessionsToGetWaiters(actions); possiblySpawnMoreProcessesForExistingGroups(); fullVerifyInvariants(); l.unlock(); runAllActions(actions); } else { fullVerifyInvariants(); } } void Pool::setMaxIdleTime(unsigned long long value) { LockGuard l(syncher); maxIdleTime = value; wakeupGarbageCollector(); } void Pool::enableSelfChecking(bool enabled) { LockGuard l(syncher); selfchecking = enabled; } /** * Checks whether at least one process is being spawned. */ bool Pool::isSpawning(bool lock) const { DynamicScopedLock l(syncher, lock); GroupMap::ConstIterator g_it(groups); while (*g_it != NULL) { const GroupPtr &group = g_it.getValue(); if (group->spawning()) { return true; } g_it.next(); } return false; } bool Pool::authorizeByApiKey(const ApiKey &key, bool lock) const { return key.isSuper() || findGroupByApiKey(key.toStaticString(), lock) != NULL; } bool Pool::authorizeByUid(uid_t uid, bool lock) const { if (uid == 0 || uid == geteuid()) { return true; } DynamicScopedLock l(syncher, lock); GroupMap::ConstIterator g_it(groups); while (*g_it != NULL) { const GroupPtr &group = g_it.getValue(); if (group->authorizeByUid(uid)) { return true; } g_it.next(); } return false; } } // namespace ApplicationPool2 } // namespace Passenger ApplicationPool/Group.h 0000644 00000043324 14756456557 0011144 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2011-2018 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef _PASSENGER_APPLICATION_POOL2_GROUP_H_ #define _PASSENGER_APPLICATION_POOL2_GROUP_H_ #include <string> #include <map> #include <queue> #include <deque> #include <boost/thread.hpp> #include <boost/bind/bind.hpp> #include <boost/foreach.hpp> #include <boost/shared_ptr.hpp> #include <boost/make_shared.hpp> #include <boost/container/vector.hpp> #include <boost/container/small_vector.hpp> #include <boost/atomic.hpp> #include <oxt/macros.hpp> #include <oxt/thread.hpp> #include <oxt/dynamic_thread_group.hpp> #include <sys/types.h> #include <sys/stat.h> #include <cstdlib> #include <cassert> #include <MemoryKit/palloc.h> #include <WrapperRegistry/Registry.h> #include <Hooks.h> #include <Utils.h> #include <Core/ApplicationPool/Common.h> #include <Core/ApplicationPool/Context.h> #include <Core/ApplicationPool/BasicGroupInfo.h> #include <Core/ApplicationPool/Process.h> #include <Core/ApplicationPool/Options.h> #include <Core/SpawningKit/Factory.h> #include <Core/SpawningKit/Result.h> #include <Core/SpawningKit/UserSwitchingRules.h> #include <Shared/ApplicationPoolApiKey.h> namespace Passenger { namespace ApplicationPool2 { using namespace std; using namespace boost; using namespace oxt; /** * Except for otherwise documented parts, this class is not thread-safe, * so only access within ApplicationPool lock. */ class Group: public boost::enable_shared_from_this<Group> { // Actually private, but marked public so that unit tests can access the fields. public: friend class Pool; struct GetAction { GetCallback callback; SessionPtr session; }; struct DisableWaiter { ProcessPtr process; DisableCallback callback; DisableWaiter(const ProcessPtr &_process, const DisableCallback &_callback) : process(_process), callback(_callback) { } }; struct RouteResult { Process *process; bool finished; RouteResult(Process *p, bool _finished = false) : process(p), finished(_finished) { } }; enum LifeStatus { /** Up and operational. */ ALIVE, /** Being shut down. The containing Pool has issued the shutdown() * command, and this Group is now waiting for all detached processes to * exit. You cannot call `get()`, `restart()` and other mutating methods * anymore, and all threads created by this Group will exit as soon * as possible. */ SHUTTING_DOWN, /** * Shut down complete. Object no longer usable. No Processes are referenced * from this Group anymore. */ SHUT_DOWN }; BasicGroupInfo info; /** * A back reference to the containing Pool. Should never * be NULL because a Pool should outlive all its containing * Groups. * Read-only; only set during initialization. */ Pool *pool; time_t lastRestartFileMtime; time_t lastRestartFileCheckTime; /** Number of times a restart has been initiated so far. This is incremented immediately * in Group::restart(), and is used to abort the restarter thread that was active at the * time the restart was initiated. It's safe for the value to wrap around. */ unsigned int restartsInitiated; /** * The number of processes that are being spawned right now. * * Invariant: * if processesBeingSpawned > 0: m_spawning */ short processesBeingSpawned; /** * A Group object progresses through a life. * * You should not access this directly. You should use `isAlive()`/`getLifeStatus()`. * * Invariant: * if lifeStatus != ALIVE: * enabledCount == 0 * disablingCount == 0 * disabledCount == 0 * nEnabledProcessesTotallyBusy == 0 */ boost::atomic<boost::uint8_t> lifeStatus; /** * Whether the spawner thread is currently working. Note that even * if it's working, it doesn't necessarily mean that processes are * being spawned (i.e. that processesBeingSpawned > 0). After the * thread is done spawning a process, it will attempt to attach * the newly-spawned process to the group. During that time it's not * technically spawning anything. */ bool m_spawning: 1; /** Whether a non-rolling restart is in progress (i.e. whether spawnThreadRealMain() * is at work). While it is in progress, it is not possible to signal the desire to * spawn new process. If spawning was already in progress when the restart was initiated, * then the spawning will abort as soon as possible. * * When rolling restarting is in progress, this flag is false. * * Invariant: * if m_restarting: processesBeingSpawned == 0 */ bool m_restarting: 1; bool alwaysRestartFileExists: 1; /** Contains the spawn loop thread and the restarter thread. */ dynamic_thread_group interruptableThreads; string restartFile; string alwaysRestartFile; ProcessPtr nullProcess; /** This timer scans `detachedProcesses` periodically to see * whether any of the Processes can be shut down. */ bool detachedProcessesCheckerActive; boost::condition_variable detachedProcessesCheckerCond; Callback shutdownCallback; GroupPtr selfPointer; /****** Initialization and shutdown ******/ static ApiKey generateApiKey(const Pool *pool); static string generateUuid(const Pool *pool); bool shutdownCanFinish() const; void finishShutdown(boost::container::vector<Callback> &postLockActions); /****** Session management ******/ RouteResult route(const Options &options) const; SessionPtr newSession(Process *process, unsigned long long now = 0); static void _onSessionInitiateFailure(Session *session); static void _onSessionClose(Session *session); OXT_FORCE_INLINE void onSessionInitiateFailure(Process *process, Session *session); OXT_FORCE_INLINE void onSessionClose(Process *process, Session *session); /****** Spawning and restarting ******/ void spawnThreadMain(GroupPtr self, SpawningKit::SpawnerPtr spawner, Options options, unsigned int restartsInitiated); void spawnThreadRealMain(const SpawningKit::SpawnerPtr &spawner, const Options &options, unsigned int restartsInitiated); void finalizeRestart(GroupPtr self, Options oldOptions, Options newOptions, RestartMethod method, SpawningKit::FactoryPtr spawningKitFactory, unsigned int restartsInitiated, boost::container::vector<Callback> postLockActions); /****** Process list management ******/ Process *findProcessWithStickySessionId(unsigned int id) const; Process *findProcessWithStickySessionIdOrLowestBusyness(unsigned int id) const; Process *findProcessWithLowestBusyness(const ProcessList &processes) const; Process *findEnabledProcessWithLowestBusyness() const; void addProcessToList(const ProcessPtr &process, ProcessList &destination); void removeProcessFromList(const ProcessPtr &process, ProcessList &source); void removeFromDisableWaitlist(const ProcessPtr &p, DisableResult result, boost::container::vector<Callback> &postLockActions); void clearDisableWaitlist(DisableResult result, boost::container::vector<Callback> &postLockActions); void enableAllDisablingProcesses(boost::container::vector<Callback> &postLockActions); void startCheckingDetachedProcesses(bool immediately); void detachedProcessesCheckerMain(GroupPtr self); /****** Out-of-band work ******/ bool oobwAllowed() const; bool shouldInitiateOobw(Process *process) const; void maybeInitiateOobw(Process *process); void lockAndMaybeInitiateOobw(const ProcessPtr &process, DisableResult result, GroupPtr self); void initiateOobw(const ProcessPtr &process); void spawnThreadOOBWRequest(GroupPtr self, ProcessPtr process); void initiateNextOobwRequest(); /****** Internal utilities ******/ static void runAllActions(const boost::container::vector<Callback> &actions); static void interruptAndJoinAllThreads(GroupPtr self); static void doCleanupSpawner(SpawningKit::SpawnerPtr spawner); void resetOptions(const Options &newOptions, Options *destination = NULL); void mergeOptions(const Options &other); bool prepareHookScriptOptions(HookScriptOptions &hsOptions, const char *name); void runAttachHooks(const ProcessPtr process) const; void runDetachHooks(const ProcessPtr process) const; void setupAttachOrDetachHook(const ProcessPtr process, HookScriptOptions &options) const; unsigned int generateStickySessionId(); ProcessPtr createNullProcessObject(); ProcessPtr createProcessObject(const SpawningKit::Spawner &spawner, const SpawningKit::Result &spawnResult); bool poolAtFullCapacity() const; ProcessPtr poolForceFreeCapacity(const Group *exclude, boost::container::vector<Callback> &postLockActions); void wakeUpGarbageCollector(); bool anotherGroupIsWaitingForCapacity() const; Group *findOtherGroupWaitingForCapacity() const; bool pushGetWaiter(const Options &newOptions, const GetCallback &callback, boost::container::vector<Callback> &postLockActions); template<typename Lock> void assignSessionsToGetWaitersQuickly(Lock &lock); void assignSessionsToGetWaiters(boost::container::vector<Callback> &postLockActions); bool testOverflowRequestQueue() const; void callAbortLongRunningConnectionsCallback(const ProcessPtr &process); /****** Correctness verification ******/ bool selfCheckingEnabled() const; void verifyInvariants() const; void verifyExpensiveInvariants() const; #ifndef NDEBUG bool verifyNoRequestsOnGetWaitlistAreRoutable() const; #endif public: Options options; /** A UUID that's generated on Group initialization, and changes every time * the Group receives a restart command. Allows Union Station to track app * restarts. This information is public. */ string uuid; /** * Processes are categorized as enabled, disabling or disabled. * * - get() requests should go to enabled processes. * - Disabling processes are allowed to finish their current requests, * but they generally will not receive any new requests. The only * exception is when there are no enabled processes. In this case, * a new process will be spawned while in the mean time all requests * go to one of the disabling processes. Disabling processes become * disabled as soon as they finish all their requests and there are * enabled processes. * - Disabled processes never handle requests. * * 'enabledProcesses', 'disablingProcesses' and 'disabledProcesses' contain * all enabled, disabling and disabling processes in this group, respectively. * 'enabledCount', 'disablingCount' and 'disabledCount' are used to maintain * their numbers. * These lists do not intersect. A process is in exactly 1 list. * * `nEnabledProcessesTotallyBusy` counts the number of enabled processes for which * `isTotallyBusy()` is true. * * Invariants: * enabledCount >= 0 * disablingCount >= 0 * disabledCount >= 0 * enabledProcesses.size() == enabledCount * disablingProcesses.size() == disabingCount * disabledProcesses.size() == disabledCount * nEnabledProcessesTotallyBusy <= enabledCount * * if (enabledCount == 0): * processesBeingSpawned > 0 || restarting() || poolAtFullCapacity() * if (enabledCount == 0) and (disablingCount > 0): * processesBeingSpawned > 0 * if !m_spawning: * (enabledCount > 0) || (disablingCount == 0) * * for all process in enabledProcesses: * process.enabled == Process::ENABLED * process.isAlive() * process.oobwStatus == Process::OOBW_NOT_ACTIVE || process.oobwStatus == Process::OOBW_REQUESTED * for all processes in disablingProcesses: * process.enabled == Process::DISABLING * process.isAlive() * process.oobwStatus == Process::OOBW_NOT_ACTIVE || process.oobwStatus == Process::OOBW_IN_PROGRESS * for all process in disabledProcesses: * process.enabled == Process::DISABLED * process.isAlive() * process.oobwStatus == Process::OOBW_NOT_ACTIVE || process.oobwStatus == Process::OOBW_IN_PROGRESS */ int enabledCount; int disablingCount; int disabledCount; int nEnabledProcessesTotallyBusy; ProcessList enabledProcesses; ProcessList disablingProcesses; ProcessList disabledProcesses; /** * When a process is detached, it is stored here until we've confirmed * that the OS process has exited. * * for all process in detachedProcesses: * process.enabled == Process::DETACHED */ ProcessList detachedProcesses; /** * A cache of the processes' busyness. It's in a compact structure * so that `findProcessWithLowestBusyness()` can work very quickly * when there are a large number of processes. */ boost::container::vector<int> enabledProcessBusynessLevels; /** * get() requests for this group that cannot be immediately satisfied are * put on this wait list, which must be processed as soon as the necessary * resources have become free. * * ### Invariant 1 (safety) * * If requests are queued in the getWaitlist, then that's because there are * no processes that can serve them. * * if getWaitlist is non-empty: * enabledProcesses.empty() || (no request in getWaitlist is routeable) * * Here, "routeable" is defined as `route(options).process != NULL`. * * ### Invariant 2 (progress) * * The only reason why there are no enabled processes, while at the same time we're * not spawning or waiting for pool capacity, is because there is nothing to do. * * if enabledProcesses.empty() && !m_spawning && !restarting() && !poolAtFullCapacity(): * getWaitlist is empty * * Equivalently: * If requests are queued in the getWaitlist, then either we have processes that can process * them (some time in the future), or we're actively trying to spawn processes, unless we're * unable to do that because of resource limits. * * if getWaitlist is non-empty: * !enabledProcesses.empty() || m_spawning || restarting() || poolAtFullCapacity() */ deque<GetWaiter> getWaitlist; /** * Disable() commands that couldn't finish immediately will put their callbacks * in this queue. Note that there may be multiple DisableWaiters pointing to the * same Process. * * Invariant: * disableWaitlist.size() >= disablingCount */ deque<DisableWaiter> disableWaitlist; /** * Invariant: * (lifeStatus == ALIVE) == (spawner != NULL) */ SpawningKit::SpawnerPtr spawner; /****** Initialization and shutdown ******/ Group(Pool *pool, const Options &options); ~Group(); bool initialize(); void shutdown(const Callback &callback, boost::container::vector<Callback> &postLockActions); /****** Life time, basic info, backreferences and related objects ******/ bool isAlive() const; OXT_FORCE_INLINE LifeStatus getLifeStatus() const; StaticString getName() const; const BasicGroupInfo &getInfo(); const ApiKey &getApiKey() const; OXT_FORCE_INLINE Pool *getPool() const; Context *getContext() const; psg_pool_t *getPallocPool() const; const ResourceLocator &getResourceLocator() const; const WrapperRegistry::Registry &getWrapperRegistry() const; /****** Session management ******/ SessionPtr get(const Options &newOptions, const GetCallback &callback, boost::container::vector<Callback> &postLockActions); /****** Spawning and restarting ******/ void restart(const Options &options, RestartMethod method = RM_DEFAULT); bool restarting() const; bool needsRestart(const Options &options); SpawnResult spawn(); bool spawning() const; bool shouldSpawn() const; bool shouldSpawnForGetAction() const; bool allowSpawn() const; /****** Process list management ******/ AttachResult attach(const ProcessPtr &process, boost::container::vector<Callback> &postLockActions); void detach(const ProcessPtr &process, boost::container::vector<Callback> &postLockActions); void detachAll(boost::container::vector<Callback> &postLockActions); void enable(const ProcessPtr &process, boost::container::vector<Callback> &postLockActions); DisableResult disable(const ProcessPtr &process, const DisableCallback &callback); /****** State inspection ******/ unsigned int getProcessCount() const; bool processLowerLimitsSatisfied() const; bool processUpperLimitsReached() const; bool allEnabledProcessesAreTotallyBusy() const; unsigned int capacityUsed() const; bool isWaitingForCapacity() const; bool garbageCollectable(unsigned long long now = 0) const; void inspectXml(std::ostream &stream, bool includeSecrets = true) const; void inspectPropertiesInAdminPanelFormat(Json::Value &result) const; void inspectConfigInAdminPanelFormat(Json::Value &result) const; /****** Out-of-band work ******/ void requestOOBW(const ProcessPtr &process); /****** Miscellaneous ******/ void cleanupSpawner(boost::container::vector<Callback> &postLockActions); bool authorizeByUid(uid_t uid) const; bool authorizeByApiKey(const ApiKey &key) const; }; } // namespace ApplicationPool2 } // namespace Passenger #endif /* _PASSENGER_APPLICATION_POOL2_GROUP_H_ */ ApplicationPool/Pool.h 0000644 00000040407 14756456557 0010760 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2011-2018 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef _PASSENGER_APPLICATION_POOL2_POOL_H_ #define _PASSENGER_APPLICATION_POOL2_POOL_H_ #include <string> #include <vector> #include <algorithm> #include <utility> #include <sstream> #include <iomanip> #include <boost/thread.hpp> #include <boost/bind/bind.hpp> #include <boost/shared_ptr.hpp> #include <boost/make_shared.hpp> #include <boost/function.hpp> #include <boost/foreach.hpp> #include <boost/pool/object_pool.hpp> // We use boost::container::vector instead of std::vector, because the // former does not allocate memory in its default constructor. This is // useful for post lock action vectors which often remain empty. #include <boost/container/vector.hpp> #include <boost/date_time/posix_time/posix_time_types.hpp> #include <oxt/dynamic_thread_group.hpp> #include <oxt/backtrace.hpp> #include <sys/types.h> #include <MemoryKit/palloc.h> #include <LoggingKit/LoggingKit.h> #include <ConfigKit/ConfigKit.h> #include <Exceptions.h> #include <Hooks.h> #include <SystemTools/SystemMetricsCollector.h> #include <SystemTools/ProcessMetricsCollector.h> #include <SystemTools/SystemTime.h> #include <Utils/Lock.h> #include <Utils/AnsiColorConstants.h> #include <Utils/MessagePassing.h> #include <Utils/VariantMap.h> #include <Core/ApplicationPool/Common.h> #include <Core/ApplicationPool/Context.h> #include <Core/ApplicationPool/Process.h> #include <Core/ApplicationPool/Group.h> #include <Core/ApplicationPool/Session.h> #include <Core/ApplicationPool/Options.h> #include <Core/SpawningKit/Factory.h> #include <Shared/ApplicationPoolApiKey.h> namespace Passenger { namespace ApplicationPool2 { using namespace std; using namespace boost; using namespace oxt; class Pool: public boost::enable_shared_from_this<Pool> { public: struct AuthenticationOptions { uid_t uid; ApiKey apiKey; AuthenticationOptions() : uid(-1) { } static AuthenticationOptions makeAuthorized() { AuthenticationOptions options; options.apiKey = ApiKey::makeSuper(); return options; } }; /****** Group data structure utilities ******/ struct RestartOptions: public AuthenticationOptions { RestartMethod method; RestartOptions() : method(RM_DEFAULT) { } static RestartOptions makeAuthorized() { RestartOptions options; options.apiKey = ApiKey::makeSuper(); return options; } }; /****** State inspection ******/ struct InspectOptions: public AuthenticationOptions { bool colorize; bool verbose; InspectOptions() : colorize(false), verbose(false) { } InspectOptions(const VariantMap &options) : colorize(options.getBool("colorize", false, false)), verbose(options.getBool("verbose", false, false)) { } InspectOptions(const Json::Value &options) : colorize(options.get("colorize", false).asBool()), verbose(options.get("verbose", false).asBool()) { } static InspectOptions makeAuthorized() { InspectOptions options; options.apiKey = ApiKey::makeSuper(); return options; } }; struct ToXmlOptions: public AuthenticationOptions { bool secrets; ToXmlOptions() : secrets(true) { } ToXmlOptions(const VariantMap &options) : secrets(options.getBool("secrets", false, false)) { } static ToXmlOptions makeAuthorized() { ToXmlOptions options; options.apiKey = ApiKey::makeSuper(); return options; } }; struct ToJsonOptions: public AuthenticationOptions { bool secrets; bool hasApplicationIdsFilter; StringKeyTable<bool> applicationIdsFilter; ToJsonOptions() : secrets(false), hasApplicationIdsFilter(false), applicationIdsFilter(0, 0) { } ToJsonOptions(const VariantMap &options) : secrets(options.getBool("secrets", false, false)), hasApplicationIdsFilter(false), applicationIdsFilter(0, 0) { } void set(const Json::Value &_options) { ConfigKit::Schema schema = createSchema(); ConfigKit::Store options(schema, _options); if (!options["application_ids"].isNull()) { hasApplicationIdsFilter = true; applicationIdsFilter = StringKeyTable<bool>(); const Json::Value subdoc = options["application_ids"]; Json::Value::const_iterator it, end = subdoc.end(); for (it = subdoc.begin(); it != end; it++) { applicationIdsFilter.insert(it->asString(), true); } } } static ConfigKit::Schema createSchema() { using namespace ConfigKit; ConfigKit::Schema schema; schema.add("application_ids", STRING_ARRAY_TYPE, OPTIONAL); schema.finalize(); return schema; } static ToJsonOptions makeAuthorized() { ToJsonOptions options; options.apiKey = ApiKey::makeSuper(); return options; } }; // Actually private, but marked public so that unit tests can access the fields. public: friend class Group; friend class Process; friend struct tut::ApplicationPool2_PoolTest; mutable boost::mutex syncher; unsigned int max; unsigned long long maxIdleTime; bool selfchecking; Context *context; /** * Code can register background threads in one of these dynamic thread groups * to ensure that threads are interrupted and/or joined properly upon Pool * destruction. * All threads in 'interruptableThreads' will be interrupted and joined upon * Pool destruction. * All threads in 'nonInterruptableThreads' will be joined, but not interrupted, * upon Pool destruction. */ dynamic_thread_group interruptableThreads; dynamic_thread_group nonInterruptableThreads; enum LifeStatus { ALIVE, PREPARED_FOR_SHUTDOWN, SHUTTING_DOWN, SHUT_DOWN } lifeStatus; mutable GroupMap groups; psg_pool_t *palloc; /** * get() requests that... * - cannot be immediately satisfied because the pool is at full * capacity and no existing processes can be killed, * - and for which the super group isn't in the pool, * ...are put on this wait list. * * This wait list is processed when one of the following things happen: * * - A process has been spawned but its associated group has * no get waiters. This process can be killed and the resulting * free capacity will be used to spawn a process for this * get request. * - A process (that has apparently been spawned after getWaitlist * was populated) is done processing a request. This process can * then be killed to free capacity. * - A process has failed to spawn, resulting in capacity to * become free. * - A Group failed to initialize, resulting in free capacity. * - Someone commanded Pool to detach a process, resulting in free * capacity. * - Someone commanded Pool to detach a Group, resulting in * free capacity. * - The 'max' option has been increased, resulting in free capacity. * * Invariant 1: * for all options in getWaitlist: * options.getAppGroupName() is not in 'groups'. * * Invariant 2: * if getWaitlist is non-empty: * atFullCapacity() * Equivalently: * if !atFullCapacity(): * getWaitlist is empty. */ vector<GetWaiter> getWaitlist; // Actually private, but marked public so that unit tests can access the fields. public: /****** Debugging support *******/ struct DebugSupport { /** Mailbox for the unit tests to receive messages on. */ MessageBoxPtr debugger; /** Mailbox for the ApplicationPool code to receive messages on. */ MessageBoxPtr messages; // Choose aspects to debug. bool restarting; bool spawning; bool oobw; bool testOverflowRequestQueue; bool detachedProcessesChecker; // The following fields may only be accessed by Pool. boost::mutex syncher; unsigned int spawnLoopIteration; DebugSupport() { debugger = boost::make_shared<MessageBox>(); messages = boost::make_shared<MessageBox>(); restarting = true; spawning = true; oobw = false; detachedProcessesChecker = false; testOverflowRequestQueue = false; spawnLoopIteration = 0; } }; typedef boost::shared_ptr<DebugSupport> DebugSupportPtr; DebugSupportPtr debugSupport; /****** Analytics collection ******/ SystemMetricsCollector systemMetricsCollector; SystemMetrics systemMetrics; void initializeAnalyticsCollection(); static void collectAnalytics(PoolPtr self); static void collectPids(const ProcessList &processes, vector<pid_t> &pids); static void updateProcessMetrics(const ProcessList &processes, const ProcessMetricMap &allMetrics, vector<ProcessPtr> &processesToDetach); void realCollectAnalytics(); /****** Garbage collection ******/ struct GarbageCollectorState { unsigned long long now; unsigned long long nextGcRunTime; boost::container::vector<Callback> actions; }; boost::condition_variable garbageCollectionCond; void initializeGarbageCollection(); static void garbageCollect(PoolPtr self); void maybeUpdateNextGcRuntime(GarbageCollectorState &state, unsigned long candidate); void checkWhetherProcessCanBeGarbageCollected(GarbageCollectorState &state, const GroupPtr &group, const ProcessPtr &process, ProcessList &output); void garbageCollectProcessesInGroup(GarbageCollectorState &state, const GroupPtr &group); void maybeCleanPreloader(GarbageCollectorState &state, const GroupPtr &group); unsigned long long realGarbageCollect(); void wakeupGarbageCollector(); /****** General utilities ******/ static const char *maybeColorize(const InspectOptions &options, const char *color); static const char *maybePluralize(unsigned int count, const char *singular, const char *plural); static void runAllActions(const boost::container::vector<Callback> &actions); static void runAllActionsWithCopy(boost::container::vector<Callback> actions); bool runHookScripts(const char *name, const boost::function<void (HookScriptOptions &)> &setup) const; void verifyInvariants() const; void verifyExpensiveInvariants() const; void fullVerifyInvariants() const; void assignSessionsToGetWaiters(boost::container::vector<Callback> &postLockActions); template<typename Queue> static void assignExceptionToGetWaiters(Queue &getWaitlist, const ExceptionPtr &exception, boost::container::vector<Callback> &postLockActions); static void syncGetCallback(const AbstractSessionPtr &session, const ExceptionPtr &e, void *userData); /****** Group data structure utilities ******/ struct DetachGroupWaitTicket { boost::mutex syncher; boost::condition_variable cond; bool done; DetachGroupWaitTicket() { done = false; } }; const GroupPtr getGroup(const char *name); const pair<uid_t, gid_t> getGroupRunUidAndGids(const StaticString &appGroupName); Group *findMatchingGroup(const Options &options); GroupPtr createGroup(const Options &options); GroupPtr createGroupAndAsyncGetFromIt(const Options &options, const GetCallback &callback, boost::container::vector<Callback> &postLockActions); void forceDetachGroup(const GroupPtr &group, const Callback &callback, boost::container::vector<Callback> &postLockActions); static void syncDetachGroupCallback(boost::shared_ptr<DetachGroupWaitTicket> ticket); static void waitDetachGroupCallback(boost::shared_ptr<DetachGroupWaitTicket> ticket); /****** Process data structure utilities ******/ struct DisableWaitTicket { boost::mutex syncher; boost::condition_variable cond; DisableResult result; bool done; DisableWaitTicket() { done = false; } }; ProcessPtr findOldestIdleProcess(const Group *exclude = NULL) const; ProcessPtr findBestProcessToTrash() const; ProcessPtr forceFreeCapacity(const Group *exclude, boost::container::vector<Callback> &postLockActions); bool detachProcessUnlocked(const ProcessPtr &process, boost::container::vector<Callback> &postLockActions); static void syncDisableProcessCallback(const ProcessPtr &process, DisableResult result, boost::shared_ptr<DisableWaitTicket> ticket); void possiblySpawnMoreProcessesForExistingGroups(); /****** State inspection ******/ static Json::Value makeSingleValueJsonConfigFormat(const Json::Value &v, const Json::Value &defaultValue = Json::Value()); static Json::Value makeSingleStrValueJsonConfigFormat(const StaticString &val); static Json::Value makeSingleStrValueJsonConfigFormat(const StaticString &val, const StaticString &defaultValue); static Json::Value makeSingleNonEmptyStrValueJsonConfigFormat(const StaticString &val); unsigned int capacityUsedUnlocked() const; bool atFullCapacityUnlocked() const; void inspectProcessList(const InspectOptions &options, stringstream &result, const Group *group, const ProcessList &processes) const; public: typedef void (*AbortLongRunningConnectionsCallback)(const ProcessPtr &process); AbortLongRunningConnectionsCallback abortLongRunningConnectionsCallback; /****** Initialization and shutdown ******/ Pool(Context *context); ~Pool(); void initialize(); void initDebugging(); void prepareForShutdown(); void destroy(); /****** General utilities ******/ Context *getContext(); SpawningKit::Context *getSpawningKitContext() const; const RandomGeneratorPtr &getRandomGenerator() const; /****** Group manipulation ******/ GroupPtr findOrCreateGroup(const Options &options); GroupPtr findGroupByApiKey(const StaticString &value, bool lock = true) const; bool detachGroupByName(const HashedStaticString &name); bool detachGroupByApiKey(const StaticString &value); bool restartGroupByName(const StaticString &name, const RestartOptions &options = RestartOptions::makeAuthorized()); unsigned int restartGroupsByAppRoot(const StaticString &appRoot, const RestartOptions &options = RestartOptions::makeAuthorized()); /***** Process manipulation ******/ vector<ProcessPtr> getProcesses(bool lock = true) const; ProcessPtr findProcessByGupid(const StaticString &gupid, bool lock = true) const; ProcessPtr findProcessByPid(pid_t pid, bool lock = true) const; bool detachProcess(const ProcessPtr &process); bool detachProcess(pid_t pid, const AuthenticationOptions &options = AuthenticationOptions::makeAuthorized()); bool detachProcess(const string &gupid, const AuthenticationOptions &options = AuthenticationOptions::makeAuthorized()); DisableResult disableProcess(const StaticString &gupid); /****** State inspection ******/ unsigned int capacityUsed() const; bool atFullCapacity() const; unsigned int getProcessCount(bool lock = true) const; unsigned int getGroupCount() const; string inspect(const InspectOptions &options = InspectOptions::makeAuthorized(), bool lock = true) const; string toXml(const ToXmlOptions &options = ToXmlOptions::makeAuthorized(), bool lock = true) const; Json::Value inspectPropertiesInAdminPanelFormat(const ToJsonOptions &options = ToJsonOptions::makeAuthorized()) const; Json::Value inspectConfigInAdminPanelFormat(const ToJsonOptions &options = ToJsonOptions::makeAuthorized()) const; /****** Miscellaneous ******/ void asyncGet(const Options &options, const GetCallback &callback, bool lockNow = true); SessionPtr get(const Options &options, Ticket *ticket); void setMax(unsigned int max); void setMaxIdleTime(unsigned long long value); void enableSelfChecking(bool enabled); bool isSpawning(bool lock = true) const; bool authorizeByApiKey(const ApiKey &key, bool lock = true) const; bool authorizeByUid(uid_t uid, bool lock = true) const; }; } // namespace ApplicationPool2 } // namespace Passenger #endif /* _PASSENGER_APPLICATION_POOL2_POOL_H_ */ Controller/Implementation.cpp 0000644 00000003457 14756456557 0012421 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2015-2017 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include <Core/Controller.h> #include <Core/Controller/InitRequest.cpp> #include <Core/Controller/BufferBody.cpp> #include <Core/Controller/CheckoutSession.cpp> #include <Core/Controller/SendRequest.cpp> #include <Core/Controller/ForwardResponse.cpp> #include <Core/Controller/Hooks.cpp> #include <Core/Controller/InitializationAndShutdown.cpp> #include <Core/Controller/InternalUtils.cpp> #include <Core/Controller/Miscellaneous.cpp> #include <Core/Controller/Config.cpp> #include <Core/Controller/StateInspection.cpp> Controller/Request.h 0000644 00000010142 14756456557 0010516 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2011-2017 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef _PASSENGER_REQUEST_HANDLER_REQUEST_H_ #define _PASSENGER_REQUEST_HANDLER_REQUEST_H_ #include <ev++.h> #include <string> #include <cstring> #include <ServerKit/HttpRequest.h> #include <ServerKit/FdSinkChannel.h> #include <ServerKit/FdSourceChannel.h> #include <LoggingKit/LoggingKit.h> #include <Core/ApplicationPool/Pool.h> #include <Core/Controller/Config.h> #include <Core/Controller/AppResponse.h> namespace Passenger { namespace Core { using namespace std; using namespace boost; using namespace ApplicationPool2; class Request: public ServerKit::BaseHttpRequest { public: enum State { ANALYZING_REQUEST, BUFFERING_REQUEST_BODY, CHECKING_OUT_SESSION, SENDING_HEADER_TO_APP, FORWARDING_BODY_TO_APP, WAITING_FOR_APP_OUTPUT }; enum HalfClosePolicy { HALF_CLOSE_POLICY_UNINITIALIZED, HALF_CLOSE_UPON_REACHING_REQUEST_BODY_END, HALF_CLOSE_UPON_NEXT_REQUEST_EARLY_READ_ERROR, HALF_CLOSE_PERFORMED }; ev_tstamp startedAt; State state: 3; bool dechunkResponse: 1; bool requestBodyBuffering: 1; bool https: 1; bool stickySession: 1; // Range: 0..MAX_SESSION_CHECKOUT_TRY boost::uint8_t sessionCheckoutTry: 4; HalfClosePolicy halfClosePolicy: 2; bool appResponseInitialized: 1; bool strip100ContinueHeader: 1; bool hasPragmaHeader: 1; Options options; AbstractSessionPtr session; const LString *host; ControllerRequestConfigPtr config; ServerKit::FdSinkChannel appSink; ServerKit::FdSourceChannel appSource; AppResponse appResponse; ServerKit::FileBufferedChannel bodyBuffer; boost::uint64_t bodyBytesBuffered; // After dechunking HashedStaticString cacheKey; LString *cacheControl; LString *varyCookie; // Value of the `!~PASSENGER_ENV_VARS` header. This is different // from `options.environmentVariables`. If `!~PASSENGER_ENV_VARS` // is not set or is empty, then `envvars` is NULL, while // `options.environmentVariables` retains a previous value. // // This value is guaranteed to be contiguous. LString *envvars; #ifdef DEBUG_CC_EVENT_LOOP_BLOCKING bool timedAppPoolGet; ev_tstamp timeBeforeAccessingApplicationPool; ev_tstamp timeOnRequestHeaderSent; ev_tstamp timeOnResponseBegun; #endif Request() : BaseHttpRequest() { } const char *getStateString() const { switch (state) { case ANALYZING_REQUEST: return "ANALYZING_REQUEST"; case BUFFERING_REQUEST_BODY: return "BUFFERING_REQUEST_BODY"; case CHECKING_OUT_SESSION: return "CHECKING_OUT_SESSION"; case SENDING_HEADER_TO_APP: return "SENDING_HEADER_TO_APP"; case FORWARDING_BODY_TO_APP: return "FORWARDING_BODY_TO_APP"; case WAITING_FOR_APP_OUTPUT: return "WAITING_FOR_APP_OUTPUT"; default: return "UNKNOWN"; } } DEFINE_SERVER_KIT_BASE_HTTP_REQUEST_FOOTER(Passenger::Core::Request); }; } // namespace Core } // namespace Passenger #endif /* _PASSENGER_REQUEST_HANDLER_REQUEST_H_ */ Controller/InitializationAndShutdown.cpp 0000644 00000010613 14756456557 0014572 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2014-2017 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include <Core/Controller.h> /************************************************************************* * * Initialization and shutdown-related code for Core::Controller * *************************************************************************/ namespace Passenger { namespace Core { using namespace std; using namespace boost; /**************************** * * Public methods * ****************************/ Controller::~Controller() { ev_check_stop(getLoop(), &checkWatcher); delete singleAppModeConfig; } void Controller::preinitialize() { ev_check_init(&checkWatcher, onEventLoopCheck); ev_set_priority(&checkWatcher, EV_MAXPRI); ev_check_start(getLoop(), &checkWatcher); checkWatcher.data = this; #ifdef DEBUG_CC_EVENT_LOOP_BLOCKING ev_prepare_init(&prepareWatcher, onEventLoopPrepare); ev_prepare_start(getLoop(), &prepareWatcher); prepareWatcher.data = this; timeBeforeBlocking = 0; #endif PASSENGER_APP_GROUP_NAME = "!~PASSENGER_APP_GROUP_NAME"; PASSENGER_ENV_VARS = "!~PASSENGER_ENV_VARS"; PASSENGER_MAX_REQUESTS = "!~PASSENGER_MAX_REQUESTS"; PASSENGER_SHOW_VERSION_IN_HEADER = "!~PASSENGER_SHOW_VERSION_IN_HEADER"; PASSENGER_STICKY_SESSIONS = "!~PASSENGER_STICKY_SESSIONS"; PASSENGER_STICKY_SESSIONS_COOKIE_NAME = "!~PASSENGER_STICKY_SESSIONS_COOKIE_NAME"; PASSENGER_STICKY_SESSIONS_COOKIE_ATTRIBUTES = "!~PASSENGER_STICKY_SESSIONS_COOKIE_ATTRIBUTES"; PASSENGER_REQUEST_OOB_WORK = "!~Request-OOB-Work"; REMOTE_ADDR = "!~REMOTE_ADDR"; REMOTE_PORT = "!~REMOTE_PORT"; REMOTE_USER = "!~REMOTE_USER"; FLAGS = "!~FLAGS"; HTTP_COOKIE = "cookie"; HTTP_DATE = "date"; HTTP_HOST = "host"; HTTP_CONTENT_LENGTH = "content-length"; HTTP_CONTENT_TYPE = "content-type"; HTTP_EXPECT = "expect"; HTTP_CONNECTION = "connection"; HTTP_STATUS = "status"; HTTP_TRANSFER_ENCODING = "transfer-encoding"; /**************************/ } void Controller::initialize() { TRACE_POINT(); if (resourceLocator == NULL) { throw RuntimeException("ResourceLocator not initialized"); } if (wrapperRegistry == NULL) { throw RuntimeException("WrapperRegistry not initialized"); } if (appPool == NULL) { throw RuntimeException("AppPool not initialized"); } ParentClass::initialize(); turboCaching.initialize(config["turbocaching"].asBool()); if (mainConfig.singleAppMode) { boost::shared_ptr<Options> options = boost::make_shared<Options>(); fillPoolOptionsFromConfigCaches(*options, mainConfig.pool, requestConfig); string appRoot = singleAppModeConfig->get("app_root").asString(); string environment = config["default_environment"].asString(); string appType = singleAppModeConfig->get("app_type").asString(); string startupFile = singleAppModeConfig->get("startup_file").asString(); string appStartCommand = singleAppModeConfig->get("app_start_command").asString(); options->appRoot = appRoot; options->environment = environment; options->appType = appType; options->startupFile = startupFile; options->appStartCommand = appStartCommand; *options = options->copyAndPersist(); poolOptionsCache.insert(options->getAppGroupName(), options); } } } // namespace Core } // namespace Passenger Controller/ForwardResponse.cpp 0000644 00000113143 14756456557 0012551 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2011-2017 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include <Core/Controller.h> /************************************************************************* * * Implements Core::Controller methods pertaining sending application * response data to the client. This happens in parallel to the process * of sending request data to the application. * *************************************************************************/ namespace Passenger { namespace Core { using namespace std; using namespace boost; /**************************** * * Private methods * ****************************/ ServerKit::Channel::Result Controller::_onAppSourceData(Channel *_channel, const MemoryKit::mbuf &buffer, int errcode) { FdSourceChannel *channel = reinterpret_cast<FdSourceChannel *>(_channel); Request *req = static_cast<Request *>(static_cast< ServerKit::BaseHttpRequest *>(channel->getHooks()->userData)); Client *client = static_cast<Client *>(req->client); Controller *self = static_cast<Controller *>(getServerFromClient(client)); return self->onAppSourceData(client, req, buffer, errcode); } ServerKit::Channel::Result Controller::onAppSourceData(Client *client, Request *req, const MemoryKit::mbuf &buffer, int errcode) { SKC_LOG_EVENT(Controller, client, "onAppSourceData"); AppResponse *resp = &req->appResponse; switch (resp->httpState) { case AppResponse::PARSING_HEADERS: if (buffer.size() > 0) { // Data UPDATE_TRACE_POINT(); size_t ret; SKC_TRACE(client, 3, "Processing " << buffer.size() << " bytes of application data: \"" << cEscapeString(StaticString( buffer.start, buffer.size())) << "\""); { ret = createAppResponseHeaderParser(getContext(), req). feed(buffer); } if (resp->httpState == AppResponse::PARSING_HEADERS) { // Not yet done parsing. return Channel::Result(buffer.size(), false); } // Done parsing. UPDATE_TRACE_POINT(); SKC_TRACE(client, 2, "Application response headers received"); getHeaderParserStatePool().destroy(resp->parserState.headerParser); resp->parserState.headerParser = NULL; switch (resp->httpState) { case AppResponse::COMPLETE: req->appSource.stop(); onAppResponseBegin(client, req); return Channel::Result(ret, false); case AppResponse::PARSING_BODY_WITH_LENGTH: SKC_TRACE(client, 2, "Expecting an app response body with fixed length"); onAppResponseBegin(client, req); return Channel::Result(ret, false); case AppResponse::PARSING_BODY_UNTIL_EOF: SKC_TRACE(client, 2, "Expecting app response body until end of stream"); req->wantKeepAlive = false; onAppResponseBegin(client, req); return Channel::Result(ret, false); case AppResponse::PARSING_CHUNKED_BODY: SKC_TRACE(client, 2, "Expecting a chunked app response body"); prepareAppResponseChunkedBodyParsing(client, req); onAppResponseBegin(client, req); return Channel::Result(ret, false); case AppResponse::UPGRADED: SKC_TRACE(client, 2, "Application upgraded connection"); req->wantKeepAlive = false; onAppResponseBegin(client, req); return Channel::Result(ret, false); case AppResponse::ONEHUNDRED_CONTINUE: SKC_TRACE(client, 2, "Application sent 100-Continue status"); onAppResponse100Continue(client, req); return Channel::Result(ret, false); case AppResponse::ERROR: SKC_ERROR(client, "Error parsing application response header: " << ServerKit::getErrorDesc(resp->aux.parseError)); endRequestAsBadGateway(&client, &req); return Channel::Result(0, true); default: P_BUG("Invalid response HTTP state " << (int) resp->httpState); return Channel::Result(0, true); } } else if (errcode == 0 || errcode == ECONNRESET) { // EOF UPDATE_TRACE_POINT(); SKC_DEBUG(client, "Application sent EOF before finishing response headers"); endRequestWithAppSocketIncompleteResponse(&client, &req); return Channel::Result(0, true); } else { // Error UPDATE_TRACE_POINT(); SKC_DEBUG(client, "Application socket read error occurred before finishing response headers"); endRequestWithAppSocketReadError(&client, &req, errcode); return Channel::Result(0, true); } case AppResponse::PARSING_BODY_WITH_LENGTH: if (buffer.size() > 0) { // Data UPDATE_TRACE_POINT(); boost::uint64_t maxRemaining, remaining; maxRemaining = resp->aux.bodyInfo.contentLength - resp->bodyAlreadyRead; remaining = std::min<boost::uint64_t>(buffer.size(), maxRemaining); resp->bodyAlreadyRead += remaining; SKC_TRACE(client, 3, "Processing " << buffer.size() << " bytes of application data: \"" << cEscapeString(StaticString( buffer.start, buffer.size())) << "\""); SKC_TRACE(client, 3, "Application response body: " << resp->bodyAlreadyRead << " of " << resp->aux.bodyInfo.contentLength << " bytes already read"); if (remaining > 0) { UPDATE_TRACE_POINT(); writeResponseAndMarkForTurboCaching(client, req, MemoryKit::mbuf(buffer, 0, remaining)); if (!req->ended()) { if (resp->bodyFullyRead()) { SKC_TRACE(client, 2, "End of application response body reached"); handleAppResponseBodyEnd(client, req); endRequest(&client, &req); } else { maybeThrottleAppSource(client, req); } } } else { UPDATE_TRACE_POINT(); SKC_TRACE(client, 2, "End of application response body reached"); handleAppResponseBodyEnd(client, req); endRequest(&client, &req); } return Channel::Result(remaining, false); } else if (errcode == 0 || errcode == ECONNRESET) { // EOF UPDATE_TRACE_POINT(); if (resp->bodyFullyRead()) { SKC_TRACE(client, 2, "Application sent EOF"); handleAppResponseBodyEnd(client, req); endRequest(&client, &req); } else { SKC_WARN(client, "Application sent EOF before finishing response body: " << resp->bodyAlreadyRead << " bytes already read, " << resp->aux.bodyInfo.contentLength << " bytes expected"); endRequestWithAppSocketIncompleteResponse(&client, &req); } return Channel::Result(0, true); } else { // Error UPDATE_TRACE_POINT(); endRequestWithAppSocketReadError(&client, &req, errcode); return Channel::Result(0, true); } case AppResponse::PARSING_CHUNKED_BODY: if (!buffer.empty()) { // Data UPDATE_TRACE_POINT(); SKC_TRACE(client, 3, "Processing " << buffer.size() << " bytes of application data: \"" << cEscapeString(StaticString( buffer.start, buffer.size())) << "\""); ServerKit::HttpChunkedEvent event(createAppResponseChunkedBodyParser(req) .feed(buffer)); resp->bodyAlreadyRead += event.consumed; if (req->dechunkResponse) { UPDATE_TRACE_POINT(); switch (event.type) { case ServerKit::HttpChunkedEvent::NONE: assert(!event.end); return Channel::Result(event.consumed, false); case ServerKit::HttpChunkedEvent::DATA: assert(!event.end); writeResponseAndMarkForTurboCaching(client, req, event.data); maybeThrottleAppSource(client, req); return Channel::Result(event.consumed, false); case ServerKit::HttpChunkedEvent::END: assert(event.end); SKC_TRACE(client, 2, "End of application response body reached"); resp->aux.bodyInfo.endReached = true; handleAppResponseBodyEnd(client, req); endRequest(&client, &req); return Channel::Result(event.consumed, true); case ServerKit::HttpChunkedEvent::ERROR: assert(event.end); { string message = "error parsing app response chunked encoding: "; message.append(ServerKit::getErrorDesc(event.errcode)); disconnectWithError(&client, message); } return Channel::Result(event.consumed, true); } } else { UPDATE_TRACE_POINT(); switch (event.type) { case ServerKit::HttpChunkedEvent::NONE: case ServerKit::HttpChunkedEvent::DATA: assert(!event.end); writeResponse(client, MemoryKit::mbuf(buffer, 0, event.consumed)); markResponsePartForTurboCaching(client, req, event.data); maybeThrottleAppSource(client, req); return Channel::Result(event.consumed, false); case ServerKit::HttpChunkedEvent::END: assert(event.end); SKC_TRACE(client, 2, "End of application response body reached"); resp->aux.bodyInfo.endReached = true; handleAppResponseBodyEnd(client, req); writeResponse(client, MemoryKit::mbuf(buffer, 0, event.consumed)); if (!req->ended()) { endRequest(&client, &req); } return Channel::Result(event.consumed, true); case ServerKit::HttpChunkedEvent::ERROR: assert(event.end); { string message = "error parsing app response chunked encoding: "; message.append(ServerKit::getErrorDesc(event.errcode)); disconnectWithError(&client, message); } return Channel::Result(event.consumed, true); } } } else if (errcode == 0 || errcode == ECONNRESET) { // Premature EOF. This cannot be an expected EOF because // we end the request upon consuming the end of the chunked body. UPDATE_TRACE_POINT(); disconnectWithError(&client, "error parsing app response chunked encoding: " "unexpected end-of-stream"); return Channel::Result(0, false); } else { // Error UPDATE_TRACE_POINT(); endRequestWithAppSocketReadError(&client, &req, errcode); return Channel::Result(0, true); } break; // Never reached, shut up compiler warning. case AppResponse::PARSING_BODY_UNTIL_EOF: case AppResponse::UPGRADED: if (buffer.size() > 0) { // Data UPDATE_TRACE_POINT(); SKC_TRACE(client, 3, "Processing " << buffer.size() << " bytes of application data: \"" << cEscapeString(StaticString( buffer.start, buffer.size())) << "\""); resp->bodyAlreadyRead += buffer.size(); writeResponseAndMarkForTurboCaching(client, req, buffer); maybeThrottleAppSource(client, req); return Channel::Result(buffer.size(), false); } else if (errcode == 0 || errcode == ECONNRESET) { // EOF UPDATE_TRACE_POINT(); SKC_TRACE(client, 2, "Application sent EOF"); SKC_TRACE(client, 2, "Not keep-aliving application session connection"); req->session->close(true, false); endRequest(&client, &req); return Channel::Result(0, false); } else { // Error UPDATE_TRACE_POINT(); endRequestWithAppSocketReadError(&client, &req, errcode); return Channel::Result(0, false); } break; // Never reached, shut up compiler warning. default: P_BUG("Invalid request HTTP state " << (int) resp->httpState); return Channel::Result(0, false); } return Channel::Result(0, false); // Never reached, shut up compiler warning. } void Controller::onAppResponseBegin(Client *client, Request *req) { TRACE_POINT(); AppResponse *resp = &req->appResponse; ssize_t bytesWritten; bool oobw; #ifdef DEBUG_CC_EVENT_LOOP_BLOCKING req->timeOnRequestHeaderSent = ev_now(getLoop()); reportLargeTimeDiff(client, "Headers sent until response begun", req->timeOnRequestHeaderSent, ev_now(getLoop())); #endif // Localize hash table operations for better CPU caching. oobw = resp->secureHeaders.lookup(PASSENGER_REQUEST_OOB_WORK) != NULL; resp->date = resp->headers.lookup(HTTP_DATE); resp->setCookie = resp->headers.lookup(ServerKit::HTTP_SET_COOKIE); if (resp->setCookie != NULL) { // Move the Set-Cookie header from resp->headers to resp->setCookie; // remove Set-Cookie from resp->headers without deallocating it. LString *copy; copy = (LString *) psg_palloc(req->pool, sizeof(LString)); psg_lstr_init(copy); psg_lstr_move_and_append(resp->setCookie, req->pool, copy); P_ASSERT_EQ(resp->setCookie->size, 0); psg_lstr_append(resp->setCookie, req->pool, "x", 1); resp->headers.erase(ServerKit::HTTP_SET_COOKIE); resp->setCookie = copy; } resp->headers.erase(HTTP_CONNECTION); resp->headers.erase(HTTP_STATUS); if (resp->bodyType == AppResponse::RBT_CONTENT_LENGTH) { resp->headers.erase(HTTP_CONTENT_LENGTH); } if (resp->bodyType == AppResponse::RBT_CHUNKED) { resp->headers.erase(HTTP_TRANSFER_ENCODING); if (req->dechunkResponse) { req->wantKeepAlive = false; } } if (resp->headers.lookup(ServerKit::HTTP_X_SENDFILE) != NULL || resp->headers.lookup(ServerKit::HTTP_X_ACCEL_REDIRECT) != NULL) { // If X-Sendfile or X-Accel-Redirect is set, then HttpHeaderParser // treats the app response as having no body, and removes the // Content-Length and Transfer-Encoding headers. Because of this, // the response that we output also doesn't Content-Length // or Transfer-Encoding. So we should disable keep-alive. req->wantKeepAlive = false; } prepareAppResponseCaching(client, req); if (OXT_UNLIKELY(oobw)) { SKC_TRACE(client, 2, "Response with OOBW detected"); if (req->session != NULL) { req->session->requestOOBW(); } } UPDATE_TRACE_POINT(); if (!sendResponseHeaderWithWritev(client, req, bytesWritten)) { UPDATE_TRACE_POINT(); if (bytesWritten >= 0 || errno == EAGAIN || errno == EWOULDBLOCK) { sendResponseHeaderWithBuffering(client, req, bytesWritten); } else { int e = errno; P_ASSERT_EQ(bytesWritten, -1); disconnectWithClientSocketWriteError(&client, e); } } if (!req->ended() && !resp->hasBody() && !resp->upgraded()) { UPDATE_TRACE_POINT(); handleAppResponseBodyEnd(client, req); endRequest(&client, &req); } } void Controller::prepareAppResponseCaching(Client *client, Request *req) { if (turboCaching.isEnabled() && !req->cacheKey.empty()) { TRACE_POINT(); AppResponse *resp = &req->appResponse; SKC_TRACE(client, 2, "Turbocache: preparing response caching"); if (turboCaching.responseCache.requestAllowsStoring(req) && turboCaching.responseCache.prepareRequestForStoring(req)) { if (resp->bodyType == AppResponse::RBT_CONTENT_LENGTH && resp->aux.bodyInfo.contentLength > ResponseCache<Request>::MAX_BODY_SIZE) { SKC_DEBUG(client, "Response body larger than " << ResponseCache<Request>::MAX_BODY_SIZE << " bytes, so response is not eligible for turbocaching"); // Decrease store success ratio. turboCaching.responseCache.incStores(); req->cacheKey = HashedStaticString(); } } else if (turboCaching.responseCache.requestAllowsInvalidating(req)) { SKC_DEBUG(client, "Processing turbocache invalidation based on response"); turboCaching.responseCache.invalidate(req); req->cacheKey = HashedStaticString(); SKC_TRACE(client, 2, "Turbocache entries:\n" << turboCaching.responseCache.inspect()); } else { SKC_TRACE(client, 2, "Turbocache: response not eligible for turbocaching"); // Decrease store success ratio. turboCaching.responseCache.incStores(); req->cacheKey = HashedStaticString(); } } } void Controller::onAppResponse100Continue(Client *client, Request *req) { TRACE_POINT(); if (!req->strip100ContinueHeader) { UPDATE_TRACE_POINT(); const unsigned int BUFSIZE = 32; char *buf = (char *) psg_pnalloc(req->pool, BUFSIZE); int size = snprintf(buf, BUFSIZE, "HTTP/%d.%d 100 Continue\r\n", (int) req->httpMajor, (int) req->httpMinor); writeResponse(client, buf, size); } if (!req->ended()) { UPDATE_TRACE_POINT(); deinitializeAppResponse(client, req); reinitializeAppResponse(client, req); req->appResponse.oneHundredContinueSent = !req->strip100ContinueHeader; // Allow sending more response headers. req->responseBegun = false; } } /** * Construct an array of buffers, which together contain the HTTP response * data that should be sent to the client. This method does not copy any data: * it just constructs buffers that point to the data stored inside `req->pool`, * `req->appResponse.headers`, etc. * * The buffers will be stored in the array pointed to by `buffer`. This array must * have space for at least `maxbuffers` items. The actual number of buffers constructed * is stored in `nbuffers`, and the total data size of the buffers is stored in `dataSize`. * Upon success, returns true. If the actual number of buffers necessary exceeds * `maxbuffers`, then false is returned. * * You can also set `buffers` to NULL, in which case this method will not construct any * buffers, but only count the number of buffers necessary, as well as the total data size. * In this case, this method always returns true. */ bool Controller::constructHeaderBuffersForResponse(Request *req, struct iovec *buffers, unsigned int maxbuffers, unsigned int & restrict_ref nbuffers, unsigned int & restrict_ref dataSize, unsigned int & restrict_ref nCacheableBuffers) { #define BEGIN_PUSH_NEXT_BUFFER() \ do { \ if (buffers != NULL && i >= maxbuffers) { \ return false; \ } \ } while (false) #define INC_BUFFER_ITER(i) \ do { \ i++; \ } while (false) #define PUSH_STATIC_BUFFER(str) \ do { \ BEGIN_PUSH_NEXT_BUFFER(); \ if (buffers != NULL) { \ buffers[i].iov_base = (void *) str; \ buffers[i].iov_len = sizeof(str) - 1; \ } \ INC_BUFFER_ITER(i); \ dataSize += sizeof(str) - 1; \ } while (false) AppResponse *resp = &req->appResponse; ServerKit::HeaderTable::Iterator it(resp->headers); const LString::Part *part; const char *statusAndReason; unsigned int i = 0; nbuffers = 0; dataSize = 0; PUSH_STATIC_BUFFER("HTTP/"); if (buffers != NULL) { BEGIN_PUSH_NEXT_BUFFER(); const unsigned int BUFSIZE = 16; char *buf = (char *) psg_pnalloc(req->pool, BUFSIZE); const char *end = buf + BUFSIZE; char *pos = buf; pos += uintToString(req->httpMajor, pos, end - pos); pos = appendData(pos, end, ".", 1); pos += uintToString(req->httpMinor, pos, end - pos); buffers[i].iov_base = (void *) buf; buffers[i].iov_len = pos - buf; dataSize += pos - buf; } else { char buf[16]; const char *end = buf + sizeof(buf); char *pos = buf; pos += uintToString(req->httpMajor, pos, end - pos); pos = appendData(pos, end, ".", 1); pos += uintToString(req->httpMinor, pos, end - pos); dataSize += pos - buf; } INC_BUFFER_ITER(i); PUSH_STATIC_BUFFER(" "); statusAndReason = getStatusCodeAndReasonPhrase(resp->statusCode); if (statusAndReason != NULL) { size_t len = strlen(statusAndReason); BEGIN_PUSH_NEXT_BUFFER(); if (buffers != NULL) { BEGIN_PUSH_NEXT_BUFFER(); buffers[i].iov_base = (void *) statusAndReason; buffers[i].iov_len = len; } INC_BUFFER_ITER(i); dataSize += len; PUSH_STATIC_BUFFER("\r\nStatus: "); if (buffers != NULL) { BEGIN_PUSH_NEXT_BUFFER(); buffers[i].iov_base = (void *) statusAndReason; buffers[i].iov_len = len; } INC_BUFFER_ITER(i); dataSize += len; PUSH_STATIC_BUFFER("\r\n"); } else { if (buffers != NULL) { BEGIN_PUSH_NEXT_BUFFER(); const unsigned int BUFSIZE = 8; char *buf = (char *) psg_pnalloc(req->pool, BUFSIZE); const char *end = buf + BUFSIZE; char *pos = buf; unsigned int size = uintToString(resp->statusCode, pos, end - pos); buffers[i].iov_base = (void *) buf; buffers[i].iov_len = size; INC_BUFFER_ITER(i); dataSize += size; PUSH_STATIC_BUFFER(" Unknown Reason-Phrase\r\nStatus: "); BEGIN_PUSH_NEXT_BUFFER(); buffers[i].iov_base = (void *) buf; buffers[i].iov_len = size; INC_BUFFER_ITER(i); dataSize += size; PUSH_STATIC_BUFFER("\r\n"); } else { char buf[8]; const char *end = buf + sizeof(buf); char *pos = buf; unsigned int size = uintToString(resp->statusCode, pos, end - pos); INC_BUFFER_ITER(i); dataSize += size; dataSize += sizeof(" Unknown Reason-Phrase\r\nStatus: ") - 1; INC_BUFFER_ITER(i); dataSize += size; INC_BUFFER_ITER(i); dataSize += sizeof("\r\n"); INC_BUFFER_ITER(i); } } while (*it != NULL) { dataSize += it->header->origKey.size + sizeof(": ") - 1; dataSize += it->header->val.size + sizeof("\r\n") - 1; part = it->header->origKey.start; while (part != NULL) { if (buffers != NULL) { BEGIN_PUSH_NEXT_BUFFER(); buffers[i].iov_base = (void *) part->data; buffers[i].iov_len = part->size; } INC_BUFFER_ITER(i); part = part->next; } if (buffers != NULL) { BEGIN_PUSH_NEXT_BUFFER(); buffers[i].iov_base = (void *) ": "; buffers[i].iov_len = sizeof(": ") - 1; } INC_BUFFER_ITER(i); part = it->header->val.start; while (part != NULL) { if (buffers != NULL) { BEGIN_PUSH_NEXT_BUFFER(); buffers[i].iov_base = (void *) part->data; buffers[i].iov_len = part->size; } INC_BUFFER_ITER(i); part = part->next; } if (buffers != NULL) { BEGIN_PUSH_NEXT_BUFFER(); buffers[i].iov_base = (void *) "\r\n"; buffers[i].iov_len = sizeof("\r\n") - 1; } INC_BUFFER_ITER(i); it.next(); } // Add Date header. https://code.google.com/p/phusion-passenger/issues/detail?id=485 if (resp->date == NULL) { unsigned int size; if (buffers != NULL) { BEGIN_PUSH_NEXT_BUFFER(); const unsigned int BUFSIZE = 60; char *dateStr = (char *) psg_pnalloc(req->pool, BUFSIZE); size = constructDateHeaderBuffersForResponse(dateStr, BUFSIZE); buffers[i].iov_base = dateStr; buffers[i].iov_len = size; } else { char dateStr[60]; size = constructDateHeaderBuffersForResponse(dateStr, sizeof(dateStr)); } INC_BUFFER_ITER(i); dataSize += size; PUSH_STATIC_BUFFER("\r\n"); } if (resp->setCookie != NULL) { PUSH_STATIC_BUFFER("Set-Cookie: "); part = resp->setCookie->start; while (part != NULL) { if (part->size == 1 && part->data[0] == '\n') { // HeaderTable joins multiple Set-Cookie headers together using \n. PUSH_STATIC_BUFFER("\r\nSet-Cookie: "); } else { if (buffers != NULL) { BEGIN_PUSH_NEXT_BUFFER(); buffers[i].iov_base = (void *) part->data; buffers[i].iov_len = part->size; } INC_BUFFER_ITER(i); dataSize += part->size; } part = part->next; } PUSH_STATIC_BUFFER("\r\n"); } nCacheableBuffers = i; if (resp->bodyType == AppResponse::RBT_CONTENT_LENGTH) { PUSH_STATIC_BUFFER("Content-Length: "); if (buffers != NULL) { BEGIN_PUSH_NEXT_BUFFER(); const unsigned int BUFSIZE = 16; char *buf = (char *) psg_pnalloc(req->pool, BUFSIZE); unsigned int size = integerToOtherBase<boost::uint64_t, 10>( resp->aux.bodyInfo.contentLength, buf, BUFSIZE); buffers[i].iov_base = (void *) buf; buffers[i].iov_len = size; dataSize += size; } else { dataSize += integerSizeInOtherBase<boost::uint64_t, 10>( resp->aux.bodyInfo.contentLength); } INC_BUFFER_ITER(i); PUSH_STATIC_BUFFER("\r\n"); } else if (resp->bodyType == AppResponse::RBT_CHUNKED && !req->dechunkResponse) { PUSH_STATIC_BUFFER("Transfer-Encoding: chunked\r\n"); } if (resp->bodyType == AppResponse::RBT_UPGRADE) { PUSH_STATIC_BUFFER("Connection: upgrade\r\n"); } else if (canKeepAlive(req)) { unsigned int httpVersion = req->httpMajor * 1000 + req->httpMinor * 10; if (httpVersion < 1010) { // HTTP < 1.1 defaults to "Connection: close" PUSH_STATIC_BUFFER("Connection: keep-alive\r\n"); } } else { unsigned int httpVersion = req->httpMajor * 1000 + req->httpMinor * 10; if (httpVersion >= 1010) { // HTTP 1.1 defaults to "Connection: keep-alive" PUSH_STATIC_BUFFER("Connection: close\r\n"); } } if (req->stickySession) { StaticString baseURI = req->options.baseURI; if (baseURI.empty()) { baseURI = P_STATIC_STRING("/"); } // Note that we do NOT set HttpOnly. If we set that flag then Chrome // doesn't send cookies over WebSocket handshakes. Confirmed on Chrome 25. const LString *cookieName = getStickySessionCookieName(req); unsigned int stickySessionId; unsigned int stickySessionIdSize; char *stickySessionIdStr; PUSH_STATIC_BUFFER("Set-Cookie: "); part = cookieName->start; while (part != NULL) { if (buffers != NULL) { BEGIN_PUSH_NEXT_BUFFER(); buffers[i].iov_base = (void *) part->data; buffers[i].iov_len = part->size; } dataSize += part->size; INC_BUFFER_ITER(i); part = part->next; } stickySessionId = req->session->getStickySessionId(); stickySessionIdSize = uintSizeAsString(stickySessionId); stickySessionIdStr = (char *) psg_pnalloc(req->pool, stickySessionIdSize + 1); uintToString(stickySessionId, stickySessionIdStr, stickySessionIdSize + 1); PUSH_STATIC_BUFFER("="); if (buffers != NULL) { BEGIN_PUSH_NEXT_BUFFER(); buffers[i].iov_base = stickySessionIdStr; buffers[i].iov_len = stickySessionIdSize; } dataSize += stickySessionIdSize; INC_BUFFER_ITER(i); PUSH_STATIC_BUFFER("; Path="); if (buffers != NULL) { BEGIN_PUSH_NEXT_BUFFER(); buffers[i].iov_base = (void *) baseURI.data(); buffers[i].iov_len = baseURI.size(); } dataSize += baseURI.size(); INC_BUFFER_ITER(i); StaticString stickyAttributes = req->options.stickySessionsCookieAttributes; if (stickyAttributes.size() > 0) { PUSH_STATIC_BUFFER("; "); if (buffers != NULL) { BEGIN_PUSH_NEXT_BUFFER(); buffers[i].iov_base = (void *) stickyAttributes.data(); buffers[i].iov_len = stickyAttributes.size(); } dataSize += stickyAttributes.size(); INC_BUFFER_ITER(i); } PUSH_STATIC_BUFFER("\r\n"); } if (req->config->showVersionInHeader) { #ifdef PASSENGER_IS_ENTERPRISE PUSH_STATIC_BUFFER("X-Powered-By: " PROGRAM_NAME " Enterprise " PASSENGER_VERSION "\r\n\r\n"); #else PUSH_STATIC_BUFFER("X-Powered-By: " PROGRAM_NAME " " PASSENGER_VERSION "\r\n\r\n"); #endif } else { #ifdef PASSENGER_IS_ENTERPRISE PUSH_STATIC_BUFFER("X-Powered-By: " PROGRAM_NAME " Enterprise\r\n\r\n"); #else PUSH_STATIC_BUFFER("X-Powered-By: " PROGRAM_NAME "\r\n\r\n"); #endif } nbuffers = i; return true; #undef BEGIN_PUSH_NEXT_BUFFER #undef INC_BUFFER_ITER #undef PUSH_STATIC_BUFFER } unsigned int Controller::constructDateHeaderBuffersForResponse(char *dateStr, unsigned int bufsize) { char *pos = dateStr; const char *end = dateStr + bufsize - 1; time_t the_time = (time_t) ev_now(getContext()->libev->getLoop()); struct tm the_tm; pos = appendData(pos, end, "Date: "); gmtime_r(&the_time, &the_tm); pos += strftime(pos, end - pos, "%a, %d %b %Y %H:%M:%S GMT", &the_tm); return pos - dateStr; } bool Controller::sendResponseHeaderWithWritev(Client *client, Request *req, ssize_t &bytesWritten) { TRACE_POINT(); if (OXT_UNLIKELY(mainConfig.benchmarkMode == BM_RESPONSE_BEGIN)) { writeBenchmarkResponse(&client, &req, false); return true; } unsigned int maxbuffers = std::min<unsigned int>( 8 + req->appResponse.headers.size() * 4 + 11, IOV_MAX); struct iovec *buffers = (struct iovec *) psg_palloc(req->pool, sizeof(struct iovec) * maxbuffers); unsigned int nbuffers, dataSize, nCacheableBuffers; if (constructHeaderBuffersForResponse(req, buffers, maxbuffers, nbuffers, dataSize, nCacheableBuffers)) { UPDATE_TRACE_POINT(); SKC_TRACE(client, 2, "Sending response headers using writev()"); logResponseHeaders(client, req, buffers, nbuffers, dataSize); markHeaderBuffersForTurboCaching(client, req, buffers, nCacheableBuffers); ssize_t ret; do { ret = writev(client->getFd(), buffers, nbuffers); } while (ret == -1 && errno == EINTR); bytesWritten = ret; req->responseBegun |= ret > 0; return ret == (ssize_t) dataSize; } else { UPDATE_TRACE_POINT(); bytesWritten = 0; return false; } } void Controller::sendResponseHeaderWithBuffering(Client *client, Request *req, unsigned int offset) { TRACE_POINT(); struct iovec *buffers; unsigned int nbuffers, dataSize, nCacheableBuffers; bool ok; ok = constructHeaderBuffersForResponse(req, NULL, 0, nbuffers, dataSize, nCacheableBuffers); assert(ok); buffers = (struct iovec *) psg_palloc(req->pool, sizeof(struct iovec) * nbuffers); ok = constructHeaderBuffersForResponse(req, buffers, nbuffers, nbuffers, dataSize, nCacheableBuffers); assert(ok); (void) ok; // Shut up compiler warning UPDATE_TRACE_POINT(); logResponseHeaders(client, req, buffers, nbuffers, dataSize); markHeaderBuffersForTurboCaching(client, req, buffers, nCacheableBuffers); MemoryKit::mbuf_pool &mbuf_pool = getContext()->mbuf_pool; const unsigned int MBUF_MAX_SIZE = mbuf_pool_data_size(&mbuf_pool); if (dataSize <= MBUF_MAX_SIZE) { UPDATE_TRACE_POINT(); SKC_TRACE(client, 2, "Sending response headers using an mbuf"); MemoryKit::mbuf buffer(MemoryKit::mbuf_get(&mbuf_pool)); gatherBuffers(buffer.start, MBUF_MAX_SIZE, buffers, nbuffers); buffer = MemoryKit::mbuf(buffer, offset, dataSize - offset); writeResponse(client, buffer); } else { UPDATE_TRACE_POINT(); SKC_TRACE(client, 2, "Sending response headers using a psg_pool buffer"); char *buffer = (char *) psg_pnalloc(req->pool, dataSize); gatherBuffers(buffer, dataSize, buffers, nbuffers); writeResponse(client, buffer + offset, dataSize - offset); } } void Controller::logResponseHeaders(Client *client, Request *req, struct iovec *buffers, unsigned int nbuffers, unsigned int dataSize) { if (OXT_UNLIKELY(LoggingKit::getLevel() >= LoggingKit::DEBUG3)) { TRACE_POINT(); char *buffer = (char *) psg_pnalloc(req->pool, dataSize); gatherBuffers(buffer, dataSize, buffers, nbuffers); SKC_TRACE(client, 3, "Sending response headers: \"" << cEscapeString(StaticString(buffer, dataSize)) << "\""); } } void Controller::markHeaderBuffersForTurboCaching(Client *client, Request *req, struct iovec *buffers, unsigned int nbuffers) { if (turboCaching.isEnabled() && !req->cacheKey.empty()) { unsigned int totalSize = 0; for (unsigned int i = 0; i < nbuffers; i++) { totalSize += buffers[i].iov_len; } if (totalSize > ResponseCache<Request>::MAX_HEADER_SIZE) { SKC_DEBUG(client, "Response headers larger than " << ResponseCache<Request>::MAX_HEADER_SIZE << " bytes, so response is not eligible for turbocaching"); // Decrease store success ratio. turboCaching.responseCache.incStores(); req->cacheKey = HashedStaticString(); } else { req->appResponse.headerCacheBuffers = buffers; req->appResponse.nHeaderCacheBuffers = nbuffers; } } } ServerKit::HttpHeaderParser<AppResponse, ServerKit::HttpParseResponse> Controller::createAppResponseHeaderParser(ServerKit::Context *ctx, Request *req) { return ServerKit::HttpHeaderParser<AppResponse, ServerKit::HttpParseResponse>( ctx, req->appResponse.parserState.headerParser, &req->appResponse, req->pool, req->method); } ServerKit::HttpChunkedBodyParser Controller::createAppResponseChunkedBodyParser(Request *req) { return ServerKit::HttpChunkedBodyParser( &req->appResponse.parserState.chunkedBodyParser, formatAppResponseChunkedBodyParserLoggingPrefix, req); } unsigned int Controller::formatAppResponseChunkedBodyParserLoggingPrefix(char *buf, unsigned int bufsize, void *userData) { Request *req = static_cast<Request *>(userData); return snprintf(buf, bufsize, "[Client %u] ChunkedBodyParser: ", static_cast<Client *>(req->client)->number); } void Controller::prepareAppResponseChunkedBodyParsing(Client *client, Request *req) { P_ASSERT_EQ(req->appResponse.bodyType, AppResponse::RBT_CHUNKED); createAppResponseChunkedBodyParser(req).initialize(); } void Controller::writeResponseAndMarkForTurboCaching(Client *client, Request *req, const MemoryKit::mbuf &buffer) { if (OXT_LIKELY(mainConfig.benchmarkMode != BM_RESPONSE_BEGIN)) { writeResponse(client, buffer); } markResponsePartForTurboCaching(client, req, buffer); } void Controller::markResponsePartForTurboCaching(Client *client, Request *req, const MemoryKit::mbuf &buffer) { if (!req->ended() && turboCaching.isEnabled() && !req->cacheKey.empty()) { unsigned int totalSize = req->appResponse.bodyCacheBuffer.size + buffer.size(); if (totalSize > ResponseCache<Request>::MAX_BODY_SIZE) { SKC_DEBUG(client, "Response body larger than " << ResponseCache<Request>::MAX_HEADER_SIZE << " bytes, so response is not eligible for turbocaching"); // Decrease store success ratio. turboCaching.responseCache.incStores(); req->cacheKey = HashedStaticString(); psg_lstr_deinit(&req->appResponse.bodyCacheBuffer); } else { psg_lstr_append(&req->appResponse.bodyCacheBuffer, req->pool, buffer, buffer.start, buffer.size()); } } } void Controller::maybeThrottleAppSource(Client *client, Request *req) { if (!req->ended()) { assert(client->output.getBuffersFlushedCallback() == NULL); assert(client->output.getDataFlushedCallback() == getClientOutputDataFlushedCallback()); if (mainConfig.responseBufferHighWatermark > 0 && client->output.getTotalBytesBuffered() >= mainConfig.responseBufferHighWatermark) { SKC_TRACE(client, 2, "Application is sending response data quicker than the client " "can keep up with. Throttling application socket"); client->output.setDataFlushedCallback(_outputDataFlushed); req->appSource.stop(); } else if (client->output.passedThreshold()) { SKC_TRACE(client, 2, "Application is sending response data quicker than the on-disk " "buffer can keep up with (currently buffered " << client->output.getBytesBuffered() << " bytes). Throttling application socket"); client->output.setBuffersFlushedCallback(_outputBuffersFlushed); req->appSource.stop(); } } } void Controller::_outputBuffersFlushed(FileBufferedChannel *_channel) { FileBufferedFdSinkChannel *channel = reinterpret_cast<FileBufferedFdSinkChannel *>(_channel); Client *client = static_cast<Client *>(static_cast< ServerKit::BaseClient *>(channel->getHooks()->userData)); Request *req = static_cast<Request *>(client->currentRequest); Controller *self = static_cast<Controller *>(getServerFromClient(client)); if (client->connected() && req != NULL) { self->outputBuffersFlushed(client, req); } } void Controller::outputBuffersFlushed(Client *client, Request *req) { if (!req->ended()) { assert(!req->appSource.isStarted()); SKC_TRACE(client, 2, "Buffered response data has been written to disk. Resuming application socket"); client->output.clearBuffersFlushedCallback(); req->appSource.start(); } } void Controller::_outputDataFlushed(FileBufferedChannel *_channel) { FileBufferedFdSinkChannel *channel = reinterpret_cast<FileBufferedFdSinkChannel *>(_channel); Client *client = static_cast<Client *>(static_cast< ServerKit::BaseClient *>(channel->getHooks()->userData)); Request *req = static_cast<Request *>(client->currentRequest); Controller *self = static_cast<Controller *>(getServerFromClient(client)); getClientOutputDataFlushedCallback()(_channel); if (client->connected() && req != NULL) { self->outputDataFlushed(client, req); } } void Controller::outputDataFlushed(Client *client, Request *req) { if (!req->ended()) { assert(!req->appSource.isStarted()); SKC_TRACE(client, 2, "The client is ready to receive more data. Resuming application socket"); client->output.setDataFlushedCallback(getClientOutputDataFlushedCallback()); req->appSource.start(); } } void Controller::handleAppResponseBodyEnd(Client *client, Request *req) { keepAliveAppConnection(client, req); storeAppResponseInTurboCache(client, req); assert(!req->ended()); } OXT_FORCE_INLINE void Controller::keepAliveAppConnection(Client *client, Request *req) { if (req->halfClosePolicy == Request::HALF_CLOSE_PERFORMED) { SKC_TRACE(client, 2, "Not keep-aliving application session connection" " because it had been half-closed before"); req->session->close(true, false); } else { // halfClosePolicy is initialized in sendHeaderToApp(). That method is // called immediately after checking out a session, before any events // from the appSource channel can be received. assert(req->halfClosePolicy != Request::HALF_CLOSE_POLICY_UNINITIALIZED); if (req->appResponse.wantKeepAlive) { SKC_TRACE(client, 2, "Keep-aliving application session connection"); req->session->close(true, true); } else { SKC_TRACE(client, 2, "Not keep-aliving application session connection" " because application did not allow it"); req->session->close(true, false); } } } void Controller::storeAppResponseInTurboCache(Client *client, Request *req) { if (turboCaching.isEnabled() && !req->cacheKey.empty()) { TRACE_POINT(); AppResponse *resp = &req->appResponse; unsigned int headerSize = 0; unsigned int i; for (i = 0; i < resp->nHeaderCacheBuffers; i++) { headerSize += resp->headerCacheBuffers[i].iov_len; } ResponseCache<Request>::Entry entry( turboCaching.responseCache.store(req, ev_now(getLoop()), headerSize, resp->bodyCacheBuffer.size)); if (entry.valid()) { UPDATE_TRACE_POINT(); SKC_DEBUG(client, "Storing app response in turbocache"); SKC_TRACE(client, 2, "Turbocache entries:\n" << turboCaching.responseCache.inspect()); gatherBuffers(entry.body->httpHeaderData, ResponseCache<Request>::MAX_HEADER_SIZE, resp->headerCacheBuffers, resp->nHeaderCacheBuffers); char *pos = entry.body->httpBodyData; const char *end = entry.body->httpBodyData + ResponseCache<Request>::MAX_BODY_SIZE; const LString::Part *part = resp->bodyCacheBuffer.start; while (part != NULL) { pos = appendData(pos, end, part->data, part->size); part = part->next; } } else { SKC_DEBUG(client, "Could not store app response for turbocaching"); } } } } // namespace Core } // namespace Passenger Controller/AppResponse.h 0000644 00000015067 14756456557 0011340 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2014-2017 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef _PASSENGER_APP_RESPONSE_H_ #define _PASSENGER_APP_RESPONSE_H_ #include <psg_sysqueue.h> #include <boost/cstdint.hpp> #include <boost/atomic.hpp> #include <sys/uio.h> #include <ServerKit/http_parser.h> #include <ServerKit/Hooks.h> #include <ServerKit/Client.h> #include <ServerKit/HeaderTable.h> #include <ServerKit/HttpHeaderParserState.h> #include <ServerKit/HttpChunkedBodyParserState.h> #include <MemoryKit/palloc.h> #include <DataStructures/LString.h> namespace Passenger { namespace Core { class HttpHeaderParser; class AppResponse { public: enum HttpState { /** The headers are still being parsed. */ PARSING_HEADERS, /** Internal state used by the parser. Users should never see this state. */ PARSED_HEADERS, /** The headers have been parsed, and there is no body. */ COMPLETE, /** The headers have been parsed, and we are now receiving/parsing the body, * whose length is specified by Content-Length. */ PARSING_BODY_WITH_LENGTH, /** The headers have been parsed, and we are now receiving/parsing the body, * which has the chunked transfer-encoding. */ PARSING_CHUNKED_BODY, /** The headers have been parsed, and we are now receiving/parsing the body, * which ends when EOF is encountered on the app socket. */ PARSING_BODY_UNTIL_EOF, /** The headers have been parsed, and the connection has been upgraded. */ UPGRADED, /** A 100-Continue status line has been encountered. */ ONEHUNDRED_CONTINUE, /** An error occurred. */ ERROR }; // Enum values are deliberately chosen so that hasRequestBody() can be branchless. enum BodyType { /** The message has no body. */ RBT_NO_BODY = 0, /** The connection has been upgraded. */ RBT_UPGRADE = 1, /** The message body's size is determined by the Content-Length header. */ RBT_CONTENT_LENGTH = 2, /** The message body's size is determined by the chunked Transfer-Encoding. */ RBT_CHUNKED = 4, /** The message body's size is equal to the stream's size. */ RBT_UNTIL_EOF = 8 }; boost::uint8_t httpMajor; boost::uint8_t httpMinor; HttpState httpState: 5; bool wantKeepAlive: 1; bool oneHundredContinueSent: 1; BodyType bodyType; boost::uint16_t statusCode; union { // If httpState == PARSING_HEADERS ServerKit::HttpHeaderParserState *headerParser; // If httpState == PARSING_CHUNKED_BODY ServerKit::HttpChunkedBodyParserState chunkedBodyParser; } parserState; ServerKit::HeaderTable headers; ServerKit::HeaderTable secureHeaders; union { /** Length of the message body. Only use when httpState != ERROR. */ union { // If bodyType == RBT_CONTENT_LENGTH. Guaranteed to be > 0. boost::uint64_t contentLength; // If bodyType == RBT_CHUNKED bool endChunkReached; // If bodyType == PARSING_BODY_UNTIL_EOF bool endReached; } bodyInfo; /** If a request parsing error occurred, the error code is stored here. * Only use if httpState == ERROR. */ int parseError; } aux; boost::uint64_t bodyAlreadyRead; LString *date; LString *setCookie; LString *cacheControl; LString *expiresHeader; LString *lastModifiedHeader; /* If the response is eligible for turbocaching, then the buffers * that contain the part of the response that can be cached, will be * stored here. */ struct iovec *headerCacheBuffers; unsigned int nHeaderCacheBuffers; /* If the response is eligible for turbocaching, then all response mbufs * will be stored here, so that we can store it in the response cache * at the end of the response. */ LString bodyCacheBuffer; AppResponse() : headers(16), secureHeaders(0), bodyAlreadyRead(0) { parserState.headerParser = NULL; aux.bodyInfo.contentLength = 0; // Sets the entire union to 0. } const char *getHttpStateString() const { switch (httpState) { case PARSING_HEADERS: return "PARSING_HEADERS"; case PARSED_HEADERS: return "PARSED_HEADERS"; case COMPLETE: return "COMPLETE"; case PARSING_BODY_WITH_LENGTH: return "PARSING_BODY_WITH_LENGTH"; case PARSING_CHUNKED_BODY: return "PARSING_CHUNKED_BODY"; case PARSING_BODY_UNTIL_EOF: return "PARSING_BODY_UNTIL_EOF"; case UPGRADED: return "UPGRADED"; case ONEHUNDRED_CONTINUE: return "ONEHUNDRED_CONTINUE"; case ERROR: return "ERROR"; default: return "UNKNOWN"; } } const char *getBodyTypeString() const { switch (bodyType) { case RBT_NO_BODY: return "NO_BODY"; case RBT_UPGRADE: return "UPGRADE"; case RBT_CONTENT_LENGTH: return "CONTENT_LENGTH"; case RBT_UNTIL_EOF: return "RBT_UNTIL_EOF"; case RBT_CHUNKED: return "CHUNKED"; default: return "UNKNOWN"; } } bool bodyFullyRead() const { switch (bodyType) { case RBT_NO_BODY: return true; case RBT_UPGRADE: return false; case RBT_CONTENT_LENGTH: return bodyAlreadyRead >= aux.bodyInfo.contentLength; case RBT_CHUNKED: return aux.bodyInfo.endChunkReached; case RBT_UNTIL_EOF: return aux.bodyInfo.endReached; default: return false; } } bool hasBody() const { return bodyType & (RBT_CONTENT_LENGTH | RBT_CHUNKED | RBT_UNTIL_EOF); } bool upgraded() const { return bodyType == RBT_UPGRADE; } bool begun() const { return (int) httpState >= COMPLETE; } bool canKeepAlive() const { return wantKeepAlive && bodyFullyRead(); } }; } // namespace Core } // namespace Passenger #endif /* _PASSENGER_APP_RESPONSE_H_ */ Controller/Hooks.cpp 0000644 00000023070 14756456557 0010510 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2011-2017 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include <Core/Controller.h> /************************************************************************* * * Hook functions for Core::Controller. This pertains the hooks that the * parent classes (ServerKit::HttpServer and ServerKit::Server) provide, * as well as hooks by libraries such as libev. * *************************************************************************/ namespace Passenger { namespace Core { using namespace std; using namespace boost; /**************************** * * Private methods * ****************************/ ServerKit::Channel::Result Controller::onBodyBufferData(Channel *_channel, const MemoryKit::mbuf &buffer, int errcode) { FileBufferedChannel *channel = reinterpret_cast<FileBufferedChannel *>(_channel); Request *req = static_cast<Request *>(static_cast< ServerKit::BaseHttpRequest *>(channel->getHooks()->userData)); Client *client = static_cast<Client *>(req->client); Controller *self = static_cast<Controller *>(getServerFromClient(client)); SKC_LOG_EVENT_FROM_STATIC(self, Controller, client, "onBodyBufferData"); assert(req->requestBodyBuffering); return self->whenSendingRequest_onRequestBody(client, req, buffer, errcode); } #ifdef DEBUG_CC_EVENT_LOOP_BLOCKING void Controller::onEventLoopPrepare(EV_P_ struct ev_prepare *w, int revents) { Controller *self = static_cast<Controller *>(w->data); ev_now_update(EV_A); self->timeBeforeBlocking = ev_now(EV_A); } #endif void Controller::onEventLoopCheck(EV_P_ struct ev_check *w, int revents) { Controller *self = static_cast<Controller *>(w->data); self->turboCaching.updateState(ev_now(EV_A)); #ifdef DEBUG_CC_EVENT_LOOP_BLOCKING self->reportLargeTimeDiff(NULL, "Event loop slept", self->timeBeforeBlocking, ev_now(EV_A)); #endif } /**************************** * * Protected methods * ****************************/ void Controller::onClientAccepted(Client *client) { ParentClass::onClientAccepted(client); client->connectedAt = ev_now(getLoop()); } void Controller::onRequestObjectCreated(Client *client, Request *req) { ParentClass::onRequestObjectCreated(client, req); req->appSink.setContext(getContext()); req->appSink.setHooks(&req->hooks); req->appSource.setContext(getContext()); req->appSource.setHooks(&req->hooks); req->appSource.setDataCallback(_onAppSourceData); req->bodyBuffer.setContext(getContext()); req->bodyBuffer.setHooks(&req->hooks); req->bodyBuffer.setDataCallback(onBodyBufferData); } void Controller::deinitializeClient(Client *client) { ParentClass::deinitializeClient(client); client->output.clearBuffersFlushedCallback(); client->output.setDataFlushedCallback(getClientOutputDataFlushedCallback()); } void Controller::reinitializeRequest(Client *client, Request *req) { ParentClass::reinitializeRequest(client, req); // bodyBuffer is initialized in Controller::beginBufferingBody(). // appSink and appSource are initialized in Controller::checkoutSession(). req->startedAt = 0; req->state = Request::ANALYZING_REQUEST; req->dechunkResponse = false; req->requestBodyBuffering = false; req->https = false; req->stickySession = false; req->sessionCheckoutTry = 0; req->halfClosePolicy = Request::HALF_CLOSE_POLICY_UNINITIALIZED; req->appResponseInitialized = false; req->strip100ContinueHeader = false; req->hasPragmaHeader = false; req->host = NULL; req->config = requestConfig; req->bodyBytesBuffered = 0; req->cacheKey = HashedStaticString(); req->cacheControl = NULL; req->varyCookie = NULL; req->envvars = NULL; #ifdef DEBUG_CC_EVENT_LOOP_BLOCKING req->timedAppPoolGet = false; req->timeBeforeAccessingApplicationPool = 0; req->timeOnRequestHeaderSent = 0; req->timeOnResponseBegun = 0; #endif /***************/ } void Controller::deinitializeRequest(Client *client, Request *req) { req->session.reset(); req->config.reset(); req->appSink.setConsumedCallback(NULL); req->appSink.deinitialize(); req->appSource.deinitialize(); req->bodyBuffer.clearBuffersFlushedCallback(); req->bodyBuffer.deinitialize(); /***************/ /***************/ if (req->appResponseInitialized) { deinitializeAppResponse(client, req); } ParentClass::deinitializeRequest(client, req); } void Controller::reinitializeAppResponse(Client *client, Request *req) { AppResponse *resp = &req->appResponse; req->appResponseInitialized = true; resp->httpMajor = 1; resp->httpMinor = 0; resp->httpState = AppResponse::PARSING_HEADERS; resp->bodyType = AppResponse::RBT_NO_BODY; resp->wantKeepAlive = false; resp->oneHundredContinueSent = false; resp->statusCode = 0; resp->parserState.headerParser = getHeaderParserStatePool().construct(); createAppResponseHeaderParser(getContext(), req).initialize(); resp->aux.bodyInfo.contentLength = 0; // Sets the entire union to 0. resp->bodyAlreadyRead = 0; resp->date = NULL; resp->setCookie = NULL; resp->cacheControl = NULL; resp->expiresHeader = NULL; resp->lastModifiedHeader = NULL; resp->headerCacheBuffers = NULL; resp->nHeaderCacheBuffers = 0; psg_lstr_init(&resp->bodyCacheBuffer); } void Controller::deinitializeAppResponse(Client *client, Request *req) { AppResponse *resp = &req->appResponse; req->appResponseInitialized = false; if (resp->httpState == AppResponse::PARSING_HEADERS && resp->parserState.headerParser != NULL) { getHeaderParserStatePool().destroy(resp->parserState.headerParser); resp->parserState.headerParser = NULL; } ServerKit::HeaderTable::Iterator it(resp->headers); while (*it != NULL) { psg_lstr_deinit(&it->header->key); psg_lstr_deinit(&it->header->origKey); psg_lstr_deinit(&it->header->val); it.next(); } it = ServerKit::HeaderTable::Iterator(resp->secureHeaders); while (*it != NULL) { psg_lstr_deinit(&it->header->key); psg_lstr_deinit(&it->header->origKey); psg_lstr_deinit(&it->header->val); it.next(); } resp->headers.clear(); resp->secureHeaders.clear(); if (resp->setCookie != NULL) { psg_lstr_deinit(resp->setCookie); } psg_lstr_deinit(&resp->bodyCacheBuffer); } ServerKit::Channel::Result Controller::onRequestBody(Client *client, Request *req, const MemoryKit::mbuf &buffer, int errcode) { switch (req->state) { case Request::BUFFERING_REQUEST_BODY: return whenBufferingBody_onRequestBody(client, req, buffer, errcode); case Request::FORWARDING_BODY_TO_APP: return whenSendingRequest_onRequestBody(client, req, buffer, errcode); default: P_BUG("Unknown state " << req->state); return Channel::Result(0, false); } } void Controller::onNextRequestEarlyReadError(Client *client, Request *req, int errcode) { ParentClass::onNextRequestEarlyReadError(client, req, errcode); if (req->halfClosePolicy == Request::HALF_CLOSE_UPON_NEXT_REQUEST_EARLY_READ_ERROR) { SKC_TRACE(client, 3, "Half-closing application socket with SHUT_WR" " because the next request's early read error has been detected: " << ServerKit::getErrorDesc(errcode) << " (errno=" << errcode << ")"); req->halfClosePolicy = Request::HALF_CLOSE_PERFORMED; assert(req->session != NULL); ::shutdown(req->session->fd(), SHUT_WR); } } bool Controller::shouldDisconnectClientOnShutdown(Client *client) { return ParentClass::shouldDisconnectClientOnShutdown(client) || !mainConfig.gracefulExit; } bool Controller::shouldAutoDechunkBody(Client *client, Request *req) { // When buffering the body, we'll want to buffer the dechunked data, // (and when passing the request to the app we'll also add Content-Length // and remove Transfer-Encoding) so turn auto-dechunking on in that case. // // Otherwise we'll want to disable auto-dechunking because we'll // pass the raw chunked body to the app. return req->requestBodyBuffering; } bool Controller::supportsUpgrade(Client *client, Request *req) { return true; } /**************************** * * Public methods * ****************************/ unsigned int Controller::getClientName(const Client *client, char *buf, size_t size) const { char *pos = buf; const char *end = buf + size - 1; // WARNING: If you change the format, be sure to change // ApiServer::extractThreadNumberFromClientName() too. pos += uintToString(mainConfig.threadNumber, pos, end - pos); pos = appendData(pos, end, "-", 1); pos += uintToString(client->number, pos, end - pos); *pos = '\0'; return pos - buf; } StaticString Controller::getServerName() const { return mainConfig.serverLogName; } } // namespace Core } // namespace Passenger Controller/TurboCaching.h 0000644 00000022053 14756456557 0011442 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2014-2018 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef _PASSENGER_TURBO_CACHING_H_ #define _PASSENGER_TURBO_CACHING_H_ #include <oxt/backtrace.hpp> #include <ev++.h> #include <ctime> #include <cstddef> #include <cassert> #include <MemoryKit/mbuf.h> #include <ServerKit/Context.h> #include <Constants.h> #include <LoggingKit/LoggingKit.h> #include <StrIntTools/StrIntUtils.h> #include <Core/ResponseCache.h> namespace Passenger { namespace Core { using namespace std; template<typename Request> class TurboCaching { public: /** The interval of the timer while we're in the ENABLED state. */ static const unsigned int ENABLED_TIMEOUT = 2; /** The interval of the timer while we're in the TEMPORARILY_DISABLED state. */ static const unsigned int TEMPORARY_DISABLE_TIMEOUT = 10; /** Only consider temporarily disabling turbocaching if the number of * fetches/stores in the current interval have reached these thresholds. */ static const unsigned int FETCH_THRESHOLD = 20; static const unsigned int STORE_THRESHOLD = 20; OXT_FORCE_INLINE static double MIN_HIT_RATIO() { return 0.5; } OXT_FORCE_INLINE static double MIN_STORE_SUCCESS_RATIO() { return 0.5; } enum State { /** * Turbocaching is permanently disabled. */ DISABLED, /** * Turbocaching is enabled. */ ENABLED, /** * In case turbocaching is enabled, and poor cache hit ratio * is detected, this state will be entered. It will stay * in this state for TEMPORARY_DISABLE_TIMEOUT seconds before * transitioning back to ENABLED. */ TEMPORARILY_DISABLED }; typedef ResponseCache<Request> ResponseCacheType; typedef typename ResponseCache<Request>::Entry ResponseCacheEntryType; private: State state; ev_tstamp lastTimeout, nextTimeout; struct ResponsePreparation { Request *req; const ResponseCacheEntryType *entry; time_t now; time_t age; unsigned int ageValueSize; unsigned int contentLengthStrSize; bool showVersionInHeader; }; template<typename Server> void prepareResponseHeader(ResponsePreparation &prep, Server *server, Request *req, const ResponseCacheEntryType &entry) { prep.req = req; prep.entry = &entry; prep.now = (time_t) ev_now(server->getLoop()); if (prep.now >= entry.header->date) { prep.age = prep.now - entry.header->date; } else { prep.age = 0; } prep.ageValueSize = integerSizeInOtherBase<time_t, 10>(prep.age); prep.contentLengthStrSize = uintSizeAsString(entry.body->httpBodySize); prep.showVersionInHeader = req->config->showVersionInHeader; } template<typename Server> unsigned int buildResponseHeader(const ResponsePreparation &prep, Server *server, char *output, unsigned int outputSize) { #define PUSH_STATIC_STRING(str) \ do { \ result += sizeof(str) - 1; \ if (output != NULL) { \ pos = appendData(pos, end, str, sizeof(str) - 1); \ } \ } while (false) const ResponseCacheEntryType *entry = prep.entry; Request *req = prep.req; unsigned int httpVersion = req->httpMajor * 1000 + req->httpMinor * 10; unsigned int result = 0; char *pos = output; const char *end = output + outputSize; result += entry->body->httpHeaderSize; if (output != NULL) { pos = appendData(pos, end, entry->body->httpHeaderData, entry->body->httpHeaderSize); } PUSH_STATIC_STRING("Content-Length: "); result += prep.contentLengthStrSize; if (output != NULL) { uintToString(entry->body->httpBodySize, pos, end - pos); pos += prep.contentLengthStrSize; } PUSH_STATIC_STRING("\r\n"); PUSH_STATIC_STRING("Age: "); result += prep.ageValueSize; if (output != NULL) { integerToOtherBase<time_t, 10>(prep.age, pos, end - pos); pos += prep.ageValueSize; } PUSH_STATIC_STRING("\r\n"); if (prep.showVersionInHeader) { PUSH_STATIC_STRING("X-Powered-By: " PROGRAM_NAME " " PASSENGER_VERSION "\r\n"); } else { PUSH_STATIC_STRING("X-Powered-By: " PROGRAM_NAME "\r\n"); } if (server->canKeepAlive(req)) { if (httpVersion < 1010) { // HTTP < 1.1 defaults to "Connection: close", but we want keep-alive PUSH_STATIC_STRING("Connection: keep-alive\r\n"); } } else { if (httpVersion >= 1010) { // HTTP 1.1 defaults to "Connection: keep-alive", but we don't want it PUSH_STATIC_STRING("Connection: close\r\n"); } } PUSH_STATIC_STRING("\r\n"); #ifndef NDEBUG if (output != NULL) { assert(size_t(pos - output) == size_t(result)); assert(size_t(pos - output) <= size_t(outputSize)); } #endif return result; #undef PUSH_STATIC_STRING } public: ResponseCache<Request> responseCache; TurboCaching() : state(ENABLED), lastTimeout(0), nextTimeout(0) { } void initialize(bool initiallyEnabled) { state = initiallyEnabled ? ENABLED : DISABLED; lastTimeout = (ev_tstamp) time(NULL); nextTimeout = (ev_tstamp) time(NULL) + ENABLED_TIMEOUT; } bool isEnabled() const { return state == ENABLED; } // Call when the event loop multiplexer returns. void updateState(ev_tstamp now) { if (OXT_UNLIKELY(state == DISABLED)) { return; } if (OXT_LIKELY(now < nextTimeout)) { return; } switch (state) { case ENABLED: if (responseCache.getFetches() >= FETCH_THRESHOLD && responseCache.getHitRatio() < MIN_HIT_RATIO()) { P_INFO("Poor turbocaching hit ratio detected (" << responseCache.getHits() << " hits, " << responseCache.getFetches() << " fetches, " << (int) (responseCache.getHitRatio() * 100) << "%). Temporarily disabling turbocaching " "for " << TEMPORARY_DISABLE_TIMEOUT << " seconds"); state = TEMPORARILY_DISABLED; nextTimeout = now + TEMPORARY_DISABLE_TIMEOUT; } else if (responseCache.getStores() >= STORE_THRESHOLD && responseCache.getStoreSuccessRatio() < MIN_STORE_SUCCESS_RATIO()) { P_INFO("Poor turbocaching store success ratio detected (" << responseCache.getStoreSuccesses() << " store successes, " << responseCache.getStores() << " stores, " << (int) (responseCache.getStoreSuccessRatio() * 100) << "%). Temporarily disabling turbocaching " "for " << TEMPORARY_DISABLE_TIMEOUT << " seconds"); state = TEMPORARILY_DISABLED; nextTimeout = now + TEMPORARY_DISABLE_TIMEOUT; } else { P_DEBUG("Clearing turbocache"); nextTimeout = now + ENABLED_TIMEOUT; } responseCache.resetStatistics(); responseCache.clear(); break; case TEMPORARILY_DISABLED: P_INFO("Re-enabling turbocaching"); state = ENABLED; nextTimeout = now + ENABLED_TIMEOUT; break; default: P_BUG("Unknown state " << (int) state); break; } lastTimeout = now; } template<typename Server, typename Client> void writeResponse(Server *server, Client *client, Request *req, ResponseCacheEntryType &entry) { MemoryKit::mbuf_pool &mbuf_pool = server->getContext()->mbuf_pool; const unsigned int MBUF_MAX_SIZE = mbuf_pool_data_size(&mbuf_pool); ResponsePreparation prep; unsigned int headerSize; prepareResponseHeader(prep, server, req, entry); headerSize = buildResponseHeader(prep, server, NULL, 0); if (headerSize + entry.body->httpBodySize <= MBUF_MAX_SIZE) { // Header and body fit inside a single mbuf MemoryKit::mbuf buffer(MemoryKit::mbuf_get(&mbuf_pool)); buffer = MemoryKit::mbuf(buffer, 0, headerSize + entry.body->httpBodySize); buildResponseHeader(prep, server, buffer.start, buffer.size()); memcpy(buffer.start + headerSize, entry.body->httpBodyData, entry.body->httpBodySize); server->writeResponse(client, buffer); } else { char *buffer = (char *) psg_pnalloc(req->pool, headerSize + entry.body->httpBodySize); buildResponseHeader(prep, server, buffer, headerSize + entry.body->httpBodySize); memcpy(buffer + headerSize, entry.body->httpBodyData, entry.body->httpBodySize); server->writeResponse(client, buffer, headerSize + entry.body->httpBodySize); } } }; } // namespace Core } // namespace Passenger #endif /* _PASSENGER_TURBO_CACHING_H_ */ Controller/InitRequest.cpp 0000644 00000042722 14756456557 0011706 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2011-2018 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include <Core/Controller.h> #include <AppTypeDetector/Detector.h> /************************************************************************* * * Implements Core::Controller methods pertaining the initialization * of a request. * *************************************************************************/ namespace Passenger { namespace Core { using namespace std; using namespace boost; /**************************** * * Private methods * ****************************/ struct Controller::RequestAnalysis { const LString *flags; ServerKit::HeaderTable::Cell *appGroupNameCell; }; void Controller::initializeFlags(Client *client, Request *req, RequestAnalysis &analysis) { if (analysis.flags != NULL) { const LString::Part *part = analysis.flags->start; while (part != NULL) { const char *data = part->data; const char *end = part->data + part->size; while (data < end) { switch (*data) { case 'D': req->dechunkResponse = true; break; case 'B': req->requestBodyBuffering = true; break; case 'S': req->https = true; break; case 'C': req->strip100ContinueHeader = true; break; default: break; } data++; } part = part->next; } if (OXT_UNLIKELY(LoggingKit::getLevel() >= LoggingKit::DEBUG2)) { if (req->dechunkResponse) { SKC_TRACE(client, 2, "Dechunk flag detected"); } if (req->requestBodyBuffering) { SKC_TRACE(client, 2, "Request body buffering enabled"); } if (req->https) { SKC_TRACE(client, 2, "HTTPS flag detected"); } if (req->strip100ContinueHeader) { SKC_TRACE(client, 2, "Stripping 100 Continue header"); } } } } bool Controller::respondFromTurboCache(Client *client, Request *req) { if (!turboCaching.isEnabled() || !turboCaching.responseCache.prepareRequest(this, req)) { return false; } SKC_TRACE(client, 2, "Turbocaching: trying to reply from cache (key \"" << cEscapeString(req->cacheKey) << "\")"); SKC_TRACE(client, 2, "Turbocache entries:\n" << turboCaching.responseCache.inspect()); if (turboCaching.responseCache.requestAllowsFetching(req)) { ResponseCache<Request>::Entry entry(turboCaching.responseCache.fetch(req, ev_now(getLoop()))); if (entry.valid()) { SKC_TRACE(client, 2, "Turbocaching: cache hit (key \"" << cEscapeString(req->cacheKey) << "\")"); turboCaching.writeResponse(this, client, req, entry); if (!req->ended()) { endRequest(&client, &req); } return true; } else { SKC_TRACE(client, 2, "Turbocaching: cache miss: " << entry.getCacheMissReasonString() << " (key \"" << cEscapeString(req->cacheKey) << "\")"); return false; } } else { SKC_TRACE(client, 2, "Turbocaching: request not eligible for caching"); return false; } } void Controller::initializePoolOptions(Client *client, Request *req, RequestAnalysis &analysis) { boost::shared_ptr<Options> *options; if (mainConfig.singleAppMode) { P_ASSERT_EQ(poolOptionsCache.size(), 1); poolOptionsCache.lookupRandom(NULL, &options); req->options = **options; } else { ServerKit::HeaderTable::Cell *appGroupNameCell = analysis.appGroupNameCell; if (appGroupNameCell != NULL && appGroupNameCell->header->val.size > 0) { const LString *appGroupName = psg_lstr_make_contiguous( &appGroupNameCell->header->val, req->pool); HashedStaticString hAppGroupName(appGroupName->start->data, appGroupName->size); poolOptionsCache.lookup(hAppGroupName, &options); if (options != NULL) { req->options = **options; fillPoolOption(req, req->options.baseURI, "!~SCRIPT_NAME"); } else { createNewPoolOptions(client, req, hAppGroupName); } } else { disconnectWithError(&client, "the !~PASSENGER_APP_GROUP_NAME header must be set"); } } if (!req->ended()) { // See comment for req->envvars to learn how it is different // from req->options.environmentVariables. req->envvars = req->secureHeaders.lookup(PASSENGER_ENV_VARS); if (req->envvars != NULL && req->envvars->size > 0) { req->envvars = psg_lstr_make_contiguous(req->envvars, req->pool); req->options.environmentVariables = StaticString( req->envvars->start->data, req->envvars->size); } // Allow certain options to be overridden on a per-request basis fillPoolOption(req, req->options.maxRequests, PASSENGER_MAX_REQUESTS); } } void Controller::fillPoolOptionsFromConfigCaches(Options &options, psg_pool_t *pool, const ControllerRequestConfigPtr &requestConfig) { options.ruby = requestConfig->defaultRuby; options.nodejs = requestConfig->defaultNodejs; options.python = requestConfig->defaultPython; options.meteorAppSettings = requestConfig->defaultMeteorAppSettings; options.fileDescriptorUlimit = requestConfig->defaultAppFileDescriptorUlimit; options.logLevel = int(LoggingKit::getLevel()); options.integrationMode = psg_pstrdup(pool, mainConfig.integrationMode); options.userSwitching = mainConfig.userSwitching; options.defaultUser = requestConfig->defaultUser; options.defaultGroup = requestConfig->defaultGroup; options.minProcesses = requestConfig->defaultMinInstances; options.maxPreloaderIdleTime = requestConfig->defaultMaxPreloaderIdleTime; options.maxRequestQueueSize = requestConfig->defaultMaxRequestQueueSize; options.abortWebsocketsOnProcessShutdown = requestConfig->defaultAbortWebsocketsOnProcessShutdown; options.forceMaxConcurrentRequestsPerProcess = requestConfig->defaultForceMaxConcurrentRequestsPerProcess; options.environment = requestConfig->defaultEnvironment; options.spawnMethod = requestConfig->defaultSpawnMethod; options.bindAddress = requestConfig->defaultBindAddress; options.loadShellEnvvars = requestConfig->defaultLoadShellEnvvars; options.preloadBundler = requestConfig->defaultPreloadBundler; options.statThrottleRate = mainConfig.statThrottleRate; options.maxRequests = requestConfig->defaultMaxRequests; options.stickySessionsCookieAttributes = requestConfig->defaultStickySessionsCookieAttributes; /******************************/ } void Controller::fillPoolOption(Request *req, StaticString &field, const HashedStaticString &name) { const LString *value = req->secureHeaders.lookup(name); if (value != NULL && value->size > 0) { value = psg_lstr_make_contiguous(value, req->pool); field = StaticString(value->start->data, value->size); } } void Controller::fillPoolOption(Request *req, bool &field, const HashedStaticString &name) { const LString *value = req->secureHeaders.lookup(name); if (value != NULL && value->size > 0) { field = psg_lstr_first_byte(value) == 't'; } } void Controller::fillPoolOption(Request *req, int &field, const HashedStaticString &name) { const LString *value = req->secureHeaders.lookup(name); if (value != NULL && value->size > 0) { value = psg_lstr_make_contiguous(value, req->pool); field = stringToInt(StaticString(value->start->data, value->size)); } } void Controller::fillPoolOption(Request *req, unsigned int &field, const HashedStaticString &name) { const LString *value = req->secureHeaders.lookup(name); if (value != NULL && value->size > 0) { value = psg_lstr_make_contiguous(value, req->pool); field = stringToUint(StaticString(value->start->data, value->size)); } } void Controller::fillPoolOption(Request *req, unsigned long &field, const HashedStaticString &name) { const LString *value = req->secureHeaders.lookup(name); if (value != NULL && value->size > 0) { value = psg_lstr_make_contiguous(value, req->pool); field = stringToUint(StaticString(value->start->data, value->size)); } } void Controller::fillPoolOption(Request *req, long &field, const HashedStaticString &name) { const LString *value = req->secureHeaders.lookup(name); if (value != NULL && value->size > 0) { value = psg_lstr_make_contiguous(value, req->pool); field = stringToInt(StaticString(value->start->data, value->size)); } } void Controller::fillPoolOptionSecToMsec(Request *req, unsigned int &field, const HashedStaticString &name) { const LString *value = req->secureHeaders.lookup(name); if (value != NULL && value->size > 0) { value = psg_lstr_make_contiguous(value, req->pool); field = stringToInt(StaticString(value->start->data, value->size)) * 1000; } } void Controller::createNewPoolOptions(Client *client, Request *req, const HashedStaticString &appGroupName) { ServerKit::HeaderTable &secureHeaders = req->secureHeaders; Options &options = req->options; SKC_TRACE(client, 2, "Creating new pool options: app group name=" << appGroupName); options = Options(); const LString *scriptName = secureHeaders.lookup("!~SCRIPT_NAME"); const LString *appRoot = secureHeaders.lookup("!~PASSENGER_APP_ROOT"); if (scriptName == NULL || scriptName->size == 0) { if (appRoot == NULL || appRoot->size == 0) { const LString *documentRoot = secureHeaders.lookup("!~DOCUMENT_ROOT"); if (OXT_UNLIKELY(documentRoot == NULL || documentRoot->size == 0)) { disconnectWithError(&client, "client did not send a !~PASSENGER_APP_ROOT or a !~DOCUMENT_ROOT header"); return; } documentRoot = psg_lstr_make_contiguous(documentRoot, req->pool); appRoot = psg_lstr_create(req->pool, extractDirNameStatic(StaticString(documentRoot->start->data, documentRoot->size))); } else { appRoot = psg_lstr_make_contiguous(appRoot, req->pool); } options.appRoot = HashedStaticString(appRoot->start->data, appRoot->size); } else { if (appRoot == NULL || appRoot->size == 0) { const LString *documentRoot = secureHeaders.lookup("!~DOCUMENT_ROOT"); if (OXT_UNLIKELY(documentRoot == NULL || documentRoot->size == 0)) { disconnectWithError(&client, "client did not send a !~DOCUMENT_ROOT header"); return; } documentRoot = psg_lstr_null_terminate(documentRoot, req->pool); documentRoot = resolveSymlink(StaticString(documentRoot->start->data, documentRoot->size), req->pool); appRoot = psg_lstr_create(req->pool, extractDirNameStatic(StaticString(documentRoot->start->data, documentRoot->size))); } else { appRoot = psg_lstr_make_contiguous(appRoot, req->pool); } options.appRoot = HashedStaticString(appRoot->start->data, appRoot->size); scriptName = psg_lstr_make_contiguous(scriptName, req->pool); options.baseURI = StaticString(scriptName->start->data, scriptName->size); } fillPoolOptionsFromConfigCaches(options, req->pool, req->config); const LString *appType = secureHeaders.lookup("!~PASSENGER_APP_TYPE"); if (appType == NULL || appType->size == 0) { const LString *appStartCommand = secureHeaders.lookup("!~PASSENGER_APP_START_COMMAND"); if (appStartCommand == NULL || appStartCommand->size == 0) { AppTypeDetector::Detector detector(*wrapperRegistry); AppTypeDetector::Detector::Result result = detector.checkAppRoot(options.appRoot); if (result.isNull()) { disconnectWithError(&client, "client did not send a recognized !~PASSENGER_APP_TYPE header"); return; } options.appType = result.wrapperRegistryEntry->language; } else { fillPoolOption(req, options.appStartCommand, "!~PASSENGER_APP_START_COMMAND"); } } else { fillPoolOption(req, options.appType, "!~PASSENGER_APP_TYPE"); } options.appGroupName = appGroupName; fillPoolOption(req, options.appLogFile, "!~PASSENGER_APP_LOG_FILE"); fillPoolOption(req, options.environment, "!~PASSENGER_APP_ENV"); fillPoolOption(req, options.ruby, "!~PASSENGER_RUBY"); fillPoolOption(req, options.python, "!~PASSENGER_PYTHON"); fillPoolOption(req, options.nodejs, "!~PASSENGER_NODEJS"); fillPoolOption(req, options.meteorAppSettings, "!~PASSENGER_METEOR_APP_SETTINGS"); fillPoolOption(req, options.user, "!~PASSENGER_USER"); fillPoolOption(req, options.group, "!~PASSENGER_GROUP"); fillPoolOption(req, options.minProcesses, "!~PASSENGER_MIN_PROCESSES"); fillPoolOption(req, options.spawnMethod, "!~PASSENGER_SPAWN_METHOD"); fillPoolOption(req, options.bindAddress, "!~PASSENGER_DIRECT_INSTANCE_REQUEST_ADDRESS"); fillPoolOption(req, options.appStartCommand, "!~PASSENGER_APP_START_COMMAND"); fillPoolOptionSecToMsec(req, options.startTimeout, "!~PASSENGER_START_TIMEOUT"); fillPoolOption(req, options.maxPreloaderIdleTime, "!~PASSENGER_MAX_PRELOADER_IDLE_TIME"); fillPoolOption(req, options.maxRequestQueueSize, "!~PASSENGER_MAX_REQUEST_QUEUE_SIZE"); fillPoolOption(req, options.abortWebsocketsOnProcessShutdown, "!~PASSENGER_ABORT_WEBSOCKETS_ON_PROCESS_SHUTDOWN"); fillPoolOption(req, options.forceMaxConcurrentRequestsPerProcess, "!~PASSENGER_FORCE_MAX_CONCURRENT_REQUESTS_PER_PROCESS"); fillPoolOption(req, options.restartDir, "!~PASSENGER_RESTART_DIR"); fillPoolOption(req, options.startupFile, "!~PASSENGER_STARTUP_FILE"); fillPoolOption(req, options.loadShellEnvvars, "!~PASSENGER_LOAD_SHELL_ENVVARS"); fillPoolOption(req, options.preloadBundler, "!~PASSENGER_PRELOAD_BUNDLER"); fillPoolOption(req, options.fileDescriptorUlimit, "!~PASSENGER_APP_FILE_DESCRIPTOR_ULIMIT"); fillPoolOption(req, options.raiseInternalError, "!~PASSENGER_RAISE_INTERNAL_ERROR"); fillPoolOption(req, options.lveMinUid, "!~PASSENGER_LVE_MIN_UID"); fillPoolOption(req, options.stickySessionsCookieAttributes, "!~PASSENGER_STICKY_SESSIONS_COOKIE_ATTRIBUTES"); // maxProcesses is configured per-application by the (Enterprise) maxInstances option (and thus passed // via request headers). In OSS the max processes can also be configured, but on a global level // (i.e. the same for all apps) using the maxInstancesPerApp option. As an easy implementation shortcut // we apply maxInstancesPerApp to options.maxProcesses (which can be overridden by Enterprise). options.maxProcesses = mainConfig.maxInstancesPerApp; /******************/ boost::shared_ptr<Options> optionsCopy = boost::make_shared<Options>(options); optionsCopy->persist(options); optionsCopy->clearPerRequestFields(); poolOptionsCache.insert(options.getAppGroupName(), optionsCopy); } void Controller::setStickySessionId(Client *client, Request *req) { if (req->stickySession) { // TODO: This is not entirely correct. Clients MAY send multiple Cookie // headers, although this is in practice extremely rare. // http://stackoverflow.com/questions/16305814/are-multiple-cookie-headers-allowed-in-an-http-request const LString *cookieHeader = req->headers.lookup(HTTP_COOKIE); if (cookieHeader != NULL && cookieHeader->size > 0) { const LString *cookieName = getStickySessionCookieName(req); vector< pair<StaticString, StaticString> > cookies; pair<StaticString, StaticString> cookie; parseCookieHeader(req->pool, cookieHeader, cookies); foreach (cookie, cookies) { if (psg_lstr_cmp(cookieName, cookie.first)) { // This cookie matches the one we're looking for. req->options.stickySessionId = stringToUint(cookie.second); return; } } } } } const LString * Controller::getStickySessionCookieName(Request *req) { const LString *value = req->headers.lookup(PASSENGER_STICKY_SESSIONS_COOKIE_NAME); if (value == NULL || value->size == 0) { return psg_lstr_create(req->pool, req->config->defaultStickySessionsCookieName); } else { return value; } } /**************************** * * Protected methods * ****************************/ void Controller::onRequestBegin(Client *client, Request *req) { ParentClass::onRequestBegin(client, req); CC_BENCHMARK_POINT(client, req, BM_AFTER_ACCEPT); { // Perform hash table operations as close to header parsing as possible, // and localize them as much as possible, for better CPU caching. RequestAnalysis analysis; analysis.flags = req->secureHeaders.lookup(FLAGS); analysis.appGroupNameCell = mainConfig.singleAppMode ? NULL : req->secureHeaders.lookupCell(PASSENGER_APP_GROUP_NAME); req->stickySession = getBoolOption(req, PASSENGER_STICKY_SESSIONS, mainConfig.defaultStickySessions); req->host = req->headers.lookup(HTTP_HOST); /***************/ /***************/ SKC_TRACE(client, 2, "Initiating request"); req->startedAt = ev_now(getLoop()); req->bodyChannel.stop(); initializeFlags(client, req, analysis); if (respondFromTurboCache(client, req)) { return; } initializePoolOptions(client, req, analysis); if (req->ended()) { return; } if (req->ended()) { return; } setStickySessionId(client, req); } if (!req->hasBody() || !req->requestBodyBuffering) { req->requestBodyBuffering = false; checkoutSession(client, req); } else { beginBufferingBody(client, req); } } } // namespace Core } // namespace Passenger Controller/SendRequest.cpp 0000644 00000106001 14756456557 0011663 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2011-2018 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include <Core/Controller.h> #include <SystemTools/SystemTime.h> /************************************************************************* * * Implements Core::Controller methods pertaining sending request data * to a selected application process. This happens in parallel to forwarding * application response data to the client. * *************************************************************************/ namespace Passenger { namespace Core { using namespace std; using namespace boost; /**************************** * * Private methods * ****************************/ struct Controller::SessionProtocolWorkingState { StaticString path; StaticString queryString; StaticString methodStr; StaticString serverName; StaticString serverPort; const LString *remoteAddr; const LString *remotePort; const LString *remoteUser; const LString *contentType; const LString *contentLength; char *environmentVariablesData; size_t environmentVariablesSize; bool hasBaseURI; SessionProtocolWorkingState() : environmentVariablesData(NULL) { } ~SessionProtocolWorkingState() { free(environmentVariablesData); } }; struct Controller::HttpHeaderConstructionCache { StaticString methodStr; const LString *remoteAddr; const LString *setCookie; bool cached; }; void Controller::sendHeaderToApp(Client *client, Request *req) { TRACE_POINT(); SKC_TRACE(client, 2, "Sending headers to application with " << req->session->getProtocol() << " protocol"); req->state = Request::SENDING_HEADER_TO_APP; P_ASSERT_EQ(req->halfClosePolicy, Request::HALF_CLOSE_POLICY_UNINITIALIZED); if (req->session->getProtocol() == "session") { UPDATE_TRACE_POINT(); if (req->bodyType == Request::RBT_NO_BODY) { // When there is no request body we will try to keep-alive the // application connection, so half-close the application // connection upon encountering the next request's early error // in order not to break the keep-alive. req->halfClosePolicy = Request::HALF_CLOSE_UPON_NEXT_REQUEST_EARLY_READ_ERROR; } else { // When there is a request body we won't try to keep-alive // the application connection, so it's safe to half-close immediately // upon reaching the end of the request body. req->halfClosePolicy = Request::HALF_CLOSE_UPON_REACHING_REQUEST_BODY_END; } sendHeaderToAppWithSessionProtocol(client, req); } else { UPDATE_TRACE_POINT(); if (req->bodyType == Request::RBT_UPGRADE) { req->halfClosePolicy = Request::HALF_CLOSE_UPON_REACHING_REQUEST_BODY_END; } else { // HTTP does not formally support half-closing. Some apps support // HTTP with half-closing, others (such as Node.js http.Server with // default settings) treat a half-close as a full close. Furthermore, // we always try to keep-alive the application connection. // // So we can't half-close immediately upon reaching the end of the // request body. The app might not have yet sent a response by then. // We only half-close upon the next request's early error. req->halfClosePolicy = Request::HALF_CLOSE_UPON_NEXT_REQUEST_EARLY_READ_ERROR; } sendHeaderToAppWithHttpProtocol(client, req); } UPDATE_TRACE_POINT(); if (!req->ended()) { if (req->appSink.acceptingInput()) { UPDATE_TRACE_POINT(); sendBodyToApp(client, req); if (!req->ended()) { req->appSource.startReading(); } } else if (req->appSink.mayAcceptInputLater()) { UPDATE_TRACE_POINT(); SKC_TRACE(client, 3, "Waiting for appSink channel to become " "idle before sending body to application"); req->appSink.setConsumedCallback(sendBodyToAppWhenAppSinkIdle); req->appSource.startReading(); } else { // Either we're done feeding to req->appSink, or req->appSink.feed() // encountered an error while writing to the application socket. // But we don't care about either scenarios; we just care that // ForwardResponse.cpp will now forward the response data and end the // request when it's done. UPDATE_TRACE_POINT(); assert(req->appSink.ended() || req->appSink.hasError()); logAppSocketWriteError(client, req->appSink.getErrcode()); req->state = Request::WAITING_FOR_APP_OUTPUT; req->appSource.startReading(); } } } void Controller::sendHeaderToAppWithSessionProtocol(Client *client, Request *req) { TRACE_POINT(); SessionProtocolWorkingState state; // Workaround for Ruby < 2.1 support. std::string deltaMonotonic; unsigned long long now = SystemTime::getUsec(); MonotonicTimeUsec monotonicNow = SystemTime::getMonotonicUsec(); if (now > monotonicNow) { deltaMonotonic = boost::to_string(now - monotonicNow); } else { long long diff = monotonicNow - now; deltaMonotonic = boost::to_string(-diff); } unsigned int bufferSize = determineMaxHeaderSizeForSessionProtocol(req, state, deltaMonotonic); MemoryKit::mbuf_pool &mbuf_pool = getContext()->mbuf_pool; const unsigned int MBUF_MAX_SIZE = mbuf_pool_data_size(&mbuf_pool); bool ok; if (bufferSize <= MBUF_MAX_SIZE) { MemoryKit::mbuf buffer(MemoryKit::mbuf_get(&mbuf_pool)); bufferSize = MBUF_MAX_SIZE; ok = constructHeaderForSessionProtocol(req, buffer.start, bufferSize, state, deltaMonotonic); assert(ok); buffer = MemoryKit::mbuf(buffer, 0, bufferSize); SKC_TRACE(client, 3, "Header data: \"" << cEscapeString( StaticString(buffer.start, bufferSize)) << "\""); req->appSink.feedWithoutRefGuard(boost::move(buffer)); } else { char *buffer = (char *) psg_pnalloc(req->pool, bufferSize); ok = constructHeaderForSessionProtocol(req, buffer, bufferSize, state, deltaMonotonic); assert(ok); SKC_TRACE(client, 3, "Header data: \"" << cEscapeString( StaticString(buffer, bufferSize)) << "\""); req->appSink.feedWithoutRefGuard(MemoryKit::mbuf( buffer, bufferSize)); } (void) ok; // Shut up compiler warning } void Controller::sendBodyToAppWhenAppSinkIdle(Channel *_channel, unsigned int size) { FdSinkChannel *channel = reinterpret_cast<FdSinkChannel *>(_channel); Request *req = static_cast<Request *>(static_cast< ServerKit::BaseHttpRequest *>(channel->getHooks()->userData)); Client *client = static_cast<Client *>(req->client); Controller *self = static_cast<Controller *>( getServerFromClient(client)); SKC_LOG_EVENT_FROM_STATIC(self, Controller, client, "sendBodyToAppWhenAppSinkIdle"); channel->setConsumedCallback(NULL); if (channel->acceptingInput()) { self->sendBodyToApp(client, req); if (!req->ended()) { req->appSource.startReading(); } } else { // req->appSink.feed() encountered an error while writing to the // application socket. But we don't care about that; we just care that // ForwardResponse.cpp will now forward the response data and end the // request when it's done. UPDATE_TRACE_POINT(); assert(!req->appSink.ended()); assert(req->appSink.hasError()); self->logAppSocketWriteError(client, req->appSink.getErrcode()); req->state = Request::WAITING_FOR_APP_OUTPUT; req->appSource.startReading(); } } static bool isAlphaNum(char ch) { return (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z'); } /** * For CGI, alphanum headers with optional dashes are mapped to UPP3R_CAS3. This * function can be used to reject non-alphanum/dash headers that would end up with * the same mapping (e.g. upp3r_cas3 and upp3r-cas3 would end up the same, and * potentially collide each other in the receiving application). This is * used to fix CVE-2015-7519. */ static bool containsNonAlphaNumDash(const LString &s) { const LString::Part *part = s.start; while (part != NULL) { for (unsigned int i = 0; i < part->size; i++) { const char start = part->data[i]; if (start != '-' && !isAlphaNum(start)) { return true; } } part = part->next; } return false; } static void httpHeaderToScgiUpperCase(unsigned char *data, unsigned int size) { static const boost::uint8_t toUpperMap[256] = { '\0', 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, '\t', '\n', 0x0b, 0x0c, '\r', 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, ' ', '!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '_', '.', '/', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '[', '\\', ']', '^', '_', '`', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '{', '|', '}', '~', 0x7f, 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, 0x8e, 0x8f, 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0x9b, 0x9c, 0x9d, 0x9e, 0x9f, 0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xab, 0xac, 0xad, 0xae, 0xaf, 0xb0, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xbb, 0xbc, 0xbd, 0xbe, 0xbf, 0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xcb, 0xcc, 0xcd, 0xce, 0xcf, 0xd0, 0xd1, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xdb, 0xdc, 0xdd, 0xde, 0xdf, 0xe0, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xeb, 0xec, 0xed, 0xee, 0xef, 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff }; const unsigned char *buf = data; const size_t imax = size / 8; const size_t leftover = size % 8; size_t i; for (i = 0; i < imax; i++, data += 8) { data[0] = (unsigned char) toUpperMap[data[0]]; data[1] = (unsigned char) toUpperMap[data[1]]; data[2] = (unsigned char) toUpperMap[data[2]]; data[3] = (unsigned char) toUpperMap[data[3]]; data[4] = (unsigned char) toUpperMap[data[4]]; data[5] = (unsigned char) toUpperMap[data[5]]; data[6] = (unsigned char) toUpperMap[data[6]]; data[7] = (unsigned char) toUpperMap[data[7]]; } i = imax * 8; switch (leftover) { case 7: *data++ = (unsigned char) toUpperMap[buf[i++]]; /* Falls through. */ case 6: *data++ = (unsigned char) toUpperMap[buf[i++]]; /* Falls through. */ case 5: *data++ = (unsigned char) toUpperMap[buf[i++]]; /* Falls through. */ case 4: *data++ = (unsigned char) toUpperMap[buf[i++]]; /* Falls through. */ case 3: *data++ = (unsigned char) toUpperMap[buf[i++]]; /* Falls through. */ case 2: *data++ = (unsigned char) toUpperMap[buf[i++]]; /* Falls through. */ case 1: *data++ = (unsigned char) toUpperMap[buf[i]]; /* Falls through. */ case 0: break; } } unsigned int Controller::determineMaxHeaderSizeForSessionProtocol(Request *req, SessionProtocolWorkingState &state, string delta_monotonic) { unsigned int dataSize = sizeof(boost::uint32_t); state.path = req->getPathWithoutQueryString(); state.hasBaseURI = req->options.baseURI != P_STATIC_STRING("/") && startsWith(state.path, req->options.baseURI); if (state.hasBaseURI) { state.path = state.path.substr(req->options.baseURI.size()); if (state.path.empty()) { state.path = P_STATIC_STRING("/"); } } state.queryString = req->getQueryString(); state.methodStr = StaticString(http_method_str(req->method)); state.remoteAddr = req->secureHeaders.lookup(REMOTE_ADDR); state.remotePort = req->secureHeaders.lookup(REMOTE_PORT); state.remoteUser = req->secureHeaders.lookup(REMOTE_USER); state.contentType = req->headers.lookup(HTTP_CONTENT_TYPE); if (req->hasBody()) { state.contentLength = req->headers.lookup(HTTP_CONTENT_LENGTH); } else { state.contentLength = NULL; } if (req->envvars != NULL) { size_t len = modp_b64_decode_len(req->envvars->size); state.environmentVariablesData = (char *) malloc(len); if (state.environmentVariablesData == NULL) { throw RuntimeException("Unable to allocate memory for base64 " "decoding of environment variables"); } len = modp_b64_decode(state.environmentVariablesData, req->envvars->start->data, req->envvars->size); if (len == (size_t) -1) { throw RuntimeException("Unable to base64 decode environment variables"); } state.environmentVariablesSize = len; } dataSize += sizeof("REQUEST_URI"); dataSize += req->path.size + 1; dataSize += sizeof("PATH_INFO"); dataSize += state.path.size() + 1; dataSize += sizeof("SCRIPT_NAME"); if (state.hasBaseURI) { dataSize += req->options.baseURI.size(); } else { dataSize += sizeof(""); } dataSize += sizeof("QUERY_STRING"); dataSize += state.queryString.size() + 1; dataSize += sizeof("REQUEST_METHOD"); dataSize += state.methodStr.size() + 1; if (req->host != NULL && req->host->size > 0) { const LString *host = psg_lstr_make_contiguous(req->host, req->pool); const char *sep = (const char *) memchr(host->start->data, ':', host->size); if (sep != NULL) { state.serverName = StaticString(host->start->data, sep - host->start->data); state.serverPort = StaticString(sep + 1, host->start->data + host->size - sep - 1); } else { state.serverName = StaticString(host->start->data, host->size); if (req->https) { state.serverPort = P_STATIC_STRING("443"); } else { state.serverPort = P_STATIC_STRING("80"); } } } else { state.serverName = req->config->defaultServerName; state.serverPort = req->config->defaultServerPort; } dataSize += sizeof("SERVER_NAME"); dataSize += state.serverName.size() + 1; dataSize += sizeof("SERVER_PORT"); dataSize += state.serverPort.size() + 1; dataSize += sizeof("SERVER_SOFTWARE"); dataSize += req->config->serverSoftware.size() + 1; dataSize += sizeof("SERVER_PROTOCOL"); dataSize += sizeof("HTTP/1.1"); dataSize += sizeof("REMOTE_ADDR"); if (state.remoteAddr != NULL) { dataSize += state.remoteAddr->size + 1; } else { dataSize += sizeof("127.0.0.1"); } dataSize += sizeof("REMOTE_PORT"); if (state.remotePort != NULL) { dataSize += state.remotePort->size + 1; } else { dataSize += sizeof("0"); } if (state.remoteUser != NULL) { dataSize += sizeof("REMOTE_USER"); dataSize += state.remoteUser->size + 1; } if (state.contentType != NULL) { dataSize += sizeof("CONTENT_TYPE"); dataSize += state.contentType->size + 1; } if (state.contentLength != NULL) { dataSize += sizeof("CONTENT_LENGTH"); dataSize += state.contentLength->size + 1; } dataSize += sizeof("PASSENGER_CONNECT_PASSWORD"); dataSize += ApplicationPool2::ApiKey::SIZE + 1; if (req->https) { dataSize += sizeof("HTTPS"); dataSize += sizeof("on"); } if (req->upgraded()) { dataSize += sizeof("HTTP_CONNECTION"); dataSize += sizeof("upgrade"); } ServerKit::HeaderTable::Iterator it(req->headers); while (*it != NULL) { dataSize += sizeof("HTTP_") - 1 + it->header->key.size + 1; dataSize += it->header->val.size + 1; it.next(); } if (state.environmentVariablesData != NULL) { dataSize += state.environmentVariablesSize; } return dataSize + 1; } bool Controller::constructHeaderForSessionProtocol(Request *req, char * restrict buffer, unsigned int &size, const SessionProtocolWorkingState &state, string delta_monotonic) { char *pos = buffer; const char *end = buffer + size; pos += sizeof(boost::uint32_t); pos = appendData(pos, end, P_STATIC_STRING_WITH_NULL("REQUEST_URI")); pos = appendData(pos, end, req->path.start->data, req->path.size); pos = appendData(pos, end, "", 1); pos = appendData(pos, end, P_STATIC_STRING_WITH_NULL("PATH_INFO")); pos = appendData(pos, end, state.path.data(), state.path.size()); pos = appendData(pos, end, "", 1); pos = appendData(pos, end, P_STATIC_STRING_WITH_NULL("SCRIPT_NAME")); if (state.hasBaseURI) { pos = appendData(pos, end, req->options.baseURI); pos = appendData(pos, end, "", 1); } else { pos = appendData(pos, end, P_STATIC_STRING_WITH_NULL("")); } pos = appendData(pos, end, P_STATIC_STRING_WITH_NULL("QUERY_STRING")); pos = appendData(pos, end, state.queryString.data(), state.queryString.size()); pos = appendData(pos, end, "", 1); pos = appendData(pos, end, P_STATIC_STRING_WITH_NULL("REQUEST_METHOD")); pos = appendData(pos, end, state.methodStr); pos = appendData(pos, end, "", 1); pos = appendData(pos, end, P_STATIC_STRING_WITH_NULL("SERVER_NAME")); pos = appendData(pos, end, state.serverName); pos = appendData(pos, end, "", 1); pos = appendData(pos, end, P_STATIC_STRING_WITH_NULL("SERVER_PORT")); pos = appendData(pos, end, state.serverPort); pos = appendData(pos, end, "", 1); pos = appendData(pos, end, P_STATIC_STRING_WITH_NULL("SERVER_SOFTWARE")); pos = appendData(pos, end, req->config->serverSoftware); pos = appendData(pos, end, "", 1); pos = appendData(pos, end, P_STATIC_STRING_WITH_NULL("SERVER_PROTOCOL")); pos = appendData(pos, end, P_STATIC_STRING_WITH_NULL("HTTP/1.1")); pos = appendData(pos, end, P_STATIC_STRING_WITH_NULL("REMOTE_ADDR")); if (state.remoteAddr != NULL) { pos = appendData(pos, end, state.remoteAddr); pos = appendData(pos, end, "", 1); } else { pos = appendData(pos, end, P_STATIC_STRING_WITH_NULL("127.0.0.1")); } pos = appendData(pos, end, P_STATIC_STRING_WITH_NULL("REMOTE_PORT")); if (state.remotePort != NULL) { pos = appendData(pos, end, state.remotePort); pos = appendData(pos, end, "", 1); } else { pos = appendData(pos, end, P_STATIC_STRING_WITH_NULL("0")); } if (state.remoteUser != NULL) { pos = appendData(pos, end, P_STATIC_STRING_WITH_NULL("REMOTE_USER")); pos = appendData(pos, end, state.remoteUser); pos = appendData(pos, end, "", 1); } if (state.contentType != NULL) { pos = appendData(pos, end, P_STATIC_STRING_WITH_NULL("CONTENT_TYPE")); pos = appendData(pos, end, state.contentType); pos = appendData(pos, end, "", 1); } if (state.contentLength != NULL) { pos = appendData(pos, end, P_STATIC_STRING_WITH_NULL("CONTENT_LENGTH")); pos = appendData(pos, end, state.contentLength); pos = appendData(pos, end, "", 1); } pos = appendData(pos, end, P_STATIC_STRING_WITH_NULL("PASSENGER_CONNECT_PASSWORD")); pos = appendData(pos, end, req->session->getApiKey().toStaticString()); pos = appendData(pos, end, "", 1); if (req->https) { pos = appendData(pos, end, P_STATIC_STRING_WITH_NULL("HTTPS")); pos = appendData(pos, end, P_STATIC_STRING_WITH_NULL("on")); } if (req->upgraded()) { pos = appendData(pos, end, P_STATIC_STRING_WITH_NULL("HTTP_CONNECTION")); pos = appendData(pos, end, P_STATIC_STRING_WITH_NULL("upgrade")); } ServerKit::HeaderTable::Iterator it(req->headers); while (*it != NULL) { // This header-skipping is not accounted for in determineMaxHeaderSizeForSessionProtocol(), but // since we are only reducing the size it just wastes some mem bytes. if (( (it->header->hash == HTTP_CONTENT_LENGTH.hash() || it->header->hash == HTTP_CONTENT_TYPE.hash() || it->header->hash == HTTP_CONNECTION.hash() ) && (psg_lstr_cmp(&it->header->key, HTTP_CONTENT_TYPE) || psg_lstr_cmp(&it->header->key, HTTP_CONTENT_LENGTH) || psg_lstr_cmp(&it->header->key, HTTP_CONNECTION) ) ) || containsNonAlphaNumDash(it->header->key) ) { it.next(); continue; } pos = appendData(pos, end, P_STATIC_STRING("HTTP_")); const LString::Part *part = it->header->key.start; while (part != NULL) { char *start = pos; pos = appendData(pos, end, part->data, part->size); httpHeaderToScgiUpperCase((unsigned char *) start, pos - start); part = part->next; } pos = appendData(pos, end, "", 1); part = it->header->val.start; while (part != NULL) { pos = appendData(pos, end, part->data, part->size); part = part->next; } pos = appendData(pos, end, "", 1); it.next(); } if (state.environmentVariablesData != NULL) { pos = appendData(pos, end, state.environmentVariablesData, state.environmentVariablesSize); } Uint32Message::generate(buffer, pos - buffer - sizeof(boost::uint32_t)); size = pos - buffer; return pos < end; } void Controller::sendHeaderToAppWithHttpProtocol(Client *client, Request *req) { ssize_t bytesWritten; HttpHeaderConstructionCache cache; cache.cached = false; if (OXT_UNLIKELY(LoggingKit::getLevel() >= LoggingKit::DEBUG3)) { struct iovec *buffers; unsigned int nbuffers, dataSize; bool ok; ok = constructHeaderBuffersForHttpProtocol(req, NULL, 0, nbuffers, dataSize, cache); assert(ok); buffers = (struct iovec *) psg_palloc(req->pool, sizeof(struct iovec) * nbuffers); ok = constructHeaderBuffersForHttpProtocol(req, buffers, nbuffers, nbuffers, dataSize, cache); assert(ok); (void) ok; // Shut up compiler warning char *buffer = (char *) psg_pnalloc(req->pool, dataSize); gatherBuffers(buffer, dataSize, buffers, nbuffers); SKC_TRACE(client, 3, "Header data: \"" << cEscapeString(StaticString(buffer, dataSize)) << "\""); } if (!sendHeaderToAppWithHttpProtocolAndWritev(req, bytesWritten, cache)) { if (bytesWritten >= 0 || errno == EAGAIN || errno == EWOULDBLOCK) { sendHeaderToAppWithHttpProtocolWithBuffering(req, bytesWritten, cache); } else { int e = errno; P_ASSERT_EQ(bytesWritten, -1); disconnectWithAppSocketWriteError(&client, e); } } } /** * Construct an array of buffers, which together contain the 'http' protocol header * data that should be sent to the application. This method does not copy any data: * it just constructs buffers that point to the data stored inside `req->pool`, * `req->headers`, etc. * * The buffers will be stored in the array pointed to by `buffer`. This array must * have space for at least `maxbuffers` items. The actual number of buffers constructed * is stored in `nbuffers`, and the total data size of the buffers is stored in `dataSize`. * Upon success, returns true. If the actual number of buffers necessary exceeds * `maxbuffers`, then false is returned. * * You can also set `buffers` to NULL, in which case this method will not construct any * buffers, but only count the number of buffers necessary, as well as the total data size. * In this case, this method always returns true. */ bool Controller::constructHeaderBuffersForHttpProtocol(Request *req, struct iovec *buffers, unsigned int maxbuffers, unsigned int & restrict_ref nbuffers, unsigned int & restrict_ref dataSize, HttpHeaderConstructionCache &cache) { #define BEGIN_PUSH_NEXT_BUFFER() \ do { \ if (buffers != NULL && i >= maxbuffers) { \ return false; \ } \ } while (false) #define INC_BUFFER_ITER(i) \ do { \ i++; \ } while (false) #define PUSH_STATIC_BUFFER(buf) \ do { \ BEGIN_PUSH_NEXT_BUFFER(); \ if (buffers != NULL) { \ buffers[i].iov_base = (void *) buf; \ buffers[i].iov_len = sizeof(buf) - 1; \ } \ INC_BUFFER_ITER(i); \ dataSize += sizeof(buf) - 1; \ } while (false) ServerKit::HeaderTable::Iterator it(req->headers); const LString::Part *part; unsigned int i = 0; nbuffers = 0; dataSize = 0; if (!cache.cached) { cache.methodStr = http_method_str(req->method); cache.remoteAddr = req->secureHeaders.lookup(REMOTE_ADDR); cache.setCookie = req->headers.lookup(ServerKit::HTTP_SET_COOKIE); cache.cached = true; } if (buffers != NULL) { BEGIN_PUSH_NEXT_BUFFER(); buffers[i].iov_base = (void *) cache.methodStr.data(); buffers[i].iov_len = cache.methodStr.size(); } INC_BUFFER_ITER(i); dataSize += cache.methodStr.size(); PUSH_STATIC_BUFFER(" "); if (buffers != NULL) { BEGIN_PUSH_NEXT_BUFFER(); buffers[i].iov_base = (void *) req->path.start->data; buffers[i].iov_len = req->path.size; } INC_BUFFER_ITER(i); dataSize += req->path.size; if (req->upgraded()) { PUSH_STATIC_BUFFER(" HTTP/1.1\r\nConnection: upgrade\r\n"); } else { PUSH_STATIC_BUFFER(" HTTP/1.1\r\nConnection: close\r\n"); } if (cache.setCookie != NULL) { LString::Part *part; PUSH_STATIC_BUFFER("Set-Cookie: "); part = cache.setCookie->start; while (part != NULL) { if (part->size == 1 && part->data[0] == '\n') { // HeaderTable joins multiple Set-Cookie headers together using \n. PUSH_STATIC_BUFFER("\r\nSet-Cookie: "); } else { if (buffers != NULL) { BEGIN_PUSH_NEXT_BUFFER(); buffers[i].iov_base = (void *) part->data; buffers[i].iov_len = part->size; } INC_BUFFER_ITER(i); dataSize += part->size; } part = part->next; } PUSH_STATIC_BUFFER("\r\n"); } while (*it != NULL) { if ((it->header->hash == HTTP_CONNECTION.hash() || it->header->hash == ServerKit::HTTP_SET_COOKIE.hash()) && (psg_lstr_cmp(&it->header->key, HTTP_CONNECTION) || psg_lstr_cmp(&it->header->key, ServerKit::HTTP_SET_COOKIE))) { it.next(); continue; } part = it->header->key.start; while (part != NULL) { if (buffers != NULL) { BEGIN_PUSH_NEXT_BUFFER(); buffers[i].iov_base = (void *) part->data; buffers[i].iov_len = part->size; } INC_BUFFER_ITER(i); part = part->next; } dataSize += it->header->key.size; PUSH_STATIC_BUFFER(": "); part = it->header->val.start; while (part != NULL) { if (buffers != NULL) { BEGIN_PUSH_NEXT_BUFFER(); buffers[i].iov_base = (void *) part->data; buffers[i].iov_len = part->size; } INC_BUFFER_ITER(i); part = part->next; } dataSize += it->header->val.size; PUSH_STATIC_BUFFER("\r\n"); it.next(); } if (req->https) { PUSH_STATIC_BUFFER("X-Forwarded-Proto: https\r\n"); PUSH_STATIC_BUFFER("!~Passenger-Proto: https\r\n"); } if (cache.remoteAddr != NULL && cache.remoteAddr->size > 0) { PUSH_STATIC_BUFFER("X-Forwarded-For: "); part = cache.remoteAddr->start; while (part != NULL) { if (buffers != NULL) { BEGIN_PUSH_NEXT_BUFFER(); buffers[i].iov_base = (void *) part->data; buffers[i].iov_len = part->size; } INC_BUFFER_ITER(i); part = part->next; } dataSize += cache.remoteAddr->size; PUSH_STATIC_BUFFER("\r\n"); PUSH_STATIC_BUFFER("!~Passenger-Client-Address: "); part = cache.remoteAddr->start; while (part != NULL) { if (buffers != NULL) { BEGIN_PUSH_NEXT_BUFFER(); buffers[i].iov_base = (void *) part->data; buffers[i].iov_len = part->size; } INC_BUFFER_ITER(i); part = part->next; } dataSize += cache.remoteAddr->size; PUSH_STATIC_BUFFER("\r\n"); } if (req->envvars != NULL) { PUSH_STATIC_BUFFER("!~Passenger-Envvars: "); if (buffers != NULL) { BEGIN_PUSH_NEXT_BUFFER(); buffers[i].iov_base = (void *) req->envvars->start->data; buffers[i].iov_len = req->envvars->size; } INC_BUFFER_ITER(i); dataSize += req->envvars->size; PUSH_STATIC_BUFFER("\r\n"); } PUSH_STATIC_BUFFER("\r\n"); nbuffers = i; return true; #undef BEGIN_PUSH_NEXT_BUFFER #undef INC_BUFFER_ITER #undef PUSH_STATIC_BUFFER } bool Controller::sendHeaderToAppWithHttpProtocolAndWritev(Request *req, ssize_t &bytesWritten, HttpHeaderConstructionCache &cache) { unsigned int maxbuffers = std::min<unsigned int>( 5 + req->headers.size() * 4 + 4, IOV_MAX); struct iovec *buffers = (struct iovec *) psg_palloc(req->pool, sizeof(struct iovec) * maxbuffers); unsigned int nbuffers, dataSize; if (constructHeaderBuffersForHttpProtocol(req, buffers, maxbuffers, nbuffers, dataSize, cache)) { ssize_t ret; do { ret = writev(req->session->fd(), buffers, nbuffers); } while (ret == -1 && errno == EINTR); bytesWritten = ret; return ret == (ssize_t) dataSize; } else { bytesWritten = 0; return false; } } void Controller::sendHeaderToAppWithHttpProtocolWithBuffering(Request *req, unsigned int offset, HttpHeaderConstructionCache &cache) { struct iovec *buffers; unsigned int nbuffers, dataSize; bool ok; ok = constructHeaderBuffersForHttpProtocol(req, NULL, 0, nbuffers, dataSize, cache); assert(ok); buffers = (struct iovec *) psg_palloc(req->pool, sizeof(struct iovec) * nbuffers); ok = constructHeaderBuffersForHttpProtocol(req, buffers, nbuffers, nbuffers, dataSize, cache); assert(ok); (void) ok; // Shut up compiler warning MemoryKit::mbuf_pool &mbuf_pool = getContext()->mbuf_pool; const unsigned int MBUF_MAX_SIZE = mbuf_pool_data_size(&mbuf_pool); if (dataSize <= MBUF_MAX_SIZE) { MemoryKit::mbuf buffer(MemoryKit::mbuf_get(&mbuf_pool)); gatherBuffers(buffer.start, MBUF_MAX_SIZE, buffers, nbuffers); buffer = MemoryKit::mbuf(buffer, offset, dataSize - offset); req->appSink.feedWithoutRefGuard(boost::move(buffer)); } else { char *buffer = (char *) psg_pnalloc(req->pool, dataSize); gatherBuffers(buffer, dataSize, buffers, nbuffers); req->appSink.feedWithoutRefGuard(MemoryKit::mbuf( buffer + offset, dataSize - offset)); } } void Controller::sendBodyToApp(Client *client, Request *req) { TRACE_POINT(); assert(req->appSink.acceptingInput()); #ifdef DEBUG_CC_EVENT_LOOP_BLOCKING req->timeOnRequestHeaderSent = ev_now(getLoop()); reportLargeTimeDiff(client, "ApplicationPool get until headers sent", req->timeBeforeAccessingApplicationPool, req->timeOnRequestHeaderSent); #endif if (req->hasBody() || req->upgraded()) { // onRequestBody() will take care of forwarding // the request body to the app. SKC_TRACE(client, 2, "Sending body to application"); req->state = Request::FORWARDING_BODY_TO_APP; startBodyChannel(client, req); } else { // Our task is done. ForwardResponse.cpp will take // care of ending the request, once all response // data is forwarded. SKC_TRACE(client, 2, "No body to send to application"); req->state = Request::WAITING_FOR_APP_OUTPUT; maybeHalfCloseAppSinkBecauseRequestBodyEndReached(client, req); } } void Controller::maybeHalfCloseAppSinkBecauseRequestBodyEndReached(Client *client, Request *req) { P_ASSERT_EQ(req->state, Request::WAITING_FOR_APP_OUTPUT); if (req->halfClosePolicy == Request::HALF_CLOSE_UPON_REACHING_REQUEST_BODY_END) { SKC_TRACE(client, 3, "Half-closing application socket with SHUT_WR" " because end of request body reached"); req->halfClosePolicy = Request::HALF_CLOSE_PERFORMED; ::shutdown(req->session->fd(), SHUT_WR); } } ServerKit::Channel::Result Controller::whenSendingRequest_onRequestBody(Client *client, Request *req, const MemoryKit::mbuf &buffer, int errcode) { TRACE_POINT(); if (buffer.size() > 0) { // Data if (req->bodyType == Request::RBT_CONTENT_LENGTH) { SKC_TRACE(client, 3, "Forwarding " << buffer.size() << " bytes of client request body (" << req->bodyAlreadyRead << " of " << req->aux.bodyInfo.contentLength << " bytes forwarded in total): \"" << cEscapeString(StaticString(buffer.start, buffer.size())) << "\""); } else { SKC_TRACE(client, 3, "Forwarding " << buffer.size() << " bytes of client request body (" << req->bodyAlreadyRead << " bytes forwarded in total): \"" << cEscapeString(StaticString(buffer.start, buffer.size())) << "\""); } req->appSink.feed(buffer); if (!req->appSink.acceptingInput()) { if (req->appSink.mayAcceptInputLater()) { SKC_TRACE(client, 3, "Waiting for appSink channel to become " "idle before continuing sending body to application"); req->appSink.setConsumedCallback(resumeRequestBodyChannelWhenAppSinkIdle); stopBodyChannel(client, req); return Channel::Result(buffer.size(), false); } else { // Either we're done feeding to req->appSink, or req->appSink.feed() // encountered an error while writing to the application socket. // But we don't care about either scenarios; we just care that // ForwardResponse.cpp will now forward the response data and end the // request when it's done. assert(!req->ended()); assert(req->appSink.hasError()); logAppSocketWriteError(client, req->appSink.getErrcode()); req->state = Request::WAITING_FOR_APP_OUTPUT; stopBodyChannel(client, req); } } return Channel::Result(buffer.size(), false); } else if (errcode == 0 || errcode == ECONNRESET) { // EOF SKC_TRACE(client, 2, "End of request body encountered"); // Our task is done. ForwardResponse.cpp will take // care of ending the request, once all response // data is forwarded. req->state = Request::WAITING_FOR_APP_OUTPUT; maybeHalfCloseAppSinkBecauseRequestBodyEndReached(client, req); return Channel::Result(0, true); } else { const unsigned int BUFSIZE = 1024; char *message = (char *) psg_pnalloc(req->pool, BUFSIZE); int size = snprintf(message, BUFSIZE, "error reading request body: %s (errno=%d)", ServerKit::getErrorDesc(errcode), errcode); disconnectWithError(&client, StaticString(message, size)); return Channel::Result(0, true); } } void Controller::resumeRequestBodyChannelWhenAppSinkIdle(Channel *_channel, unsigned int size) { FdSinkChannel *channel = reinterpret_cast<FdSinkChannel *>(_channel); Request *req = static_cast<Request *>(static_cast< ServerKit::BaseHttpRequest *>(channel->getHooks()->userData)); Client *client = static_cast<Client *>(req->client); Controller *self = static_cast<Controller *>(getServerFromClient(client)); SKC_LOG_EVENT_FROM_STATIC(self, Controller, client, "resumeRequestBodyChannelWhenAppSinkIdle"); P_ASSERT_EQ(req->state, Request::FORWARDING_BODY_TO_APP); req->appSink.setConsumedCallback(NULL); if (req->appSink.acceptingInput()) { self->startBodyChannel(client, req); } else { // Either we're done feeding to req->appSink, or req->appSink.feed() // encountered an error while writing to the application socket. // But we don't care about either scenarios; we just care that // ForwardResponse.cpp will now forward the response data and end the // request when it's done. assert(!req->ended()); assert(req->appSink.hasError()); self->logAppSocketWriteError(client, req->appSink.getErrcode()); req->state = Request::WAITING_FOR_APP_OUTPUT; } } void Controller::startBodyChannel(Client *client, Request *req) { if (req->requestBodyBuffering) { req->bodyBuffer.start(); } else { req->bodyChannel.start(); } } void Controller::stopBodyChannel(Client *client, Request *req) { if (req->requestBodyBuffering) { req->bodyBuffer.stop(); } else { req->bodyChannel.stop(); } } void Controller::logAppSocketWriteError(Client *client, int errcode) { if (errcode == EPIPE) { SKC_INFO(client, "App socket write error: the application closed the socket prematurely" " (Broken pipe; errno=" << errcode << ")"); } else { SKC_INFO(client, "App socket write error: " << ServerKit::getErrorDesc(errcode) << " (errno=" << errcode << ")"); } } } // namespace Core } // namespace Passenger Controller/InternalUtils.cpp 0000644 00000025033 14756456557 0012223 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2011-2017 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include <Core/Controller.h> /************************************************************************* * * Internal utility functions for Core::Controller * *************************************************************************/ namespace Passenger { namespace Core { using namespace std; using namespace boost; /**************************** * * Private methods * ****************************/ void Controller::disconnectWithClientSocketWriteError(Client **client, int e) { stringstream message; LoggingKit::Level logLevel; message << "client socket write error: "; message << ServerKit::getErrorDesc(e); message << " (errno=" << e << ")"; if (e == EPIPE || e == ECONNRESET) { logLevel = LoggingKit::INFO; } else { logLevel = LoggingKit::WARN; } disconnectWithError(client, message.str(), logLevel); } void Controller::disconnectWithAppSocketIncompleteResponseError(Client **client) { disconnectWithError(client, "application did not send a complete response"); } void Controller::disconnectWithAppSocketReadError(Client **client, int e) { stringstream message; message << "app socket read error: "; message << ServerKit::getErrorDesc(e); message << " (errno=" << e << ")"; disconnectWithError(client, message.str()); } void Controller::disconnectWithAppSocketWriteError(Client **client, int e) { stringstream message; message << "app socket write error: "; message << ServerKit::getErrorDesc(e); message << " (errno=" << e << ")"; disconnectWithError(client, message.str()); } void Controller::endRequestWithAppSocketIncompleteResponse(Client **client, Request **req) { if (!(*req)->responseBegun) { // The application might have decided to abort the response because it thinks the client // is already gone (Passenger relays socket half-close events from clients), so don't // make a big warning out of that situation. if ((*req)->halfClosePolicy == Request::HALF_CLOSE_PERFORMED) { SKC_DEBUG(*client, "Sending 502 response: application did not send a complete response" " (likely because client half-closed)"); } else { SKC_WARN(*client, "Sending 502 response: application did not send a complete response"); } endRequestWithSimpleResponse(client, req, getFormattedMessage(*req, "Incomplete response received from application"), 502); } else { disconnectWithAppSocketIncompleteResponseError(client); } } void Controller::endRequestWithAppSocketReadError(Client **client, Request **req, int e) { Client *c = *client; if (!(*req)->responseBegun) { SKC_WARN(*client, "Sending 502 response: application socket read error"); endRequestWithSimpleResponse(client, req, getFormattedMessage(*req, "Application socket read error"), 502); } else { disconnectWithAppSocketReadError(&c, e); } } ServerKit::HeaderTable Controller::getHeadersWithContentType(Request *req) { ServerKit::HeaderTable headers; const LString *value = req->headers.lookup(P_STATIC_STRING("content-type")); if (value != NULL) { if (psg_lstr_cmp(value, P_STATIC_STRING("application/json"))) { headers.insert(req->pool, "content-type", "application/json"); } // Here we can extend setting `content-type` with more supported formats, ie: xml,... } return headers; } const string Controller::getFormattedMessage(Request *req, const StaticString &body) { const LString *value = req->headers.lookup(P_STATIC_STRING("content-type")); if (value != NULL) { if (psg_lstr_cmp(value, P_STATIC_STRING("application/json"))) { return "{\"status\":\"error\", \"message\": \""+body+"\"}"; } } return "<h1>"+body+"</h1>"; } /** * `data` must outlive the request. */ void Controller::endRequestWithSimpleResponse(Client **c, Request **r, const StaticString &body, int code) { Client *client = *c; Request *req = *r; ServerKit::HeaderTable headers = getHeadersWithContentType(req); headers.insert(req->pool, "cache-control", "no-cache, no-store, must-revalidate"); writeSimpleResponse(client, code, &headers, body); endRequest(c, r); } void Controller::endRequestAsBadGateway(Client **client, Request **req) { if ((*req)->responseBegun) { disconnectWithError(client, "bad gateway"); } else { ServerKit::HeaderTable headers = getHeadersWithContentType(*req); headers.insert((*req)->pool, "cache-control", "no-cache, no-store, must-revalidate"); writeSimpleResponse(*client, 502, &headers, getFormattedMessage(*req, "Bad Gateway")); endRequest(client, req); } } void Controller::writeBenchmarkResponse(Client **client, Request **req, bool end) { if (canKeepAlive(*req)) { writeResponse(*client, P_STATIC_STRING( "HTTP/1.1 200 OK\r\n" "Status: 200 OK\r\n" "Date: Wed, 15 Nov 1995 06:25:24 GMT\r\n" "Content-Type: text/plain\r\n" "Content-Length: 3\r\n" "Connection: keep-alive\r\n" "\r\n" "ok\n")); } else { writeResponse(*client, P_STATIC_STRING( "HTTP/1.1 200 OK\r\n" "Status: 200 OK\r\n" "Date: Wed, 15 Nov 1995 06:25:24 GMT\r\n" "Content-Type: text/plain\r\n" "Content-Length: 3\r\n" "Connection: close\r\n" "\r\n" "ok\n")); } if (end && !(*req)->ended()) { endRequest(client, req); } } bool Controller::getBoolOption(Request *req, const HashedStaticString &name, bool defaultValue) { const LString *value = req->secureHeaders.lookup(name); if (value != NULL && value->size > 0) { return psg_lstr_first_byte(value) == 't'; } else { return defaultValue; } } template<typename Number> Number Controller::clamp(Number value, Number min, Number max) { return std::max(std::min(value, max), min); } void Controller::gatherBuffers(char * restrict dest, unsigned int size, const struct iovec *buffers, unsigned int nbuffers) { const char *end = dest + size; char *pos = dest; for (unsigned int i = 0; i < nbuffers; i++) { assert(pos + buffers[i].iov_len <= end); memcpy(pos, buffers[i].iov_base, buffers[i].iov_len); pos += buffers[i].iov_len; } } // `path` MUST be NULL-terminated. Returns a contiguous LString. LString * Controller::resolveSymlink(const StaticString &path, psg_pool_t *pool) { char linkbuf[PATH_MAX + 1]; ssize_t size; size = readlink(path.data(), linkbuf, PATH_MAX); if (size == -1) { if (errno == EINVAL) { return psg_lstr_create(pool, path); } else { int e = errno; string message = "Cannot resolve possible symlink '"; message.append(path.data(), path.size()); message.append("'"); throw FileSystemException(message, e, path.data()); } } else { linkbuf[size] = '\0'; if (linkbuf[0] == '\0') { string message = "The file '"; message.append(path.data(), path.size()); message.append("' is a symlink, and it refers to an empty filename. This is not allowed."); throw FileSystemException(message, ENOENT, path.data()); } else if (linkbuf[0] == '/') { // Symlink points to an absolute path. size_t len = strlen(linkbuf); char *data = (char *) psg_pnalloc(pool, len + 1); memcpy(data, linkbuf, len); data[len] = '\0'; return psg_lstr_create(pool, data, len); } else { // Symlink points to a relative path. // We do not use absolutizePath() because it's too slow. // This version doesn't handle all the edge cases but is // much faster. StaticString workingDir = extractDirNameStatic(path); size_t linkbuflen = strlen(linkbuf); size_t resultlen = linkbuflen + 1 + workingDir.size(); char *data = (char *) psg_pnalloc(pool, resultlen); char *pos = data; char *end = data + resultlen; pos = appendData(pos, end, workingDir); *pos = '/'; pos++; pos = appendData(pos, end, linkbuf, linkbuflen); return psg_lstr_create(pool, data, resultlen); } } } void Controller::parseCookieHeader(psg_pool_t *pool, const LString *headerValue, vector< pair<StaticString, StaticString> > &cookies) const { // See http://stackoverflow.com/questions/6108207/definite-guide-to-valid-cookie-values // for syntax grammar. vector<StaticString> parts; vector<StaticString>::const_iterator it, it_end; assert(headerValue->size > 0); headerValue = psg_lstr_make_contiguous(headerValue, pool); split(StaticString(headerValue->start->data, headerValue->size), ';', parts); cookies.reserve(parts.size()); it_end = parts.end(); for (it = parts.begin(); it != it_end; it++) { const char *begin = it->data(); const char *end = it->data() + it->size(); const char *sep; skipLeadingWhitespaces(&begin, end); skipTrailingWhitespaces(begin, &end); // Find the separator ('='). sep = (const char *) memchr(begin, '=', end - begin); if (sep != NULL) { // Valid cookie. Otherwise, ignore it. const char *nameEnd = sep; const char *valueBegin = sep + 1; skipTrailingWhitespaces(begin, &nameEnd); skipLeadingWhitespaces(&valueBegin, end); cookies.push_back(make_pair( StaticString(begin, nameEnd - begin), StaticString(valueBegin, end - valueBegin) )); } } } #ifdef DEBUG_CC_EVENT_LOOP_BLOCKING void Controller::reportLargeTimeDiff(Client *client, const char *name, ev_tstamp fromTime, ev_tstamp toTime) { if (fromTime != 0 && toTime != 0) { ev_tstamp blockTime = toTime - fromTime; if (blockTime > 0.01) { char buf[1024]; int size = snprintf(buf, sizeof(buf), "%s: %.1f msec", name, blockTime * 1000); if (client != NULL) { SKC_NOTICE(client, StaticString(buf, size)); } else { SKS_NOTICE(StaticString(buf, size)); } } } } #endif } // namespace Core } // namespace Passenger Controller/Client.h 0000644 00000003773 14756456557 0010320 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2011-2017 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef _PASSENGER_REQUEST_HANDLER_CLIENT_H_ #define _PASSENGER_REQUEST_HANDLER_CLIENT_H_ #include <ev++.h> #include <ostream> #include <ServerKit/HttpClient.h> #include <Core/Controller/Request.h> namespace Passenger { namespace Core { using namespace std; using namespace boost; using namespace ApplicationPool2; class Client: public ServerKit::BaseHttpClient<Request> { public: ev_tstamp connectedAt; Client(void *server) : ServerKit::BaseHttpClient<Request>(server) { SERVER_KIT_BASE_HTTP_CLIENT_INIT(); } DEFINE_SERVER_KIT_BASE_HTTP_CLIENT_FOOTER(Passenger::Core::Client, Passenger::Core::Request); }; } // namespace Client } // namespace Passenger #endif /* _PASSENGER_REQUEST_HANDLER_CLIENT_H_ */ Controller/BufferBody.cpp 0000644 00000012362 14756456557 0011456 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2011-2017 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include <Core/Controller.h> /************************************************************************* * * Implements Core::Controller methods pertaining buffering the request * body. * *************************************************************************/ namespace Passenger { namespace Core { using namespace std; using namespace boost; /**************************** * * Private methods * ****************************/ void Controller::beginBufferingBody(Client *client, Request *req) { TRACE_POINT(); req->state = Request::BUFFERING_REQUEST_BODY; req->bodyChannel.start(); req->bodyBuffer.reinitialize(); req->bodyBuffer.stop(); } /** * Relevant when our body data source (bodyChannel) was throttled (by whenBufferingBody_onRequestBody). * Called when our data sink (bodyBuffer) in-memory part is drained and ready for more data. */ void Controller::_bodyBufferFlushed(FileBufferedChannel *channel) { Request *req = static_cast<Request *>(static_cast< ServerKit::BaseHttpRequest *>(channel->getHooks()->userData)); req->bodyBuffer.clearBuffersFlushedCallback(); req->bodyChannel.start(); } /** * Receives data (buffer) originating from the bodyChannel, to be passed on to the bodyBuffer. * Backpressure is applied when the bodyBuffer in-memory part exceeds a threshold. */ ServerKit::Channel::Result Controller::whenBufferingBody_onRequestBody(Client *client, Request *req, const MemoryKit::mbuf &buffer, int errcode) { TRACE_POINT(); if (buffer.size() > 0) { // Data req->bodyBytesBuffered += buffer.size(); SKC_TRACE(client, 3, "Buffering " << buffer.size() << " bytes of client request body: \"" << cEscapeString(StaticString(buffer.start, buffer.size())) << "\"; " << req->bodyBytesBuffered << " bytes buffered so far"); req->bodyBuffer.feed(buffer); if (req->bodyBuffer.passedThreshold()) { // Apply backpressure.. req->bodyChannel.stop(); // ..until the in-memory part of our bodyBuffer is drained. assert(req->bodyBuffer.getBuffersFlushedCallback() == NULL); req->bodyBuffer.setBuffersFlushedCallback(_bodyBufferFlushed); } return Channel::Result(buffer.size(), false); } else if (errcode == 0 || errcode == ECONNRESET) { // EOF SKC_TRACE(client, 2, "End of request body encountered"); req->bodyBuffer.feed(MemoryKit::mbuf()); if (req->bodyType == Request::RBT_CHUNKED) { // The data that we've stored in the body buffer is dechunked, so when forwarding // the buffered body to the app we must advertise it as being a fixed-length, // non-chunked body. const unsigned int UINT64_STRSIZE = sizeof("18446744073709551615"); SKC_TRACE(client, 2, "Adjusting forwarding headers as fixed-length, non-chunked"); ServerKit::Header *header = (ServerKit::Header *) psg_palloc(req->pool, sizeof(ServerKit::Header)); char *contentLength = (char *) psg_pnalloc(req->pool, UINT64_STRSIZE); unsigned int size = integerToOtherBase<boost::uint64_t, 10>( req->bodyBytesBuffered, contentLength, UINT64_STRSIZE); psg_lstr_init(&header->key); psg_lstr_append(&header->key, req->pool, "content-length", sizeof("content-length") - 1); psg_lstr_init(&header->origKey); psg_lstr_append(&header->origKey, req->pool, "Content-Length", sizeof("Content-Length") - 1); psg_lstr_init(&header->val); psg_lstr_append(&header->val, req->pool, contentLength, size); header->hash = HashedStaticString("content-length", sizeof("content-length") - 1).hash(); req->headers.erase(HTTP_TRANSFER_ENCODING); req->headers.insert(&header, req->pool); } checkoutSession(client, req); return Channel::Result(0, true); } else { const unsigned int BUFSIZE = 1024; char *message = (char *) psg_pnalloc(req->pool, BUFSIZE); int size = snprintf(message, BUFSIZE, "error reading request body: %s (errno=%d)", ServerKit::getErrorDesc(errcode), errcode); disconnectWithError(&client, StaticString(message, size)); return Channel::Result(0, true); } } } // namespace Core } // namespace Passenger Controller/Config.cpp 0000644 00000004216 14756456557 0010633 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2017 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include <Core/Controller.h> namespace Passenger { namespace Core { using namespace std; /**************************** * * Private methods * ****************************/ bool Controller::prepareConfigChange(const Json::Value &updates, vector<ConfigKit::Error> &errors, ControllerConfigChangeRequest &req) { if (ParentClass::prepareConfigChange(updates, errors, req.forParent)) { req.mainConfig.reset(new ControllerMainConfig( *req.forParent.forParent.config)); req.requestConfig.reset(new ControllerRequestConfig( *req.forParent.forParent.config)); } return errors.empty(); } void Controller::commitConfigChange(ControllerConfigChangeRequest &req) BOOST_NOEXCEPT_OR_NOTHROW { ParentClass::commitConfigChange(req.forParent); mainConfig.swap(*req.mainConfig); requestConfig.swap(req.requestConfig); } } // namespace Core } // namespace Passenger Controller/StateInspection.cpp 0000644 00000011662 14756456557 0012545 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2014-2017 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include <Core/Controller.h> #include <Integrations/LibevJsonUtils.h> /************************************************************************* * * State inspection functions for Core::Controller * *************************************************************************/ namespace Passenger { namespace Core { using namespace std; using namespace boost; /**************************** * * Public methods * ****************************/ unsigned int Controller::getThreadNumber() const { return mainConfig.threadNumber; } Json::Value Controller::inspectStateAsJson() const { Json::Value doc = ParentClass::inspectStateAsJson(); if (turboCaching.isEnabled()) { Json::Value subdoc; subdoc["fetches"] = turboCaching.responseCache.getFetches(); subdoc["hits"] = turboCaching.responseCache.getHits(); subdoc["hit_ratio"] = turboCaching.responseCache.getHitRatio(); subdoc["stores"] = turboCaching.responseCache.getStores(); subdoc["store_successes"] = turboCaching.responseCache.getStoreSuccesses(); subdoc["store_success_ratio"] = turboCaching.responseCache.getStoreSuccessRatio(); doc["turbocaching"] = subdoc; } return doc; } Json::Value Controller::inspectClientStateAsJson(const Client *client) const { Json::Value doc = ParentClass::inspectClientStateAsJson(client); doc["connected_at"] = evTimeToJson(client->connectedAt, ev_now(getLoop())); return doc; } Json::Value Controller::inspectRequestStateAsJson(const Request *req) const { Json::Value doc = ParentClass::inspectRequestStateAsJson(req); Json::Value flags; const AppResponse *resp = &req->appResponse; if (req->startedAt != 0) { doc["started_at"] = evTimeToJson(req->startedAt, ev_now(getLoop())); } doc["state"] = req->getStateString(); if (req->stickySession) { doc["sticky_session_id"] = req->options.stickySessionId; } doc["sticky_session"] = req->stickySession; doc["session_checkout_try"] = req->sessionCheckoutTry; flags["dechunk_response"] = req->dechunkResponse; flags["request_body_buffering"] = req->requestBodyBuffering; flags["https"] = req->https; doc["flags"] = flags; if (req->requestBodyBuffering) { doc["body_bytes_buffered"] = byteSizeToJson(req->bodyBytesBuffered); } if (req->session != NULL) { Json::Value &sessionDoc = doc["session"] = Json::Value(Json::objectValue); const AbstractSession *session = req->session.get(); if (req->session->isClosed()) { sessionDoc["closed"] = true; } else { sessionDoc["pid"] = (Json::Int64) session->getPid(); sessionDoc["gupid"] = session->getGupid().toString(); } } if (req->appResponseInitialized) { doc["app_response_http_state"] = resp->getHttpStateString(); if (resp->begun()) { doc["app_response_http_major"] = resp->httpMajor; doc["app_response_http_minor"] = resp->httpMinor; doc["app_response_want_keep_alive"] = resp->wantKeepAlive; doc["app_response_body_type"] = resp->getBodyTypeString(); doc["app_response_body_fully_read"] = resp->bodyFullyRead(); doc["app_response_body_already_read"] = byteSizeToJson( resp->bodyAlreadyRead); if (resp->httpState != AppResponse::ERROR) { if (resp->bodyType == AppResponse::RBT_CONTENT_LENGTH) { doc["app_response_content_length"] = byteSizeToJson( resp->aux.bodyInfo.contentLength); } else if (resp->bodyType == AppResponse::RBT_CHUNKED) { doc["app_response_end_chunk_reached"] = resp->aux.bodyInfo.endChunkReached; } } else { doc["app_response_parse_error"] = ServerKit::getErrorDesc(resp->aux.parseError); } } } doc["app_source_state"] = req->appSource.inspectAsJson(); doc["app_sink_state"] = req->appSink.inspectAsJson(); return doc; } } // namespace Core } // namespace Passenger Controller/CheckoutSession.cpp 0000644 00000030554 14756456557 0012543 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2011-2017 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include <Core/Controller.h> #include <Core/SpawningKit/ErrorRenderer.h> /************************************************************************* * * Implements Core::Controller methods pertaining selecting an application * process to handle the current request. * *************************************************************************/ namespace Passenger { namespace Core { using namespace std; using namespace boost; /**************************** * * Private methods * ****************************/ void Controller::checkoutSession(Client *client, Request *req) { GetCallback callback; Options &options = req->options; CC_BENCHMARK_POINT(client, req, BM_BEFORE_CHECKOUT); SKC_TRACE(client, 2, "Checking out session: appRoot=" << options.appRoot); req->state = Request::CHECKING_OUT_SESSION; if (req->requestBodyBuffering) { assert(!req->bodyBuffer.isStarted()); } else { assert(!req->bodyChannel.isStarted()); } callback.func = sessionCheckedOut; callback.userData = req; options.currentTime = SystemTime::getUsec(); refRequest(req, __FILE__, __LINE__); #ifdef DEBUG_CC_EVENT_LOOP_BLOCKING req->timeBeforeAccessingApplicationPool = ev_now(getLoop()); #endif asyncGetFromApplicationPool(req, callback); #ifdef DEBUG_CC_EVENT_LOOP_BLOCKING if (!req->timedAppPoolGet) { req->timedAppPoolGet = true; ev_now_update(getLoop()); reportLargeTimeDiff(client, "ApplicationPool get until return", req->timeBeforeAccessingApplicationPool, ev_now(getLoop())); } #endif } void Controller::asyncGetFromApplicationPool(Request *req, ApplicationPool2::GetCallback callback) { appPool->asyncGet(req->options, callback, true); } void Controller::sessionCheckedOut(const AbstractSessionPtr &session, const ExceptionPtr &e, void *userData) { Request *req = static_cast<Request *>(userData); Client *client = static_cast<Client *>(req->client); Controller *self = static_cast<Controller *>(getServerFromClient(client)); if (self->getContext()->libev->onEventLoopThread()) { self->sessionCheckedOutFromEventLoopThread(client, req, session, e); self->unrefRequest(req, __FILE__, __LINE__); } else { self->getContext()->libev->runLater( boost::bind(&Controller::sessionCheckedOutFromAnotherThread, self, client, req, session, e)); } } void Controller::sessionCheckedOutFromAnotherThread(Client *client, Request *req, AbstractSessionPtr session, ExceptionPtr e) { SKC_LOG_EVENT(Controller, client, "sessionCheckedOutFromAnotherThread"); sessionCheckedOutFromEventLoopThread(client, req, session, e); unrefRequest(req, __FILE__, __LINE__); } void Controller::sessionCheckedOutFromEventLoopThread(Client *client, Request *req, const AbstractSessionPtr &session, const ExceptionPtr &e) { if (req->ended()) { return; } TRACE_POINT(); CC_BENCHMARK_POINT(client, req, BM_AFTER_CHECKOUT); #ifdef DEBUG_CC_EVENT_LOOP_BLOCKING if (!req->timedAppPoolGet) { req->timedAppPoolGet = true; ev_now_update(getLoop()); reportLargeTimeDiff(client, "ApplicationPool get until return", req->timeBeforeAccessingApplicationPool, ev_now(getLoop())); } #endif if (e == NULL) { SKC_DEBUG(client, "Session checked out: pid=" << session->getPid() << ", gupid=" << session->getGupid()); req->session = session; UPDATE_TRACE_POINT(); maybeSend100Continue(client, req); UPDATE_TRACE_POINT(); initiateSession(client, req); } else { UPDATE_TRACE_POINT(); reportSessionCheckoutError(client, req, e); } } void Controller::maybeSend100Continue(Client *client, Request *req) { int httpVersion = req->httpMajor * 1000 + req->httpMinor * 10; if (httpVersion >= 1010 && req->hasBody() && !req->strip100ContinueHeader) { // Apps with the "session" protocol don't respond with 100-Continue, // so we do it for them. const LString *value = req->headers.lookup(HTTP_EXPECT); if (value != NULL && psg_lstr_cmp(value, P_STATIC_STRING("100-continue")) && req->session->getProtocol() == P_STATIC_STRING("session")) { const unsigned int BUFSIZE = 32; char *buf = (char *) psg_pnalloc(req->pool, BUFSIZE); int size = snprintf(buf, BUFSIZE, "HTTP/%d.%d 100 Continue\r\n", (int) req->httpMajor, (int) req->httpMinor); writeResponse(client, buf, size); if (!req->ended()) { // Allow sending more response headers. req->responseBegun = false; } } } } void Controller::initiateSession(Client *client, Request *req) { TRACE_POINT(); req->sessionCheckoutTry++; try { req->session->initiate(false); } catch (const SystemException &e2) { if (req->sessionCheckoutTry < MAX_SESSION_CHECKOUT_TRY) { SKC_DEBUG(client, "Error checking out session (" << e2.what() << "); retrying (attempt " << req->sessionCheckoutTry << ")"); refRequest(req, __FILE__, __LINE__); getContext()->libev->runLater(boost::bind(checkoutSessionLater, req)); } else { string message = "could not initiate a session ("; message.append(e2.what()); message.append(")"); disconnectWithError(&client, message); } return; } UPDATE_TRACE_POINT(); UPDATE_TRACE_POINT(); SKC_DEBUG(client, "Session initiated: fd=" << req->session->fd()); req->appSink.reinitialize(req->session->fd()); req->appSource.reinitialize(req->session->fd()); /***************/ /***************/ reinitializeAppResponse(client, req); sendHeaderToApp(client, req); } void Controller::checkoutSessionLater(Request *req) { Client *client = static_cast<Client *>(req->client); Controller *self = static_cast<Controller *>( Controller::getServerFromClient(client)); SKC_LOG_EVENT_FROM_STATIC(self, Controller, client, "checkoutSessionLater"); if (!req->ended()) { self->checkoutSession(client, req); } self->unrefRequest(req, __FILE__, __LINE__); } void Controller::reportSessionCheckoutError(Client *client, Request *req, const ExceptionPtr &e) { TRACE_POINT(); { boost::shared_ptr<RequestQueueFullException> e2 = dynamic_pointer_cast<RequestQueueFullException>(e); if (e2 != NULL) { writeRequestQueueFullExceptionErrorResponse(client, req, e2); return; } } { boost::shared_ptr<SpawningKit::SpawnException> e2 = dynamic_pointer_cast<SpawningKit::SpawnException>(e); if (e2 != NULL) { writeSpawnExceptionErrorResponse(client, req, e2); return; } } writeOtherExceptionErrorResponse(client, req, e); } int Controller::lookupCodeFromHeader(Request *req, const char* header, int statusCode) { const LString *value = req->secureHeaders.lookup(header); if (value != NULL && value->size > 0) { value = psg_lstr_make_contiguous(value, req->pool); statusCode = stringToInt( StaticString(value->start->data, value->size)); } return statusCode; } void Controller::writeRequestQueueFullExceptionErrorResponse(Client *client, Request *req, const boost::shared_ptr<RequestQueueFullException> &e) { TRACE_POINT(); int requestQueueOverflowStatusCode = lookupCodeFromHeader(req, "!~PASSENGER_REQUEST_QUEUE_OVERFLOW_STATUS_CODE", 503); SKC_WARN(client, "Returning HTTP " << requestQueueOverflowStatusCode << " due to: " << e->what()); endRequestWithSimpleResponse(&client, &req, "<h2>This website is under heavy load (queue full)</h2>" "<p>We're sorry, too many people are accessing this website at the same " "time. We're working on this problem. Please try again later.</p>", requestQueueOverflowStatusCode); } void Controller::writeSpawnExceptionErrorResponse(Client *client, Request *req, const boost::shared_ptr<SpawningKit::SpawnException> &e) { TRACE_POINT(); int spawnExceptionStatusCode = lookupCodeFromHeader(req, "!~PASSENGER_SPAWN_EXCEPTION_STATUS_CODE", 500); SKC_ERROR(client, "Cannot checkout session because a spawning error occurred. " << "The identifier of the error is " << e->getId() << ". Please see earlier logs for " << "details about the error."); endRequestWithErrorResponse(&client, &req, *e, spawnExceptionStatusCode); } void Controller::writeOtherExceptionErrorResponse(Client *client, Request *req, const ExceptionPtr &e) { TRACE_POINT(); // ATM "other" exceptions always return a spawn exception error message, so use the matching status code int otherExceptionStatusCode = lookupCodeFromHeader(req, "!~PASSENGER_SPAWN_EXCEPTION_STATUS_CODE", 500); string typeName; const oxt::tracable_exception &eptr = *e; #ifdef CXX_ABI_API_AVAILABLE int status; char *tmp = abi::__cxa_demangle(typeid(eptr).name(), 0, 0, &status); if (tmp != NULL) { typeName = tmp; free(tmp); } else { typeName = typeid(eptr).name(); } #else typeName = typeid(eptr).name(); #endif const unsigned int exceptionMessageLen = strlen(e->what()); string backtrace; boost::shared_ptr<tracable_exception> e3 = dynamic_pointer_cast<tracable_exception>(e); if (e3 != NULL) { backtrace = e3->backtrace(); } SKC_WARN(client, "Cannot checkout session due to " << typeName << ": " << e->what() << (!backtrace.empty() ? "\n" + backtrace : "")); if (friendlyErrorPagesEnabled(req)) { const unsigned int BUFFER_SIZE = 512 + typeName.size() + exceptionMessageLen + backtrace.size(); char *buf = (char *) psg_pnalloc(req->pool, BUFFER_SIZE); char *pos = buf; const char *end = buf + BUFFER_SIZE; pos = appendData(pos, end, "<h2>Internal server error</h2>"); pos = appendData(pos, end, "<p>Application could not be started.</p>"); pos = appendData(pos, end, "<p>Exception type: "); pos = appendData(pos, end, typeName); pos = appendData(pos, end, "<br>Error message: "); pos = appendData(pos, end, e->what(), exceptionMessageLen); if (!backtrace.empty()) { pos = appendData(pos, end, "<br>Backtrace:<br>"); pos = appendData(pos, end, backtrace); } pos = appendData(pos, end, "</p>"); endRequestWithSimpleResponse(&client, &req, StaticString(buf, pos - buf), otherExceptionStatusCode); } else { endRequestWithSimpleResponse(&client, &req, "<h2>Internal server error</h2>" "Application could not be started. Please try again later.", otherExceptionStatusCode); } } void Controller::endRequestWithErrorResponse(Client **c, Request **r, const SpawningKit::SpawnException &e, int statusCode) { TRACE_POINT(); Client *client = *c; Request *req = *r; SpawningKit::ErrorRenderer renderer(*appPool->getSpawningKitContext()); string data; if (friendlyErrorPagesEnabled(req)) { try { data = renderer.renderWithDetails(e); } catch (const SystemException &e2) { SKC_ERROR(client, "Cannot render an error page: " << e2.what() << "\n" << e2.backtrace()); data = e.getSummary(); } } else { try { data = renderer.renderWithoutDetails(e); } catch (const SystemException &e2) { SKC_ERROR(client, "Cannot render an error page: " << e2.what() << "\n" << e2.backtrace()); data = "<h2>Internal server error</h2>"; } } endRequestWithSimpleResponse(c, r, psg_pstrdup(req->pool, data), statusCode); } bool Controller::friendlyErrorPagesEnabled(Request *req) { bool defaultValue; const StaticString &defaultStr = req->config->defaultFriendlyErrorPages; if (defaultStr == "auto") { defaultValue = (req->options.environment == "development"); } else { defaultValue = defaultStr == "true"; } return getBoolOption(req, "!~PASSENGER_FRIENDLY_ERROR_PAGES", defaultValue); } /***************/ /***************/ } // namespace Core } // namespace Passenger Controller/Config.h 0000644 00000050654 14756456557 0010307 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2017-2018 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef _PASSENGER_CORE_CONTROLLER_CONFIG_H_ #define _PASSENGER_CORE_CONTROLLER_CONFIG_H_ #include <boost/bind/bind.hpp> #include <boost/intrusive_ptr.hpp> #include <boost/smart_ptr/intrusive_ref_counter.hpp> #include <string.h> #include <unistd.h> #include <sys/param.h> #include <cerrno> #include <ConfigKit/ConfigKit.h> #include <ConfigKit/SchemaUtils.h> #include <MemoryKit/palloc.h> #include <ServerKit/HttpServer.h> #include <SystemTools/UserDatabase.h> #include <WrapperRegistry/Registry.h> #include <Constants.h> #include <Exceptions.h> #include <StaticString.h> #include <Utils.h> namespace Passenger { namespace Core { using namespace std; enum ControllerBenchmarkMode { BM_NONE, BM_AFTER_ACCEPT, BM_BEFORE_CHECKOUT, BM_AFTER_CHECKOUT, BM_RESPONSE_BEGIN, BM_UNKNOWN }; inline ControllerBenchmarkMode parseControllerBenchmarkMode(const StaticString &mode) { if (mode.empty()) { return BM_NONE; } else if (mode == "after_accept") { return BM_AFTER_ACCEPT; } else if (mode == "before_checkout") { return BM_BEFORE_CHECKOUT; } else if (mode == "after_checkout") { return BM_AFTER_CHECKOUT; } else if (mode == "response_begin") { return BM_RESPONSE_BEGIN; } else { return BM_UNKNOWN; } } /* * BEGIN ConfigKit schema: Passenger::Core::ControllerSchema * (do not edit: following text is automatically generated * by 'rake configkit_schemas_inline_comments') * * accept_burst_count unsigned integer - default(32) * benchmark_mode string - - * client_freelist_limit unsigned integer - default(0) * default_abort_websockets_on_process_shutdown boolean - default(true) * default_app_file_descriptor_ulimit unsigned integer - - * default_bind_address string - default("127.0.0.1") * default_environment string - default("production") * default_force_max_concurrent_requests_per_process integer - default(-1) * default_friendly_error_pages string - default("auto") * default_group string - default * default_load_shell_envvars boolean - default(false) * default_max_preloader_idle_time unsigned integer - default(300) * default_max_request_queue_size unsigned integer - default(100) * default_max_requests unsigned integer - default(0) * default_meteor_app_settings string - - * default_min_instances unsigned integer - default(1) * default_nodejs string - default("node") * default_preload_bundler boolean - default(false) * default_python string - default("python") * default_ruby string - default("ruby") * default_server_name string required - * default_server_port unsigned integer required - * default_spawn_method string - default("smart") * default_sticky_sessions boolean - default(false) * default_sticky_sessions_cookie_attributes string - default("SameSite=Lax; Secure;") * default_sticky_sessions_cookie_name string - default("_passenger_route") * default_user string - default("nobody") * graceful_exit boolean - default(true) * integration_mode string - default("standalone"),read_only * max_instances_per_app unsigned integer - read_only * min_spare_clients unsigned integer - default(0) * multi_app boolean - default(true),read_only * request_freelist_limit unsigned integer - default(1024) * response_buffer_high_watermark unsigned integer - default(134217728) * server_software string - default("Phusion_Passenger/6.0.20") * show_version_in_header boolean - default(true) * start_reading_after_accept boolean - default(true) * stat_throttle_rate unsigned integer - default(10) * thread_number unsigned integer required read_only * turbocaching boolean - default(true),read_only * user_switching boolean - default(true) * vary_turbocache_by_cookie string - - * * END */ class ControllerSchema: public ServerKit::HttpServerSchema { private: void initialize() { using namespace ConfigKit; add("max_instances_per_app", UINT_TYPE, OPTIONAL | READ_ONLY); add("thread_number", UINT_TYPE, REQUIRED | READ_ONLY); add("multi_app", BOOL_TYPE, OPTIONAL | READ_ONLY, true); add("turbocaching", BOOL_TYPE, OPTIONAL | READ_ONLY, true); add("integration_mode", STRING_TYPE, OPTIONAL | READ_ONLY, DEFAULT_INTEGRATION_MODE); add("user_switching", BOOL_TYPE, OPTIONAL, true); add("stat_throttle_rate", UINT_TYPE, OPTIONAL, DEFAULT_STAT_THROTTLE_RATE); add("show_version_in_header", BOOL_TYPE, OPTIONAL, true); add("response_buffer_high_watermark", UINT_TYPE, OPTIONAL, DEFAULT_RESPONSE_BUFFER_HIGH_WATERMARK); add("graceful_exit", BOOL_TYPE, OPTIONAL, true); add("benchmark_mode", STRING_TYPE, OPTIONAL); add("default_ruby", STRING_TYPE, OPTIONAL, DEFAULT_RUBY); add("default_python", STRING_TYPE, OPTIONAL, DEFAULT_PYTHON); add("default_nodejs", STRING_TYPE, OPTIONAL, DEFAULT_NODEJS); add("default_user", STRING_TYPE, OPTIONAL, DEFAULT_WEB_APP_USER); addWithDynamicDefault( "default_group", STRING_TYPE, OPTIONAL | CACHE_DEFAULT_VALUE, inferDefaultValueForDefaultGroup); add("default_server_name", STRING_TYPE, REQUIRED); add("default_server_port", UINT_TYPE, REQUIRED); add("default_sticky_sessions", BOOL_TYPE, OPTIONAL, false); add("default_sticky_sessions_cookie_name", STRING_TYPE, OPTIONAL, DEFAULT_STICKY_SESSIONS_COOKIE_NAME); add("default_sticky_sessions_cookie_attributes", STRING_TYPE, OPTIONAL, DEFAULT_STICKY_SESSIONS_COOKIE_ATTRIBUTES); add("server_software", STRING_TYPE, OPTIONAL, SERVER_TOKEN_NAME "/" PASSENGER_VERSION); add("vary_turbocache_by_cookie", STRING_TYPE, OPTIONAL); add("default_friendly_error_pages", STRING_TYPE, OPTIONAL, "auto"); add("default_environment", STRING_TYPE, OPTIONAL, DEFAULT_APP_ENV); add("default_spawn_method", STRING_TYPE, OPTIONAL, DEFAULT_SPAWN_METHOD); add("default_bind_address", STRING_TYPE, OPTIONAL, DEFAULT_BIND_ADDRESS); add("default_load_shell_envvars", BOOL_TYPE, OPTIONAL, false); add("default_preload_bundler", BOOL_TYPE, OPTIONAL, false); add("default_meteor_app_settings", STRING_TYPE, OPTIONAL); add("default_app_file_descriptor_ulimit", UINT_TYPE, OPTIONAL); add("default_min_instances", UINT_TYPE, OPTIONAL, 1); add("default_max_preloader_idle_time", UINT_TYPE, OPTIONAL, DEFAULT_MAX_PRELOADER_IDLE_TIME); add("default_max_request_queue_size", UINT_TYPE, OPTIONAL, DEFAULT_MAX_REQUEST_QUEUE_SIZE); add("default_force_max_concurrent_requests_per_process", INT_TYPE, OPTIONAL, -1); add("default_abort_websockets_on_process_shutdown", BOOL_TYPE, OPTIONAL, true); add("default_max_requests", UINT_TYPE, OPTIONAL, 0); /*******************/ /*******************/ addValidator(validate); addValidator(ConfigKit::validateIntegrationMode); } static Json::Value inferDefaultValueForDefaultGroup(const ConfigKit::Store &config) { OsUser osUser; if (!lookupSystemUserByName(config["default_user"].asString(), osUser)) { throw ConfigurationException( "The user that PassengerDefaultUser refers to, '" + config["default_user"].asString() + "', does not exist."); } return lookupSystemGroupnameByGid(osUser.pwd.pw_gid, P_STATIC_STRING("%d")); } static void validate(const ConfigKit::Store &config, vector<ConfigKit::Error> &errors) { using namespace ConfigKit; ControllerBenchmarkMode mode = parseControllerBenchmarkMode( config["benchmark_mode"].asString()); if (mode == BM_UNKNOWN) { errors.push_back(Error("'{{benchmark_mode}}' is not set to a valid value")); } /*******************/ } public: ControllerSchema() : ServerKit::HttpServerSchema(false) { initialize(); finalize(); } ControllerSchema(bool _subclassing) : ServerKit::HttpServerSchema(false) { initialize(); } }; /* * BEGIN ConfigKit schema: Passenger::Core::ControllerSingleAppModeSchema * (do not edit: following text is automatically generated * by 'rake configkit_schemas_inline_comments') * * app_root string - default,read_only * app_start_command string - read_only * app_type string - read_only * startup_file string - read_only * * END */ struct ControllerSingleAppModeSchema: public ConfigKit::Schema { ControllerSingleAppModeSchema(const WrapperRegistry::Registry *wrapperRegistry = NULL) { using namespace ConfigKit; addWithDynamicDefault("app_root", STRING_TYPE, OPTIONAL | READ_ONLY | CACHE_DEFAULT_VALUE, getDefaultAppRoot); add("app_type", STRING_TYPE, OPTIONAL | READ_ONLY); add("startup_file", STRING_TYPE, OPTIONAL | READ_ONLY); add("app_start_command", STRING_TYPE, OPTIONAL | READ_ONLY); addValidator(validateAppTypeOrAppStartCommandSet); addValidator(boost::bind(validateAppType, "app_type", wrapperRegistry, boost::placeholders::_1, boost::placeholders::_2)); addNormalizer(normalizeAppRoot); addNormalizer(normalizeStartupFile); finalize(); } static Json::Value getDefaultAppRoot(const ConfigKit::Store &config) { char buf[MAXPATHLEN]; const char *path = getcwd(buf, sizeof(buf)); if (path == NULL) { int e = errno; throw SystemException("Unable to obtain current working directory", e); } string result = path; return result; } static void validateAppTypeOrAppStartCommandSet(const ConfigKit::Store &config, vector<ConfigKit::Error> &errors) { typedef ConfigKit::Error Error; if (config["app_type"].isNull() && config["app_start_command"].isNull()) { errors.push_back(Error( "Either '{{app_type}}' or '{{app_start_command}}' must be set")); } if (!config["app_type"].isNull() && config["startup_file"].isNull()) { errors.push_back(Error( "If '{{app_type}}' is set, then '{{startup_file}}' must also be set")); } } static void validateAppType(const string &appTypeKey, const WrapperRegistry::Registry *wrapperRegistry, const ConfigKit::Store &config, vector<ConfigKit::Error> &errors) { typedef ConfigKit::Error Error; if (!config[appTypeKey].isNull() && wrapperRegistry != NULL) { const WrapperRegistry::Entry &entry = wrapperRegistry->lookup(config[appTypeKey].asString()); if (entry.isNull()) { string message = "'{{" + appTypeKey + "}}' is set to '" + config[appTypeKey].asString() + "', which is not a" " valid application type. Supported app types are:"; WrapperRegistry::Registry::ConstIterator it( wrapperRegistry->getIterator()); while (*it != NULL) { message.append(1, ' '); message.append(it.getValue().language); it.next(); } errors.push_back(Error(message)); } } } static Json::Value normalizeAppRoot(const Json::Value &effectiveValues) { Json::Value updates; updates["app_root"] = absolutizePath(effectiveValues["app_root"].asString()); return updates; } static Json::Value normalizeStartupFile(const Json::Value &effectiveValues) { Json::Value updates; if (effectiveValues.isMember("startup_file")) { updates["startup_file"] = absolutizePath( effectiveValues["startup_file"].asString()); } return updates; } }; /** * A structure that caches controller configuration which is allowed to * change at any time, even during the middle of a request. */ class ControllerMainConfig { private: StaticString createServerLogName() { string name = "ServerThr." + toString(threadNumber); return psg_pstrdup(pool, name); } public: psg_pool_t *pool; unsigned int threadNumber; unsigned int statThrottleRate; unsigned int responseBufferHighWatermark; StaticString integrationMode; StaticString serverLogName; unsigned int maxInstancesPerApp; ControllerBenchmarkMode benchmarkMode: 3; bool singleAppMode: 1; bool userSwitching: 1; bool defaultStickySessions: 1; bool gracefulExit: 1; /*******************/ /*******************/ ControllerMainConfig(const ConfigKit::Store &config) : pool(psg_create_pool(1024)), threadNumber(config["thread_number"].asUInt()), statThrottleRate(config["stat_throttle_rate"].asUInt()), responseBufferHighWatermark(config["response_buffer_high_watermark"].asUInt()), integrationMode(psg_pstrdup(pool, config["integration_mode"].asString())), serverLogName(createServerLogName()), maxInstancesPerApp(config["max_instances_per_app"].asUInt()), benchmarkMode(parseControllerBenchmarkMode(config["benchmark_mode"].asString())), singleAppMode(!config["multi_app"].asBool()), userSwitching(config["user_switching"].asBool()), defaultStickySessions(config["default_sticky_sessions"].asBool()), gracefulExit(config["graceful_exit"].asBool()) /*******************/ { /*******************/ } ~ControllerMainConfig() { psg_destroy_pool(pool); } void swap(ControllerMainConfig &other) BOOST_NOEXCEPT_OR_NOTHROW { #define SWAP_BITFIELD(Type, name) \ do { \ Type tmp = name; \ name = other.name; \ other.name = tmp; \ } while (false) std::swap(pool, other.pool); std::swap(threadNumber, other.threadNumber); std::swap(statThrottleRate, other.statThrottleRate); std::swap(responseBufferHighWatermark, other.responseBufferHighWatermark); std::swap(integrationMode, other.integrationMode); std::swap(serverLogName, other.serverLogName); SWAP_BITFIELD(ControllerBenchmarkMode, benchmarkMode); SWAP_BITFIELD(bool, singleAppMode); SWAP_BITFIELD(bool, userSwitching); SWAP_BITFIELD(bool, defaultStickySessions); SWAP_BITFIELD(bool, gracefulExit); /*******************/ #undef SWAP_BITFIELD } }; /** * A structure that caches controller configuration that must stay the * same for the entire duration of a request. * * Note that this structure has got nothing to do with per-request config * options: options which may be configured by the web server on a * per-request basis. That is an orthogonal concept. */ class ControllerRequestConfig: public boost::intrusive_ref_counter<ControllerRequestConfig, boost::thread_unsafe_counter> { public: psg_pool_t *pool; StaticString defaultRuby; StaticString defaultPython; StaticString defaultNodejs; StaticString defaultUser; StaticString defaultGroup; StaticString defaultServerName; StaticString defaultServerPort; StaticString serverSoftware; StaticString defaultStickySessionsCookieName; StaticString defaultStickySessionsCookieAttributes; StaticString defaultVaryTurbocacheByCookie; StaticString defaultFriendlyErrorPages; StaticString defaultEnvironment; StaticString defaultSpawnMethod; StaticString defaultBindAddress; StaticString defaultMeteorAppSettings; unsigned int defaultAppFileDescriptorUlimit; unsigned int defaultMinInstances; unsigned int defaultMaxPreloaderIdleTime; unsigned int defaultMaxRequestQueueSize; unsigned int defaultMaxRequests; int defaultForceMaxConcurrentRequestsPerProcess; bool showVersionInHeader: 1; bool defaultAbortWebsocketsOnProcessShutdown; bool defaultLoadShellEnvvars; bool defaultPreloadBundler; /*******************/ /*******************/ ControllerRequestConfig(const ConfigKit::Store &config) : pool(psg_create_pool(1024 * 4)), defaultRuby(psg_pstrdup(pool, config["default_ruby"].asString())), defaultPython(psg_pstrdup(pool, config["default_python"].asString())), defaultNodejs(psg_pstrdup(pool, config["default_nodejs"].asString())), defaultUser(psg_pstrdup(pool, config["default_user"].asString())), defaultGroup(psg_pstrdup(pool, config["default_group"].asString())), defaultServerName(psg_pstrdup(pool, config["default_server_name"].asString())), defaultServerPort(psg_pstrdup(pool, config["default_server_port"].asString())), serverSoftware(psg_pstrdup(pool, config["server_software"].asString())), defaultStickySessionsCookieName(psg_pstrdup(pool, config["default_sticky_sessions_cookie_name"].asString())), defaultStickySessionsCookieAttributes(psg_pstrdup(pool, config["default_sticky_sessions_cookie_attributes"].asString())), defaultVaryTurbocacheByCookie(psg_pstrdup(pool, config["vary_turbocache_by_cookie"].asString())), defaultFriendlyErrorPages(psg_pstrdup(pool, config["default_friendly_error_pages"].asString())), defaultEnvironment(psg_pstrdup(pool, config["default_environment"].asString())), defaultSpawnMethod(psg_pstrdup(pool, config["default_spawn_method"].asString())), defaultBindAddress(psg_pstrdup(pool, config["default_bind_address"].asString())), defaultMeteorAppSettings(psg_pstrdup(pool, config["default_meteor_app_settings"].asString())), defaultAppFileDescriptorUlimit(config["default_app_file_descriptor_ulimit"].asUInt()), defaultMinInstances(config["default_min_instances"].asUInt()), defaultMaxPreloaderIdleTime(config["default_max_preloader_idle_time"].asUInt()), defaultMaxRequestQueueSize(config["default_max_request_queue_size"].asUInt()), defaultMaxRequests(config["default_max_requests"].asUInt()), defaultForceMaxConcurrentRequestsPerProcess(config["default_force_max_concurrent_requests_per_process"].asInt()), showVersionInHeader(config["show_version_in_header"].asBool()), defaultAbortWebsocketsOnProcessShutdown(config["default_abort_websockets_on_process_shutdown"].asBool()), defaultLoadShellEnvvars(config["default_load_shell_envvars"].asBool()), defaultPreloadBundler(config["default_preload_bundler"].asBool()) /*******************/ { } ~ControllerRequestConfig() { psg_destroy_pool(pool); } }; typedef boost::intrusive_ptr<ControllerRequestConfig> ControllerRequestConfigPtr; struct ControllerConfigChangeRequest { ServerKit::HttpServerConfigChangeRequest forParent; boost::scoped_ptr<ControllerMainConfig> mainConfig; ControllerRequestConfigPtr requestConfig; }; } // namespace Core } // namespace Passenger #endif /* _PASSENGER_CORE_CONTROLLER_CONFIG_H_ */ Controller/Miscellaneous.cpp 0000644 00000006470 14756456557 0012235 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2014-2017 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include <Core/Controller.h> /************************************************************************* * * Miscellaneous functions for Core::Controller * *************************************************************************/ namespace Passenger { namespace Core { using namespace std; using namespace boost; /**************************** * * Public methods * ****************************/ void Controller::disconnectLongRunningConnections(const StaticString &gupid) { vector<Client *> clients; vector<Client *>::iterator v_it, v_end; Client *client; // We collect all clients in a vector so that we don't have to worry about // `activeClients` being mutated while we work. TAILQ_FOREACH (client, &activeClients, nextClient.activeOrDisconnectedClient) { P_ASSERT_EQ(client->getConnState(), Client::ACTIVE); if (client->currentRequest != NULL) { Request *req = client->currentRequest; if (req->httpState >= Request::COMPLETE && req->upgraded() && req->options.abortWebsocketsOnProcessShutdown && req->session != NULL && req->session->getGupid() == gupid) { if (LoggingKit::getLevel() >= LoggingKit::INFO) { char clientName[32]; unsigned int size; const LString *host; StaticString hostStr; size = getClientName(client, clientName, sizeof(clientName)); if (req->host != NULL && req->host->size > 0) { host = psg_lstr_make_contiguous(req->host, req->pool); hostStr = StaticString(host->start->data, host->size); } P_INFO("[" << getServerName() << "] Disconnecting client " << StaticString(clientName, size) << ": " << hostStr << StaticString(req->path.start->data, req->path.size)); } refClient(client, __FILE__, __LINE__); clients.push_back(client); } } } // Disconnect each eligible client. v_end = clients.end(); for (v_it = clients.begin(); v_it != v_end; v_it++) { client = *v_it; Client *c = client; disconnect(&client); unrefClient(c, __FILE__, __LINE__); } } } // namespace Core } // namespace Passenger SecurityUpdateChecker.h 0000644 00000063173 14756456557 0011216 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2017-2018 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef _PASSENGER_SECURITY_UPDATE_CHECKER_H_ #define _PASSENGER_SECURITY_UPDATE_CHECKER_H_ #include <string> #include <cassert> #include <boost/config.hpp> #include <boost/scoped_ptr.hpp> #include <oxt/thread.hpp> #include <oxt/backtrace.hpp> #include <SecurityKit/Crypto.h> #include <ResourceLocator.h> #include <Exceptions.h> #include <StaticString.h> #include <ConfigKit/ConfigKit.h> #include <Utils/Curl.h> #include <modp_b64.h> #if BOOST_OS_MACOS #include <sys/syslimits.h> #include <unistd.h> #include <Availability.h> #ifndef __MAC_10_13 #define __MAC_10_13 101300 #endif #define PRE_HIGH_SIERRA (__MAC_OS_X_VERSION_MIN_REQUIRED < __MAC_10_13) #if !PRE_HIGH_SIERRA #include <openssl/err.h> #endif #endif namespace Passenger { using namespace std; using namespace oxt; #define MIN_CHECK_BACKOFF_SEC (12 * 60 * 60) #define MAX_CHECK_BACKOFF_SEC (7 * 24 * 60 * 60) #if PRE_HIGH_SIERRA // Password for the .p12 client certificate (because .p12 is required to be pwd protected on some // implementations). We're OK with hardcoding because the certs are not secret anyway, and they're not used // for client id/auth (just to easily deflect unrelated probes from the server endpoint). #define CLIENT_CERT_PWD "p6PBhK8KtorrhMxHnH855MvF" #define CLIENT_CERT_LABEL "Phusion Passenger Open Source" #endif #define POSSIBLE_MITM_RESOLUTION "(if this error persists check your connection security or try upgrading " SHORT_PROGRAM_NAME ")" /** * If started, this class periodically (default: daily, immediate start) checks whether there are any important * security updates available (updates that don't fix security issues are not reported). The result is logged * (level 3:notice if no update, level 1:error otherwise), and all further action is left to the user (there is * no auto-update mechanism). */ class SecurityUpdateChecker { public: /* * BEGIN ConfigKit schema: Passenger::SecurityUpdateChecker::Schema * (do not edit: following text is automatically generated * by 'rake configkit_schemas_inline_comments') * * certificate_path string - - * disabled boolean - default(false) * interval unsigned integer - default(86400) * proxy_url string - - * server_identifier string required - * url string - default("https://securitycheck.phusionpassenger.com/v1/check.json") * web_server_version string - - * * END */ class Schema: public ConfigKit::Schema { private: static void validateInterval(const ConfigKit::Store &config, vector<ConfigKit::Error> &errors) { unsigned int interval = config["interval"].asUInt(); if (interval < MIN_CHECK_BACKOFF_SEC || interval > MAX_CHECK_BACKOFF_SEC) { errors.push_back(ConfigKit::Error("'{{interval}}' must be between " + toString(MIN_CHECK_BACKOFF_SEC) + " and " + toString(MAX_CHECK_BACKOFF_SEC))); } } static void validateProxyUrl(const ConfigKit::Store &config, vector<ConfigKit::Error> &errors) { if (config["proxy_url"].isNull()) { return; } if (config["proxy_url"].asString().empty()) { errors.push_back(ConfigKit::Error("'{{proxy_url}}', if specified, may not be empty")); return; } try { prepareCurlProxy(config["proxy_url"].asString()); } catch (const ArgumentException &e) { errors.push_back(ConfigKit::Error( P_STATIC_STRING("'{{proxy_url}}': ") + e.what())); } } public: Schema() { using namespace ConfigKit; add("disabled", BOOL_TYPE, OPTIONAL, false); add("url", STRING_TYPE, OPTIONAL, "https://securitycheck.phusionpassenger.com/v1/check.json"); // Should be in the form: scheme://user:password@proxy_host:proxy_port add("proxy_url", STRING_TYPE, OPTIONAL); add("certificate_path", STRING_TYPE, OPTIONAL); add("interval", UINT_TYPE, OPTIONAL, 24 * 60 * 60); // Should be one of { nginx, apache, standalone nginx, standalone builtin } add("server_identifier", STRING_TYPE, REQUIRED); // The version of Nginx or Apache, if relevant (otherwise empty) add("web_server_version", STRING_TYPE, OPTIONAL); addValidator(validateInterval); addValidator(validateProxyUrl); finalize(); } }; struct ConfigRealization { CurlProxyInfo proxyInfo; string url; string certificatePath; ConfigRealization(const ConfigKit::Store &config) : proxyInfo(prepareCurlProxy(config["proxy_url"].asString())), url(config["url"].asString()), certificatePath(config["certificate_path"].asString()) { } void swap(ConfigRealization &other) BOOST_NOEXCEPT_OR_NOTHROW { proxyInfo.swap(other.proxyInfo); url.swap(other.url); certificatePath.swap(other.certificatePath); } }; struct ConfigChangeRequest { boost::scoped_ptr<ConfigKit::Store> config; boost::scoped_ptr<ConfigRealization> configRlz; }; private: /* * Since the security update checker runs in a separate thread, * and the configuration can change while the checker is active, * we make a copy of the current configuration at the beginning * of each check. */ struct SessionState { ConfigKit::Store config; ConfigRealization configRlz; SessionState(const ConfigKit::Store ¤tConfig, const ConfigRealization ¤tConfigRlz) : config(currentConfig), configRlz(currentConfigRlz) { } }; mutable boost::mutex configSyncher; ConfigKit::Store config; ConfigRealization configRlz; oxt::thread *updateCheckThread; string clientCertPath; // client cert (PKCS#12), checked by server string serverPubKeyPath; // for checking signature Crypto crypto; void threadMain() { TRACE_POINT(); // Sleep for a short while to allow interruption during the Apache integration // double startup procedure, this prevents running the update check twice boost::this_thread::sleep_for(boost::chrono::seconds(2)); while (!boost::this_thread::interruption_requested()) { UPDATE_TRACE_POINT(); int backoffMin = 0; try { backoffMin = checkAndLogSecurityUpdate(); } catch (const tracable_exception &e) { P_ERROR(e.what() << "\n" << e.backtrace()); } UPDATE_TRACE_POINT(); unsigned int checkIntervalSec; { boost::lock_guard<boost::mutex> l(configSyncher); checkIntervalSec = config["interval"].asUInt(); } long backoffSec = checkIntervalSec + (backoffMin * 60); if (backoffSec < MIN_CHECK_BACKOFF_SEC) { backoffSec = MIN_CHECK_BACKOFF_SEC; } if (backoffSec > MAX_CHECK_BACKOFF_SEC) { backoffSec = MAX_CHECK_BACKOFF_SEC; } boost::this_thread::sleep_for(boost::chrono::seconds(backoffSec)); } } void logUpdateFailCurl(const SessionState &sessionState, CURLcode code) { // At this point anything could be wrong, from unloadable certificates to server not found, etc. // Let's try to enrich the log message in case there are known solutions or workarounds (e.g. "use proxy"). string error = curl_easy_strerror(code); switch (code) { case CURLE_SSL_CERTPROBLEM: error.append(" at " + clientCertPath + " (try upgrading or reinstalling " SHORT_PROGRAM_NAME ")"); break; case CURLE_COULDNT_RESOLVE_HOST: error.append(" while connecting to " + sessionState.configRlz.url + " (check your DNS)"); break; case CURLE_COULDNT_CONNECT: if (sessionState.config["proxy_url"].isNull()) { error.append(" for " + sessionState.configRlz.url + " " POSSIBLE_MITM_RESOLUTION); } else { error.append(" for " + sessionState.configRlz.url + " using proxy " + sessionState.config["proxy_url"].asString() + " (if this error persists check your firewall and/or proxy settings)"); } break; case CURLE_COULDNT_RESOLVE_PROXY: error.append(" for proxy address " + sessionState.config["proxy_url"].asString()); break; #if LIBCURL_VERSION_NUM < 0x073e00 case CURLE_SSL_CACERT: // Peer certificate cannot be authenticated with given / known CA certificates. This would happen // for MITM but could also be a truststore issue. #endif case CURLE_PEER_FAILED_VERIFICATION: // The remote server's SSL certificate or SSH md5 fingerprint was deemed not OK. error.append(" while connecting to " + sessionState.configRlz.url + "; check that your connection is secure and that the" " truststore is valid. If the problem persists, you can also try upgrading" " or reinstalling " PROGRAM_NAME); break; case CURLE_SSL_CACERT_BADFILE: error.append(" while connecting to " + sessionState.configRlz.url + " "); if (!sessionState.config["proxy_url"].isNull()) { error.append("using proxy "); error.append(sessionState.config["proxy_url"].asString()); error.append(" "); } error.append("; this might happen if the nss backend is installed for" " libcurl instead of GnuTLS or OpenSSL. If the problem persists, you can also try upgrading" " or reinstalling " PROGRAM_NAME); break; // Fallthroughs to default: case CURLE_SSL_CONNECT_ERROR: // A problem occurred somewhere in the SSL/TLS handshake. Not sure what's up, but in this case the // error buffer (printed in DEBUG) should pinpoint the problem slightly more. case CURLE_OPERATION_TIMEDOUT: // This is not a normal connect timeout, there are some refs to it occurring while downloading large // files, but we don't do that so fall through to default. default: error.append(" while connecting to " + sessionState.configRlz.url + " "); if (!sessionState.config["proxy_url"].isNull()) { error.append("using proxy "); error.append(sessionState.config["proxy_url"].asString()); error.append(" "); } error.append(POSSIBLE_MITM_RESOLUTION); break; } logUpdateFail(error); #if !(BOOST_OS_MACOS && PRE_HIGH_SIERRA) unsigned long cryptoErrorCode = ERR_get_error(); if (cryptoErrorCode == 0) { logUpdateFailAdditional("CURLcode" + toString(code)); } else { char buf[500]; ERR_error_string(cryptoErrorCode, buf); logUpdateFailAdditional("CURLcode: " + toString(code) + ", Crypto: " + toString(cryptoErrorCode) + " " + buf); } #endif } void logUpdateFailHttp(const SessionState &sessionState, int httpCode) { string error; switch (httpCode) { case 404: error.append("url not found: " + sessionState.configRlz.url + " " POSSIBLE_MITM_RESOLUTION); break; case 403: error.append("request forbidden by server " POSSIBLE_MITM_RESOLUTION); break; case 503: error.append("server temporarily unavailable, try again later"); break; case 429: error.append("rate limit hit for your IP, try again later"); break; case 400: error.append("request corrupted or not understood " POSSIBLE_MITM_RESOLUTION); break; case 422: error.append("request content was corrupted or not understood " POSSIBLE_MITM_RESOLUTION); break; default: error = "HTTP " + toString(httpCode) + " while connecting to " + sessionState.configRlz.url + " " POSSIBLE_MITM_RESOLUTION; break; } logUpdateFail(error); } void logUpdateFailResponse(string error, string responseData) { logUpdateFail("error in server response (" + error + "). If this error persists, check your connection security and try upgrading " SHORT_PROGRAM_NAME); logUpdateFailAdditional(responseData); } /** * POST a bodyJsonString using a client certificate, and receive the response in responseData. * * May allocate chunk data for setting Content-Type, receiver should deallocate with curl_slist_free_all(). */ CURLcode prepareCurlPOST(CURL *curl, SessionState &sessionState, const string &bodyJsonString, string *responseData, struct curl_slist **chunk) { CURLcode code; // Hint for advanced debugging: curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); if (CURLE_OK != (code = curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1))) { return code; } if (CURLE_OK != (code = curl_easy_setopt(curl, CURLOPT_URL, sessionState.configRlz.url.c_str()))) { return code; } if (CURLE_OK != (code = curl_easy_setopt(curl, CURLOPT_HTTPGET, 0))) { return code; } if (CURLE_OK != (code = curl_easy_setopt(curl, CURLOPT_POSTFIELDS, bodyJsonString.c_str()))) { return code; } if (CURLE_OK != (code = curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, bodyJsonString.length()))) { return code; } *chunk = curl_slist_append(NULL, "Content-Type: application/json"); if (CURLE_OK != (code = curl_easy_setopt(curl, CURLOPT_HTTPHEADER, *chunk))) { return code; } #if BOOST_OS_MACOS && PRE_HIGH_SIERRA // preauth the security update check key in the user's keychain (this is for libcurl's benefit because they don't bother to authorize themselves to use the keys they import) if (!crypto.preAuthKey(clientCertPath.c_str(), CLIENT_CERT_PWD, CLIENT_CERT_LABEL)) { return CURLE_SSL_CERTPROBLEM; } if (CURLE_OK != (code = curl_easy_setopt(curl, CURLOPT_SSLCERTTYPE, "P12"))) { return code; } if (CURLE_OK != (code = curl_easy_setopt(curl, CURLOPT_SSLCERTPASSWD, CLIENT_CERT_PWD))) { return code; } #else if (CURLE_OK != (code = curl_easy_setopt(curl, CURLOPT_SSLCERTTYPE, "PEM"))) { return code; } #endif if (CURLE_OK != (code = curl_easy_setopt(curl, CURLOPT_SSLCERT, clientCertPath.c_str()))) { return code; } // These should be on by default, but make sure. if (CURLE_OK != (code = curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L))) { return code; } if (CURLE_OK != (code = curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L))) { return code; } // Technically we could use CURLOPT_SSL_VERIFYSTATUS to check for server cert revocation, but // we want to support older versions. We don't trust the server purely based on the server cert // anyway (it needs to prove by signature later on). if (CURLE_OK != (code = curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, receiveResponseBytes))) { return code; } if (CURLE_OK != (code = curl_easy_setopt(curl, CURLOPT_WRITEDATA, responseData))) { return code; } if (CURLE_OK != (code = setCurlProxy(curl, sessionState.configRlz.proxyInfo))) { return code; } // setopt failure(s) below don't abort the check. curl_easy_setopt(curl, CURLOPT_TIMEOUT, 180); return CURLE_OK; } bool verifyFileReadable(char *filename) { FILE *fhnd = fopen(filename, "rb"); if (fhnd == NULL) { return false; } fclose(fhnd); return true; } static size_t receiveResponseBytes(void *buffer, size_t size, size_t nmemb, void *userData) { string *responseData = (string *) userData; responseData->append((const char *) buffer, size * nmemb); return size * nmemb; } public: // Dependencies ResourceLocator *resourceLocator; SecurityUpdateChecker(const Schema &schema, const Json::Value &initialConfig, const ConfigKit::Translator &translator = ConfigKit::DummyTranslator()) : config(schema, initialConfig, translator), configRlz(config), updateCheckThread(NULL), resourceLocator(NULL) { } virtual ~SecurityUpdateChecker() { if (updateCheckThread != NULL) { updateCheckThread->interrupt_and_join(); delete updateCheckThread; } } void initialize() { if (resourceLocator == NULL) { throw RuntimeException("resourceLocator must be non-NULL"); } #if BOOST_OS_MACOS && PRE_HIGH_SIERRA clientCertPath = resourceLocator->getResourcesDir() + "/update_check_client_cert.p12"; #else clientCertPath = resourceLocator->getResourcesDir() + "/update_check_client_cert.pem"; #endif serverPubKeyPath = resourceLocator->getResourcesDir() + "/update_check_server_pubkey.pem"; } /** * Starts a periodic check, as dictated by the "interval" config option. For each check, the * server may increase/decrease (within limits) the period until the next check (using the * backoff parameter in the response). * * Assumes curl_global_init() was already performed. */ void start() { updateCheckThread = new oxt::thread( boost::bind(&SecurityUpdateChecker::threadMain, this), "Security update checker", 1024 * 512 ); } /** * All error log methods eventually lead here, except for the additional below. */ virtual void logUpdateFail(string error) { unsigned int checkIntervalSec; { boost::lock_guard<boost::mutex> l(configSyncher); checkIntervalSec = config["interval"].asUInt(); } P_ERROR("Security update check failed: " << error << " (next check in " << (checkIntervalSec / (60*60)) << " hours)"); } /** * Logs additional information at a lower loglevel so that it only spams when explicitely requested via loglevel. */ virtual void logUpdateFailAdditional(string additional) { P_DEBUG(additional); } virtual void logUpdateSuccess(int update, string success) { if (update == 0) { P_NOTICE(success); } else { P_ERROR(success); } } virtual void logUpdateSuccessAdditional(string additional) { P_ERROR(additional); } virtual CURLcode sendAndReceive(CURL *curl, string *responseData, long *responseCode) { CURLcode code; if (CURLE_OK != (code = curl_easy_perform(curl))) { return code; } return curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, responseCode); } virtual bool fillNonce(string &nonce) { return crypto.generateAndAppendNonce(nonce); } /** * Sends POST to the configured URL (via SSL, with client cert) containing: * {"version":"<passenger version>", "nonce":"<random nonce>"} * The response will be: * {"data":base64(data), "signature":base64(signature)}, where: * - signature should be from server we trust and match base64(data), * - data is {"nonce":"<reflected>", "update":0 or 1, "version":"<version>", "log": "<log msg>", "backoff":"<backoff>"} * - reflected nonce should match what we POSTed * - if update is 1 then <version> is logged as the recommended version to upgrade to * - <log msg> (if present) is written to the log * - <backoff> (minutes) is added to our default next check time */ int checkAndLogSecurityUpdate() { int backoffMin = 0; // 0. Copy current configuration boost::unique_lock<boost::mutex> l(configSyncher); SessionState sessionState(config, configRlz); l.unlock(); if (sessionState.config["disabled"].asBool()) { P_INFO("Security update checking disabled; skipping check"); return backoffMin; } // 1. Assemble data to send Json::Value bodyJson; bodyJson["passenger_version"] = PASSENGER_VERSION; bodyJson["server_integration"] = sessionState.config["server_identifier"]; bodyJson["server_version"] = sessionState.config["web_server_version"]; bodyJson["curl_static"] = isCurlStaticallyLinked(); string nonce; if (!fillNonce(nonce)) { logUpdateFail("fillNonce() error"); return backoffMin; } bodyJson["nonce"] = nonce; // against replay attacks // 2. Send and get response CURL *curl = curl_easy_init(); if (curl == NULL) { logUpdateFail("curl_easy_init() error"); return backoffMin; } struct curl_slist *chunk = NULL; char *signatureChars = NULL; char *dataChars = NULL; do { // for resource cleanup string responseData; long responseCode; CURLcode code; if (!verifyFileReadable((char *) clientCertPath.c_str())) { logUpdateFail("File not readable: " + clientCertPath); break; } if (CURLE_OK != (code = setCurlDefaultCaInfo(curl))) { logUpdateFailCurl(sessionState, code); break; } if (!sessionState.configRlz.certificatePath.empty()) { curl_easy_setopt(curl, CURLOPT_CAINFO, sessionState.configRlz.certificatePath.c_str()); } string bodyJsonString = bodyJson.toStyledString(); if (CURLE_OK != (code = prepareCurlPOST(curl, sessionState, bodyJsonString, &responseData, &chunk))) { logUpdateFailCurl(sessionState, code); break; } P_DEBUG("sending: " << bodyJsonString); if (CURLE_OK != (code = sendAndReceive(curl, &responseData, &responseCode))) { logUpdateFailCurl(sessionState, code); break; } // 3a. Verify response: HTTP code if (responseCode != 200) { logUpdateFailHttp(sessionState, (int) responseCode); break; } Json::Reader reader; Json::Value responseJson; if (!reader.parse(responseData, responseJson, false)) { logUpdateFailResponse("json parse", responseData); break; } P_DEBUG("received: " << responseData); // 3b. Verify response: signature if (!responseJson.isObject() || !responseJson["data"].isString() || !responseJson["signature"].isString()) { logUpdateFailResponse("missing response fields", responseData); break; } string signature64 = responseJson["signature"].asString(); string data64 = responseJson["data"].asString(); signatureChars = (char *)malloc(modp_b64_decode_len(signature64.length())); dataChars = (char *)malloc(modp_b64_decode_len(data64.length()) + 1); if (signatureChars == NULL || dataChars == NULL) { logUpdateFailResponse("out of memory", responseData); break; } int signatureLen; signatureLen = modp_b64_decode(signatureChars, signature64.c_str(), signature64.length()); if (signatureLen <= 0) { logUpdateFailResponse("corrupted signature", responseData); break; } if (!crypto.verifySignature(serverPubKeyPath, signatureChars, signatureLen, data64)) { logUpdateFailResponse("untrusted or forged signature", responseData); break; } // 3c. Verify response: check required fields, nonce int dataLen; dataLen = modp_b64_decode(dataChars, data64.c_str(), data64.length()); if (dataLen <= 0) { logUpdateFailResponse("corrupted data", data64.c_str()); break; } dataChars[dataLen] = '\0'; Json::Value responseDataJson; if (!reader.parse(dataChars, responseDataJson, false)) { logUpdateFailResponse("unparseable data", dataChars); break; } P_DEBUG("data content (signature OK): " << responseDataJson.toStyledString()); if (!responseDataJson.isObject() || !responseDataJson["update"].isInt() || !responseDataJson["nonce"].isString()) { logUpdateFailResponse("missing data fields", responseData); break; } if (nonce != responseDataJson["nonce"].asString()) { logUpdateFailResponse("nonce mismatch, possible replay attack", responseData); break; } // 4. The main point: is there an update, and when is the next check? int update = responseDataJson["update"].asInt(); if (responseDataJson["backoff"].isInt()) { backoffMin = responseDataJson["backoff"].asInt(); } if (update == 1 && !responseDataJson["version"].isString()) { logUpdateFailResponse("update available, but version field missing", responseData); break; } if (update == 0) { unsigned int checkIntervalSec; { boost::lock_guard<boost::mutex> l(configSyncher); checkIntervalSec = config["interval"].asUInt(); } logUpdateSuccess(update, "Security update check: no update found (next check in " + toString(checkIntervalSec / (60*60)) + " hours)"); } else { logUpdateSuccess(update, "A security update is available for your version (" PASSENGER_VERSION ") of " PROGRAM_NAME ". We strongly recommend upgrading to version " + responseDataJson["version"].asString() + "."); } // 5. Shown independently of whether there is an update so that the server can provide general warnings // (e.g. about server-side detected MITM attack) if (responseDataJson["log"].isString()) { string additional = responseDataJson["log"].asString(); if (additional.length() > 0) { logUpdateSuccessAdditional("Additional security update check information: " + additional); } } } while (false); #if BOOST_OS_MACOS && PRE_HIGH_SIERRA // remove the security update check key from the user's keychain so that if we are stopped/crash and are upgraded or reinstalled before restarting we don't have permission problems crypto.killKey(CLIENT_CERT_LABEL); #endif if (signatureChars) { free(signatureChars); } if (dataChars) { free(dataChars); } curl_slist_free_all(chunk); curl_easy_cleanup(curl); return backoffMin; } bool prepareConfigChange(const Json::Value &updates, vector<ConfigKit::Error> &errors, ConfigChangeRequest &req) { { boost::lock_guard<boost::mutex> l(configSyncher); req.config.reset(new ConfigKit::Store(config, updates, errors)); } if (errors.empty()) { req.configRlz.reset(new ConfigRealization(*req.config)); } return errors.empty(); } void commitConfigChange(ConfigChangeRequest &req) BOOST_NOEXCEPT_OR_NOTHROW { boost::lock_guard<boost::mutex> l(configSyncher); config.swap(*req.config); configRlz.swap(*req.configRlz); } Json::Value inspectConfig() const { boost::lock_guard<boost::mutex> l(configSyncher); return config.inspect(); } }; } // namespace Passenger #endif /* _PASSENGER_SECURITY_UPDATE_CHECKER_H_ */ SpawningKit/ErrorRenderer.h 0000644 00000010400 14756456557 0011756 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2014-2018 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef _PASSENGER_SPAWNING_KIT_ERROR_RENDERER_H_ #define _PASSENGER_SPAWNING_KIT_ERROR_RENDERER_H_ #include <string> #include <map> #include <cctype> #include <jsoncpp/json.h> #include <Constants.h> #include <StaticString.h> #include <FileTools/FileManip.h> #include <StrIntTools/Template.h> #include <Core/SpawningKit/Context.h> #include <Core/SpawningKit/Exceptions.h> namespace Passenger { namespace SpawningKit { using namespace std; using namespace boost; using namespace oxt; class ErrorRenderer { private: string templatesDir; public: ErrorRenderer(const Context &context) { templatesDir = context.resourceLocator->getResourcesDir() + "/templates/error_renderer"; } string renderWithDetails(const SpawningKit::SpawnException &e) const { StringMap<StaticString> params; string htmlFile = templatesDir + "/with_details/src/index.html.template"; string cssFile = templatesDir + "/with_details/dist/styles.css"; string jsFile = templatesDir + "/with_details/dist/bundle.js"; string cssContent = unsafeReadFile(cssFile); string jsContent = unsafeReadFile(jsFile); Json::Value spec; spec["program_name"] = PROGRAM_NAME; spec["short_program_name"] = SHORT_PROGRAM_NAME; spec["config"] = e.getConfig().getNonConfidentialFieldsToPassToApp(); spec["journey"] = e.getJourney().inspectAsJson(); spec["error"] = e.inspectBasicInfoAsJson(); spec["diagnostics"]["system_wide"] = e.inspectSystemWideDetailsAsJson(); spec["diagnostics"]["core_process"] = e.inspectParentProcessDetailsAsJson(); if (e.getJourney().getType() == SPAWN_THROUGH_PRELOADER) { spec["diagnostics"]["preloader_process"] = e.inspectPreloaderProcessDetailsAsJson(); } spec["diagnostics"]["subprocess"] = e.inspectSubprocessDetailsAsJson(); string specContent = spec.toStyledString(); params.set("CSS", cssContent); params.set("JS", jsContent); params.set("TITLE", "Web application could not be started"); params.set("SPEC", specContent); return Template::apply(unsafeReadFile(htmlFile), params); } string renderWithoutDetails(const SpawningKit::SpawnException &e) const { StringMap<StaticString> params; string htmlFile = templatesDir + "/without_details/src/index.html.template"; string cssFile = templatesDir + "/without_details/dist/styles.css"; string jsFile = templatesDir + "/without_details/dist/bundle.js"; string cssContent = unsafeReadFile(cssFile); string jsContent = unsafeReadFile(jsFile); params.set("CSS", cssContent); params.set("JS", jsContent); params.set("TITLE", "Web application could not be started"); params.set("SUMMARY", e.getSummary()); params.set("ERROR_ID", e.getId()); params.set("PROGRAM_NAME", PROGRAM_NAME); params.set("SHORT_PROGRAM_NAME", SHORT_PROGRAM_NAME); params.set("PROGRAM_WEBSITE", PROGRAM_WEBSITE); params.set("PROGRAM_AUTHOR", PROGRAM_AUTHOR); return Template::apply(unsafeReadFile(htmlFile), params); } }; } // namespace SpawningKit } // namespace Passenger #endif /* _PASSENGER_SPAWNING_KIT_ERROR_RENDERER_H_ */ SpawningKit/Factory.h 0000644 00000007530 14756456557 0010617 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2011-2017 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef _PASSENGER_SPAWNING_KIT_FACTORY_H_ #define _PASSENGER_SPAWNING_KIT_FACTORY_H_ #include <Core/SpawningKit/Context.h> #include <Core/SpawningKit/SmartSpawner.h> #include <Core/SpawningKit/DirectSpawner.h> #include <Core/SpawningKit/DummySpawner.h> namespace Passenger { namespace SpawningKit { using namespace std; using namespace boost; using namespace oxt; class Factory { private: boost::mutex syncher; Context *context; DummySpawnerPtr dummySpawner; SpawnerPtr tryCreateSmartSpawner(const AppPoolOptions &options) { string dir = context->resourceLocator->getHelperScriptsDir(); vector<string> preloaderCommand; if (options.appType == "ruby" || options.appType == "rack") { preloaderCommand.push_back(options.ruby); preloaderCommand.push_back(dir + "/rack-preloader.rb"); } else { return SpawnerPtr(); } return boost::make_shared<SmartSpawner>(context, preloaderCommand, options); } public: unsigned int spawnerCreationSleepTime; Factory(Context *_context) : context(_context), spawnerCreationSleepTime(0) { if (context->debugSupport != NULL) { spawnerCreationSleepTime = context->debugSupport->spawnerCreationSleepTime; } } virtual ~Factory() { } virtual SpawnerPtr create(const AppPoolOptions &options) { if (options.spawnMethod == "smart" || options.spawnMethod == "smart-lv2") { SpawnerPtr spawner = tryCreateSmartSpawner(options); if (spawner == NULL) { spawner = boost::make_shared<DirectSpawner>(context); } return spawner; } else if (options.spawnMethod == "direct" || options.spawnMethod == "conservative") { boost::shared_ptr<DirectSpawner> spawner = boost::make_shared<DirectSpawner>( context); return spawner; } else if (options.spawnMethod == "dummy") { syscalls::usleep(spawnerCreationSleepTime); return getDummySpawner(); } else { throw ArgumentException("Unknown spawn method '" + options.spawnMethod + "'"); } } /** * SpawnerFactory always returns the same DummyFactory object upon * creating a dummy spawner. This allows unit tests to easily * set debugging options on the spawner. */ DummySpawnerPtr getDummySpawner() { boost::lock_guard<boost::mutex> l(syncher); if (dummySpawner == NULL) { dummySpawner = boost::make_shared<DummySpawner>(context); } return dummySpawner; } /** * All created Spawner objects share the same Context object. */ Context *getContext() const { return context; } }; typedef boost::shared_ptr<Factory> FactoryPtr; } // namespace SpawningKit } // namespace Passenger #endif /* _PASSENGER_SPAWNING_KIT_FACTORY_H_ */ SpawningKit/Result.h 0000644 00000013412 14756456557 0010462 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2014-2018 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef _PASSENGER_SPAWNING_KIT_RESULT_H_ #define _PASSENGER_SPAWNING_KIT_RESULT_H_ #include <string> #include <vector> #include <sys/types.h> #include <FileDescriptor.h> #include <Exceptions.h> #include <SystemTools/SystemTime.h> #include <ConfigKit/ConfigKit.h> #include <Core/SpawningKit/Context.h> #include <Core/SpawningKit/Config.h> namespace Passenger { namespace SpawningKit { using namespace std; /** * Represents the result of a spawning operation. * * - begin hinted parseable class - */ class Result { public: struct Socket { struct Schema: public ConfigKit::Schema { Schema() { using namespace Passenger::ConfigKit; add("address", STRING_TYPE, REQUIRED); add("protocol", STRING_TYPE, REQUIRED); add("description", STRING_TYPE, OPTIONAL); add("concurrency", INT_TYPE, OPTIONAL, -1); add("accept_http_requests", BOOL_TYPE, OPTIONAL, false); finalize(); } }; string address; string protocol; string description; /** * Special values: * 0 = unlimited concurrency * < 0 = unknown */ int concurrency; bool acceptHttpRequests; Socket() : concurrency(-1), acceptHttpRequests(false) { } Socket(const Schema &schema, const Json::Value &values) { ConfigKit::Store store(schema); vector<ConfigKit::Error> errors; if (!store.update(values, errors)) { throw ArgumentException("Invalid initial values: " + toString(errors)); } address = store["address"].asString(); protocol = store["protocol"].asString(); if (!store["description"].isNull()) { description = store["description"].asString(); } concurrency = store["concurrency"].asInt(); acceptHttpRequests = store["accept_http_requests"].asBool(); } Json::Value inspectAsJson() const { Json::Value doc; doc["address"] = address; doc["protocol"] = protocol; if (!description.empty()) { doc["description"] = description; } doc["concurrency"] = concurrency; doc["accept_http_requests"] = acceptHttpRequests; return doc; } }; enum Type { UNKNOWN, GENERIC, KURIA, AUTO_SUPPORTED, /** * Indicates that this Process does not refer to a real OS * process. The sockets in the socket list are fake and need not be deleted. * Set to true by DummySpawner, used during unit tests. */ DUMMY }; private: void validate_autoGeneratedCode(vector<StaticString> &internalFieldErrors, vector<StaticString> &appSuppliedFieldErrors) const; public: /****** Fields supplied by HandshakePrepare and HandshakePerform ******/ /** * @hinted_parseable * @require result.pid != -1 */ pid_t pid; /** * @hinted_parseable */ Type type; /** * @hinted_parseable * @require_non_empty */ string gupid; /** * @hinted_parseable */ string codeRevision; /** * @hinted_parseable */ FileDescriptor stdinFd; /** * @hinted_parseable */ FileDescriptor stdoutAndErrFd; /** * @hinted_parseable * @require result.spawnStartTime != 0 */ unsigned long long spawnStartTime; /** * @hinted_parseable * @require result.spawnEndTime != 0 */ unsigned long long spawnEndTime; /** * @hinted_parseable * @require result.spawnStartTimeMonotonic != 0 */ MonotonicTimeUsec spawnStartTimeMonotonic; /** * @hinted_parseable * @require result.spawnEndTimeMonotonic != 0 */ MonotonicTimeUsec spawnEndTimeMonotonic; /****** Fields supplied by the app ******/ vector<Socket> sockets; Result() : pid(-1), type(UNKNOWN), spawnStartTime(0), spawnEndTime(0), spawnStartTimeMonotonic(0), spawnEndTimeMonotonic(0) { } void initialize(const Context &context, const Config * const config) { gupid = integerToHex(SystemTime::get() / 60) + "-" + context.randomGenerator->generateAsciiString(10); spawnStartTime = SystemTime::getUsec(); spawnStartTimeMonotonic = SystemTime::getMonotonicUsec(); } bool validate(vector<StaticString> &internalFieldErrors, vector<StaticString> &appSuppliedFieldErrors) const { validate_autoGeneratedCode(internalFieldErrors, appSuppliedFieldErrors); if (type == UNKNOWN) { internalFieldErrors.push_back(P_STATIC_STRING("type may not be unknown")); } if (sockets.empty()) { appSuppliedFieldErrors.push_back(P_STATIC_STRING("sockets are not supplied")); } return internalFieldErrors.empty() && appSuppliedFieldErrors.empty(); } }; // - end hinted parseable class - } // namespace SpawningKit } // namespace Passenger #include <Core/SpawningKit/Result/AutoGeneratedCode.h> #endif /* _PASSENGER_SPAWNING_KIT_RESULT_H_ */ SpawningKit/Context.h 0000644 00000013130 14756456557 0010625 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2011-2018 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef _PASSENGER_SPAWNING_KIT_CONTEXT_H_ #define _PASSENGER_SPAWNING_KIT_CONTEXT_H_ #include <boost/function.hpp> #include <boost/shared_ptr.hpp> #include <boost/make_shared.hpp> #include <string> #include <algorithm> #include <cstddef> #include <ResourceLocator.h> #include <RandomGenerator.h> #include <Exceptions.h> #include <WrapperRegistry/Registry.h> #include <JsonTools/JsonUtils.h> #include <ConfigKit/Store.h> namespace Passenger { namespace ApplicationPool2 { class Options; } } namespace Passenger { namespace SpawningKit { using namespace std; class HandshakePrepare; typedef Passenger::ApplicationPool2::Options AppPoolOptions; class Context { public: class Schema: public ConfigKit::Schema { private: static void validate(const ConfigKit::Store &config, vector<ConfigKit::Error> &errors) { if (config["min_port_range"].asUInt() > config["max_port_range"].asUInt()) { errors.push_back(ConfigKit::Error( "'{{min_port_range}}' must be equal to or smaller than {{max_port_range}}")); } if (config["min_port_range"].asUInt() > 65535) { errors.push_back(ConfigKit::Error( "{{min_port_range}} must be equal to or less than 65535")); } if (config["max_port_range"].asUInt() > 65535) { errors.push_back(ConfigKit::Error( "{{max_port_range}} must be equal to or less than 65535")); } } public: Schema() { using namespace ConfigKit; add("min_port_range", UINT_TYPE, OPTIONAL, 5000); add("max_port_range", UINT_TYPE, OPTIONAL, 65535); addValidator(validate); finalize(); } }; struct DebugSupport { // Used by DummySpawner and SpawnerFactory. unsigned int dummyConcurrency; unsigned long long dummySpawnDelay; unsigned long long spawnerCreationSleepTime; DebugSupport() : dummyConcurrency(1), dummySpawnDelay(0), spawnerCreationSleepTime(0) { } }; private: friend class HandshakePrepare; mutable boost::mutex syncher; /****** Context-global configuration ******/ // Actual configuration store. ConfigKit::Store config; // Below follows cached values. // Other. unsigned int minPortRange, maxPortRange; /****** Working state ******/ bool finalized; unsigned int nextPort; void updateConfigCache() { minPortRange = config["min_port_range"].asUInt(); maxPortRange = config["max_port_range"].asUInt(); nextPort = std::max(std::min(nextPort, maxPortRange), minPortRange); } public: /****** Dependencies ******/ const ResourceLocator *resourceLocator; const WrapperRegistry::Registry *wrapperRegistry; RandomGeneratorPtr randomGenerator; string integrationMode; string instanceDir; string spawnDir; DebugSupport *debugSupport; //UnionStation::ContextPtr unionStationContext; Context(const Schema &schema, const Json::Value &initialConfig = Json::Value()) : config(schema), finalized(false), nextPort(0), resourceLocator(NULL), wrapperRegistry(NULL), debugSupport(NULL) { vector<ConfigKit::Error> errors; if (!config.update(initialConfig, errors)) { throw ArgumentException("Invalid initial configuration: " + toString(errors)); } updateConfigCache(); } Json::Value previewConfigUpdate(const Json::Value &updates, vector<ConfigKit::Error> &errors) { boost::lock_guard<boost::mutex> l(syncher); return config.previewUpdate(updates, errors); } bool configure(const Json::Value &updates, vector<ConfigKit::Error> &errors) { boost::lock_guard<boost::mutex> l(syncher); if (config.update(updates, errors)) { updateConfigCache(); return true; } else { return false; } } Json::Value inspectConfig() const { boost::lock_guard<boost::mutex> l(syncher); return config.inspect(); } void finalize() { TRACE_POINT(); if (resourceLocator == NULL) { throw RuntimeException("ResourceLocator not initialized"); } if (wrapperRegistry == NULL) { throw RuntimeException("WrapperRegistry not initialized"); } if (randomGenerator == NULL) { randomGenerator = boost::make_shared<RandomGenerator>(); } if (integrationMode.empty()) { throw RuntimeException("integrationMode not set"); } finalized = true; } bool isFinalized() const { return finalized; } }; typedef boost::shared_ptr<Context> ContextPtr; } // namespace SpawningKit } // namespace Passenger #endif /* _PASSENGER_SPAWNING_KIT_CONTEXT_H_ */ SpawningKit/UserSwitchingRules.h 0000644 00000017473 14756456557 0013030 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2011-2018 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef _PASSENGER_SPAWNING_KIT_USER_SWITCHING_RULES_H_ #define _PASSENGER_SPAWNING_KIT_USER_SWITCHING_RULES_H_ #include <sys/types.h> #include <pwd.h> #include <grp.h> #include <unistd.h> #include <string> #include <algorithm> #include <boost/shared_array.hpp> #include <oxt/backtrace.hpp> #include <oxt/system_calls.hpp> #include <WrapperRegistry/Registry.h> #include <Exceptions.h> #include <Utils.h> #include <Core/SpawningKit/Context.h> #include <FileTools/PathManip.h> #include <SystemTools/UserDatabase.h> namespace Passenger { namespace SpawningKit { using namespace std; using namespace boost; using namespace oxt; struct UserSwitchingInfo { bool enabled; string username; string groupname; uid_t uid; gid_t gid; struct passwd lveUserPwd, *lveUserPwdComplete; boost::shared_array<char> lveUserPwdStrBuf; }; inline UserSwitchingInfo prepareUserSwitching(const AppPoolOptions &options, const WrapperRegistry::Registry &wrapperRegistry) { TRACE_POINT(); UserSwitchingInfo info; if (geteuid() != 0) { struct passwd &pwd = info.lveUserPwd; boost::shared_array<char> &strings = info.lveUserPwdStrBuf; struct passwd *userInfo; long bufSize; // _SC_GETPW_R_SIZE_MAX is not a maximum: // http://tomlee.co/2012/10/problems-with-large-linux-unix-groups-and-getgrgid_r-getgrnam_r/ bufSize = std::max<long>(1024 * 128, sysconf(_SC_GETPW_R_SIZE_MAX)); strings.reset(new char[bufSize]); userInfo = (struct passwd *) NULL; if (getpwuid_r(geteuid(), &pwd, strings.get(), bufSize, &userInfo) != 0 || userInfo == (struct passwd *) NULL) { throw RuntimeException("Cannot get user database entry for user " + lookupSystemUsernameByUid(geteuid()) + "; it looks like your system's " + "user database is broken, please fix it."); } info.enabled = false; info.username = userInfo->pw_name; info.groupname = lookupSystemGroupnameByGid(userInfo->pw_gid, P_STATIC_STRING("%d")); info.uid = geteuid(); info.gid = getegid(); return info; } UPDATE_TRACE_POINT(); string defaultGroup; // This is the file that determines what user we lower privilege to. string referenceFile; struct passwd &pwd = info.lveUserPwd; boost::shared_array<char> &pwdBuf = info.lveUserPwdStrBuf; struct passwd *userInfo; struct group grp; gid_t groupId = (gid_t) -1; long pwdBufSize, grpBufSize; boost::shared_array<char> grpBuf; int ret; if (options.appType.empty()) { referenceFile = absolutizePath(options.appRoot); } else { referenceFile = absolutizePath(options.getStartupFile(wrapperRegistry), absolutizePath(options.appRoot)); } // _SC_GETPW_R_SIZE_MAX/_SC_GETGR_R_SIZE_MAX are not maximums: // http://tomlee.co/2012/10/problems-with-large-linux-unix-groups-and-getgrgid_r-getgrnam_r/ pwdBufSize = std::max<long>(1024 * 128, sysconf(_SC_GETPW_R_SIZE_MAX)); pwdBuf.reset(new char[pwdBufSize]); grpBufSize = std::max<long>(1024 * 128, sysconf(_SC_GETGR_R_SIZE_MAX)); grpBuf.reset(new char[grpBufSize]); if (options.defaultGroup.empty()) { struct passwd *info; struct group *group; info = (struct passwd *) NULL; ret = getpwnam_r(options.defaultUser.c_str(), &pwd, pwdBuf.get(), pwdBufSize, &info); if (ret != 0) { info = (struct passwd *) NULL; } if (info == (struct passwd *) NULL) { throw RuntimeException("Cannot get user database entry for username '" + options.defaultUser + "'"); } group = (struct group *) NULL; ret = getgrgid_r(info->pw_gid, &grp, grpBuf.get(), grpBufSize, &group); if (ret != 0) { group = (struct group *) NULL; } if (group == (struct group *) NULL) { throw RuntimeException(string("Cannot get group database entry for ") + "the default group belonging to username '" + options.defaultUser + "'"); } defaultGroup = group->gr_name; } else { defaultGroup = options.defaultGroup; } UPDATE_TRACE_POINT(); userInfo = (struct passwd *) NULL; if (!options.userSwitching) { // Keep userInfo at NULL so that it's set to defaultUser's UID. } else if (!options.user.empty()) { ret = getpwnam_r(options.user.c_str(), &pwd, pwdBuf.get(), pwdBufSize, &userInfo); if (ret != 0) { userInfo = (struct passwd *) NULL; } } else { struct stat buf; if (syscalls::lstat(referenceFile.c_str(), &buf) == -1) { int e = errno; throw SystemException("Cannot lstat(\"" + referenceFile + "\")", e); } ret = getpwuid_r(buf.st_uid, &pwd, pwdBuf.get(), pwdBufSize, &userInfo); if (ret != 0) { userInfo = (struct passwd *) NULL; } } if (userInfo == (struct passwd *) NULL || userInfo->pw_uid == 0) { userInfo = (struct passwd *) NULL; ret = getpwnam_r(options.defaultUser.c_str(), &pwd, pwdBuf.get(), pwdBufSize, &userInfo); if (ret != 0) { userInfo = (struct passwd *) NULL; } } UPDATE_TRACE_POINT(); if (!options.userSwitching) { // Keep groupId at -1 so that it's set to defaultGroup's GID. } else if (!options.group.empty()) { struct group *groupInfo = (struct group *) NULL; if (options.group == "!STARTUP_FILE!") { struct stat buf; if (syscalls::lstat(referenceFile.c_str(), &buf) == -1) { int e = errno; throw SystemException("Cannot lstat(\"" + referenceFile + "\")", e); } ret = getgrgid_r(buf.st_gid, &grp, grpBuf.get(), grpBufSize, &groupInfo); if (ret != 0) { groupInfo = (struct group *) NULL; } if (groupInfo != NULL) { groupId = buf.st_gid; } else { groupId = (gid_t) -1; } } else { ret = getgrnam_r(options.group.c_str(), &grp, grpBuf.get(), grpBufSize, &groupInfo); if (ret != 0) { groupInfo = (struct group *) NULL; } if (groupInfo != NULL) { groupId = groupInfo->gr_gid; } else { groupId = (gid_t) -1; } } } else if (userInfo != (struct passwd *) NULL) { groupId = userInfo->pw_gid; } if (groupId == 0 || groupId == (gid_t) -1) { OsGroup osGroup; if (lookupSystemGroupByName(defaultGroup, osGroup)) { groupId = osGroup.grp.gr_gid; } else if (looksLikePositiveNumber(defaultGroup)) { groupId = atoi(defaultGroup); } else { groupId = -1; } } UPDATE_TRACE_POINT(); if (userInfo == (struct passwd *) NULL) { throw RuntimeException("Cannot determine a user to lower privilege to"); } if (groupId == (gid_t) -1) { throw RuntimeException("Cannot determine a group to lower privilege to"); } UPDATE_TRACE_POINT(); info.enabled = true; info.username = userInfo->pw_name; info.groupname = lookupSystemGroupnameByGid(groupId, P_STATIC_STRING("%d")); info.uid = userInfo->pw_uid; info.gid = groupId; return info; } } // namespace SpawningKit } // namespace Passenger #endif /* _PASSENGER_SPAWNING_KIT_USER_SWITCHING_RULES_H_ */ SpawningKit/README.md 0000644 00000111220 14756456557 0010306 0 ustar 00 # About SpawningKit SpawningKit is a subsystem that handles the spawning of web application processes in a reliable manner. Spawning an application process is complex, involving many steps and with many failure scenarios. SpawningKit handles all this complexity while providing a simple interface that guarantees reliability. Here is how SpawningKit is used. The caller supplies various parameters such as where the application is located, what language it's written in, what environment variables to apply, etc. SpawningKit then spawns the application process, checks whether the application spawned properly or whether it encountered an error, and then either returns an object that describes the resulting process or throws an exception that describes the failure. Reliability and visibility are core features of SpawningKit. When SpawningKit returns, you know for sure whether the process started correctly or not. If the application did not start correctly, then the resulting exception describes the failure in a detailed enough manner that allows users to pinpoint the source of the problem. SpawningKit also enforces timeouts everywhere so that stuck processes are handled as well. **Table of contents**: * Important concepts and features - Generic vs SpawningKit-enabled applications - Wrappers - Preloaders - The start command - Summary with examples * API and implementation highlights - Context - Spawners (high-level API) - HandshakePrepare and HandshakePerform (low-level API) - Configuration object - Exception object - Journey - ErrorRenderer * Overview of the spawning journey - When spawning a process without a preloader - When starting a preloader - When spawning a process through a preloader - The Journey class - The preparation and the HandshakePrepare class - The handshake and the HandshakePerform class - The SpawnEnvSetupper * The work directory - Structure * Application response properties * The preloader protocol * Subprocess journey logging * Error reporting - Information sources - How an error report is presented - How supplied information affects error report generation * Mechanism for waiting until the application is up --- ## Important concepts and features ### Generic vs SpawningKit-enabled applications All applications | +-- Generic applications | (without explicit SpawningKit support; `genericApp = true`) | +-- SpawningKit-enabled applications (with explicit SpawningKit support; `genericApp = false`) | +-- Applications with SpawningKit support automatically injected | through a "wrapper"; no manual modifications | (`startsUsingWrapper = true`) | +-- Applications manually modified with SpawningKit support (`startsUsingWrapper = false`) SpawningKit can be used to spawn any web application, both those with and without explicit SpawningKit support. > A generic application corresponds to setting the SpawningKit config `genericApp = true`. When SpawningKit is used to spawn a generic application (without explicit SpawningKit support), the only requirement is that the application can be instructed to start and to listen on a specific TCP port on localhost. The user needs to specify a command string that tells SpawningKit how that is to be done. SpawningKit then looks for a free port that the application may use and executes the application using the supplied command string, telling it to listen on that specific port. (This approach is inspired by Heroku's Procfile system.) SpawningKit waits until the application is up by pinging the port. If the application fails (e.g. by terminating early or by not responding to pings in time) then SpawningKit will abort, reporting the application's stdout and stderr output. > A SpawningKit-enabled application corresponds to setting the SpawningKit config `genericApp = false`. Applications can also be modified with explicit SpawningKit support. Such applications can improve performance by telling SpawningKit that it wishes to listen on a Unix domain socket instead of a TCP socket; and they can provide more feedback about any spawning failures, such as with HTML-formatted error messages or by providing more information about where internally in the application or web framework the failure occurred. ### Wrappers As we said, apps with explicit SpawningKit support is preferred (nicer experience, better performance). There are two ways to add SpawningKit support to an app: 1. By manually modifying the application's code to add SpawningKit support. 2. By automatically injecting SpawningKit support into the app, without any manual code modifications. Option 2 is the most desirable, and is available to apps written in interpreted languages. This works by executing the application through a *wrapper* instead of directly. The wrapper, which is typically written in the same language as the app, loads the application and injects SpawningKit support. Passenger comes with a few wrappers for specific languages, but SpawningKit itself is more generic and requires the caller to specify which wrapper to use (if at all). For example, Ruby applications are typically spawned through the Passenger-supplied Ruby wrapper. The Ruby wrapper activates the Gemfile, loads the application, sets up a lightweight server that listens on a Unix domain socket, and reports this socket address back to SpawningKit. ### Preloaders Applications written in certain languages are able to save memory and to improve application startup time by using a technique called pre-forking. This works by starting an application process, and instead of using that process to handle requests, we use that process to fork (but not `exec()`) additional child processes that in turn are actually used for processing requests. In SpawningKit terminology, we call the former a "preloader". Processes that actually handle requests (and these processes may either be forked from a preloader or be spawned directly without a preloader) usually do not have a specific name. But for the sake of clarity, let's call the latter -- within the context of this document only -- "worker processes". Requests | | \ / . +-----------+ +------------------+ | Preloader | --- forks many ---> | Worker processes | +-----------+ +------------------+ Memory is saved because the preloader and its worker processes are able to share all memory that was already present in the preloader during forking, and that has not been modified in the worker processes. This concept is called Copy-on-Write (CoW) and works through the virtual memory system of modern operating systems. For example, in Ruby applications a significant amount of memory is taken up by the bytecode representation of dependent libraries (e.g. the Rails framework). Loading all the dependent libraries typically takes time in the order of many seconds. By using a preloader to fork worker processes (instead of starting the worker processes without a preloader), all worker processes can share the memory taken up by the dependent libraries, as well as the application code itself and possibly any resources that the preloder loaded (e.g. a geo-IP database loaded from a file). Forking a worker process from a preloader is also extremely fast, in the order of milliseconds -- much faster than starting a worker processes without a preloader. SpawningKit provides facilities to use this preforking technique. Obviously, this technique can only be used if the target programming language actually supports forking. This is the case with e.g. C, Ruby (using MRI) and Python (using CPython), but not with e.g. Node.js, Ruby (using JRuby), Go and anything running on the JVM. Using the preforking technique through SpawningKit requires either application code modifications, or the existance of a wrapper that supports this technique. ### The start command Regardless of whether SpawningKit is used to spawn an application directly with or without explicit SpawningKit support, and regardless of whether a wrapper is used and whether the application/wrapper can function as a preloader, SpawningKit asks the caller to supply a "start command" that tells it how to execute the wrapper or the application. SpawningKit then uses the handshaking procedure (see: "Overview of the spawning journey") to communicate with the wrapper/application whether it should start in preloader mode or not. ## API and implementation highlights ### Context Before using SpawningKit, one must create a Context object. A Context contains global state, global configuration and references dependencies that SpawningKit needs. Create a Context object, set some initial configuration, inject some dependencies, then call `finalize()`. ~~~c++ SpawningKit::Context::Schema schema; SpawningKit::Context context(schema); context.resourceLocator = ...; context.integrationMode = "standalone"; context.finalize(); ~~~ ### Spawners (high-level API) Use Spawners to spawn application processes. There are two main types of Spawners: * DirectSpawner, which corresponds to `PassengerSpawnMethod direct`. * SmartSpawner, which corresponds to `PassengerSpawnMethod smart`. See also [the background documentation on spawn methods](https://www.phusionpassenger.com/library/indepth/ruby/spawn_methods/) and the "Preloaders" section in this README. You can create Spawners directly, but it's recommended to use a Factory instead. A Factory accepts an ApplicationPool::Options object, and depending on `options.spawnMethod`, creates either a DirectSpawner or a SmartSpawner. Once you have obtained a Spawner object, call `spawn(options)` which spawns an application process. It returns a `Result` object. ~~~c++ ApplicationPool::options; options.appRoot = "/foo/bar/public"; options.appType = "rack"; options.spawnMethod = "smart"; ... SpawningKit::Factory factory(&context); SpawningKit::SpawnerPtr spawner = factory->create(options); SpawningKit::Result result = spawner->spawn(options); P_WARN("Application process spawned, PID is " << result.pid); ~~~ There is also a DummySpawner class, which is only used during unit tests. ### HandshakePrepare and HandshakePerform (low-level API) Inside SmartSpawner and DirectSpawner, HandshakePrepare and HandshakePerform are used to perform a lot of the heavy lifting. See "Overview of the spawning journey" -- HandshakePrepare and HandshakePerform are responsible for most of the stuff described there. In fact, DirectSpawner is just a thin wrapper around HandshakePrepare and HandshakePerform. It first runs HandshakePrepare, then forks a subprocess that executes SpawnEnvSetupper, and then (in the parent process) runs HandshakePerform. SmartSpawner is a bit bigger because it needs to implement the whole preloading mechanism (see section "Preloaders"), but it still uses HandshakePrepare and HandshakePerform to spawn the preloader, and to negotiate with the subprocess created by the preloader. ### Configuration object HandshakePrepare and HandshakePerform do not accept an ApplicationPool::Options object, but a SpawningKit::Config object. It contains the configuration that HandshakePrepare/Perform need to perform a single spawn. SmartSpawner and DirectSpawner internally convert an ApplicationPool::Options into a SpawningKit::Config. You can see this at work in `DirectSpawner::setConfigFromAppPoolOptions()` and `SmartSpawner::setConfigFromAppPoolOptions()`. ### Exception object If HandshakePrepare, HandshakePerform, or `Spawner::spawn()` fails, then they will throw a SpawningKit::SpawnException object. Learn more about what this exception represents in section "Error reporting". ### Journey The aforementioned exception object contains a SpawningKit::Journey object which describes how the spawning journey looked like and where in the journey something failed. It can be obtained through `exception.getJourney()`. Learn more about journeys in section "Overview of the spawning journey". ### Error renderer The ErrorRenderer class helps you render an error page, both one with and one without details. It uses the assets in `resources/templates/error_page`. ## Overview of the spawning journey Spawning a process can take one of three routes: * If we are spawning a worker process, then it is either (1) spawned through a preloader, or (2) it isn't. * We may also (3) spawn the application as a preloader instead of as a worker. We refer to the walking of this route (performing all the steps involved in a route) a "journey". Below follows an overview of the routes. In the following descriptions, "(In SpawningKit)" refers to the process that runs SpawningKit, which is typically the Passenger Core. ### When spawning a process without a preloader The journey looks like this when no preloader is used: (In SpawningKit) (In subprocess) Preparation | Fork subprocess --------------> Before first exec | | Handshake Execute SpawnEnvSetupper to setup spawn env (--before) | | Finish Load OS shell (if option enabled) | Execute SpawnEnvSetupper (--after) | Execute wrapper (if applicable) | Execute/load app | Start listening | Finish ### When starting a preloader The journey looks like this when starting a preloader: (In SpawningKit) (In subprocess) Preparation | Fork preloader --------------> Before first exec | | Handshake Execute SpawnEnvSetupper to setup spawn env (--before) | | Finish Load OS shell (if option enabled) | Execute SpawnEnvSetupper (--after) | Execute wrapper in preloader mode (if applicable) | Load application (and if applicable, do so in preloader mode) | Start listening for commands | Finish ### When spawning a process through a preloader The journey looks like this when using a preloader to spawn a process: (In SpawningKit) (In preloader) (In subprocess) Preparation | Tell preloader to spawn ------> Preparation | | Receive, process Fork ----------------> Preparation preloader response | | | Send response Start listening Handshake | | | Finish Finish Finish ### The Journey class The Journey class represents a journey. It records all the steps taken so far, which steps haven't been taken yet, at which step we failed, and how long each step took. A journey consists of a few parallel actors, with each actor having multiple steps and the ability to invoke another actor. Each step can be in one of three states: * Step has not started yet. Inside error pages, this will be visualized with an empty placeholder. * Step is currently in progress. Will be visualized with a spinner. * Step has already been performed successfully. Will be visualized with a green tick. * Step has failed. Will be visualized with a red mark. Steps that have performed successfully or failed also have an associated begin time. The duration of each step is inferred from the begin time of that step, vs either the end time of that step or (if available) the begin time of the next step. ### The preparation and the HandshakePrepare class Inside the process running SpawningKit, before forking a subprocess (regardless of whether that is going to be a preloader or a worker), various preparation needs to be done. This preparation work is implemented in Handshake/Prepare.h, in the HandshakePrepare class. Here is a list of the work involved in preparation: * Creating a temporary directory for the purpose of performing a handshake with the subprocess. This directory is called a "work directory". Learn more in sections "The handshake and the HandshakePerform class" and "The work directory". **Note**: the "work directory" in this context refers to this directory, not to the Unix concept of current working directory (`getpwd()`). This directory also has got nothing to do with the instance directory that Passenger uses to keep track of running Passenger instances; that is a separate concept and mechanism. * If the application is not SpawningKit-enabled, or if the caller explicitly instructed so, HandshakePrepare finds a free port for the worker process to listen on. This port number will be passed to the worker process. * Dumping, into the work directory, important information that the subprocess should know of. For example: whether it's going to be started in development or production mode, the process title to assume, etc. This information is called the _spawn arguments_. * Calculating which exact arguments need to be passed to the `exec()` call. Because it's unsafe to do this after forking. ### The handshake and the HandshakePerform class Once a process (whether preloader or worker) is spawned, SpawningKit needs to wait until it's up. If the spawning failed for whatever reason, then SpawningKit needs to infer that reason from information that the subprocess may have dumped into the work directory, and from the stdout/stderr output. This is implemented in Handshake/Perform.h, in the HandshakePerform class. ### The SpawnEnvSetupper The first thing the subprocess does is execute the SpawnEnvSetupper (which is contained inside PassengerAgent and can be invoked through a specific argument). This program performs various basic preparation in the subprocess such as: * Changing the current working directory to that of the application. * Setting environment variables, ulimits, etc. * Changing the UID and GID of the process. It does all this by reading arguments from the work directory (see: "The work directory"). The reason why this program exists is because all this work is unsafe to do inside the process that runs SpawningKit. Because after a `fork()`, one is only allowed to call async-signal-safe code. That means no memory allocations, or even calling `setenv()`. You can see in the diagrams that SpawnEnvSetupper is called twice, once before and once after loading the OS shell. The OS shell could arbitrarily change the environment (environment variables, ulimits, current working directory, etc.), sometimes without the user knowing about this. The main job that the SpawnEnvSetupper performs after the OS shell, is restoring some of the environment that the SpawningKit caller requested (e.g. specific environment variables, ulimits), as well as dumping the entire environment to the work directory so that the user can debug things when something is wrong. The SpawnEnvSetupper is implemented in SpawnEnvSetupperMain.cpp. ## The work directory The work directory is a temporary directory created at the very beginning of the spawning journey, during the SpawningKit preparation step. Note that this "work directory" is distinct from the Unix concept of current working directory (`getpwd()`). The work directory's purpose is to: 1. ...store information about the spawning procedure that the subprocess should know (the _spawn arguments_). 2. ...receive information from the subprocess about how spawning went (the _response_). For example the subprocess can use it to signal. 3. ...receive information about the subprocess's environment, so that this information can be displayed to the user for debugging purposes in case something goes wrong. Here, "subprocess" does not only refer to the worker process, but also to the SpawnEnvSetupper, the wrapper (if applicable) and even the shell. All of these can (not not necessarily *have to*) make use of the work directory. For example, the SpawnEnvSetupper dumps environment information into the work directory. Some wrappers may also dump environment information. The work directory's location is communicated to subprocesses through the `PASSENGER_SPAWN_WORK_DIR` environment variable. ### Structure The work directory has the following structure. Entries that are created during the SpawningKit preparation step are marked with "[P]". All other entries may be created by the subprocess. ~~~ Work directory | +-- args.json [P] | +-- args/ [P] | | | +-- app_root [P] | +-- log_level [P] | +-- ...etc... [P] | +-- stdin [P] (only when spawning through a preloader) +-- stdout_and_err [P] (only when spawning through a preloader) | +-- response/ [P] | | | +-- finish [P] | | | +-- properties.json | | | +-- error/ [P] | | | | | +-- category | | | | | +-- summary | | | | | +-- problem_description.txt | | +-- problem_description.html | | | | | +-- advanced_problem_details | | | | | +-- solution_description.txt | | +-- solution_description.html | | | +-- steps/ [P] | | | +-- subprocess_spawn_env_setupper_before_shell/ [P] | | | | | +-- state | | | | | +-- begin_time | | | -OR- | | | begin_time_monotonic | | | | | +-- end_time | | -OR- | | end_time_monotonic | | | +-- ... | | | +-- subprocess_listen/ [P] | | | +-- state | | | +-- begin_time | | -OR- | | begin_time_monotonic | | | +-- end_time | -OR- | end_time_monotonic | +-- envdump/ [P] | +-- envvars | +-- user_info | +-- ulimits | +-- annotations/ [P] | +-- some name | +-- some other name | +-- ... ~~~ There are two entries representing the spawn arguments: * `args.json` is a JSON file containing the arguments. ~~~ { "app_root": "/path-to-app", "log_level": 3, ... } ~~~ * `args/` is a directory containing the arguments. Inside this directory there are files, with each file representing a single argument. This directory provides an alternative way for subprocesses to read the arguments, which is convenient for subprocesses that don't have easy access to a JSON parser (e.g. Bash). The `response/` directory represents the response: * `finish` is a FIFO file. If a wrapper is used, or if the application has explicit support for SpawningKit, then either of them can write to this FIFO file to indicate that it has done spawning. See "Mechanism for waiting until the application is up" for more information. * If a wrapper is used, or if the application has explicit support for SpawningKit, then one of them may create a `properties.json` in order to communicate back to SpawningKit information about the spawned worker process. For example, if the application process started listening on a random port, then this file can be used to tell SpawningKit which port the process is listening on. See "Application response properties" for more information. * `stdin` and `stdout_and_err` are FIFO files. They are only created (by the preloader) when using a preloader to spawn a new worker process. These FIFOs refer to the spawned worker process's stdin, stdout and stderr. * If the subprocess fails, then it can communicate back specific error messages through the `error/` directory. See "Error reporting" (especially "Information sources") for more information. * The subprocess must regularly update the contents of the `steps/` directory to allow SpawningKit to know which step in the journey the subprocess is executing, and what the state and and begin time of each step is. See "Subprocess journey logging" for more information. The subprocess should dump information about its environment into the `envdump/` directory. Information includes environment variables (`envvars`), ulimits (`ulimits`), UID/GID (`user_info`), and anything else that the subprocess deems relevant (`annotations/`). If spawning fails, then the information reported in this directory will be included in the error report (see "Error reporting"). ## Application response properties If a wrapper is used or if the application has explicit support for SpawningKit, then either of them may create a `properties.json` inside the `response/` subdirectory of the work directory, in order to communicate back to SpawningKit information about the spawned application process. For example, if the application process started listening on a random port, then this file can be used to tell SpawningKit which port the process is listening on. `properties.json` may only contain the following keys: * `sockets` -- an array objects describing the sockets on which the application listens for requests. The format is as follows. All fields are required unless otherwise specified. [ { "address": "tcp://127.0.0.1:1234" | "unix:/path-to-unix-socket", "protocol": "http" | "session" | "preloader" | "arbitrary-other-value", "concurrency": <integer>, "accept_http_requests": true | false, // optional; default: false "description": "description of this socket" // optional }, ... ] The `address` field describes the address of the socket, which is either a TCP address or a Unix domain socket path. In case of the latter, the Unix domain socket **must** have the same file owner as the application process. The `protocol` field describes the protocol that this socket speaks. The value "http" is obvious; the value "session" refers to an internal SCGI-ish protocol that the Ruby and Python wrappers speak with Passenger. The value "preloader" means that this socket is used for receiving preloader commands (only preloaders are supposed to report such sockets; see "The preloader protocol"). Other arbitrary values are also allowed. The `concurrency` field describes how many concurrent requests this socket can handle. The special value 0 means unlimited. If the spawned process is a worker process (i.e. not a preloader process) then there must be at least one socket for which `accept_http_requests` is set to true. This field tells Passenger that HTTP traffic may be forwarded to this particular socket. You may wonder: why does this exist? Isn't it already enough if the application reports at least one socket that speaks the "http" protocol? The answer is no: whether Passenger should forward HTTP traffic to a specific socket has got nothing to do with whether that socket speaks HTTP. For example Passenger forwards HTTP traffic to the Ruby and Python wrappers using the "session" protocol. Furthermore, the Ruby wrapper spawns an HTTP socket, but it's for debugging purposes only and is slow, and so it should not be used for receiving live HTTP traffic. Note that a socket with `accept_http_requests` set to true **must** speak either the "http" or the "session" protocol. Other protocols are not allowed. The `description` field may be used in the future to display additional information about an application process, for example inside admin tools, but currently it is not used. ## The preloader protocol The "Tell preloader to spawn" and "Receive, process preloader response" steps in the spawn journey work as follows. Upon starting the preloader, the preloader listens for commands on a Unix domain socket. SpawningKit tells the preloader to spawn a worker process by sending a command over the socket. The command is a JSON document on a single line: ~~~json { "command": "spawn", "work_dir": "/path-to-work-dir" } ~~~ The preloader then forks a child process, and (before the next step in the journey is performed) immediately responds with either a success or an error response: ~~~json { "result": "ok", "pid": 1234 } { "result": "error", "message": "something went wrong" } ~~~ The worker process's stdin, stdout and stderr are stored in FIFO files inside the work directory. SpawningKit then opens these FIFOs and proceeds with handshaking with the worker process. ## Subprocess journey logging It is the Passenger Core (running SpawningKit) that initiates a spawning journey and that reports errors to users. Some steps in the journey are performed by actors that are not the Passenger Core (e.g. the preloader and the subprocess). How do these actors communicate to the SpawningKit code running inside the Passenger Core about the state of *their* part of the journey? This is done through the `steps/` subdirectory in the work directory. Subprocesses write to files in `steps/` regularly. Before SpawningKit's code returns, it loads information from `steps`/ and loads these into the journey object. Each subdirectory in `steps/` represents a step in the part of the journey that a subprocess is responsible for. The files inside such a subdirectory communicate the state of that step: * `state` must contain one of `STEP_NOT_STARTED`, `STEP_IN_PROGRESS`, `STEP_PERFORMED` or `STEP_ERRORED`. * Either `begin_time` or `begin_time_monotonic` (the latter is preferred) must exist. `begin_time` must contain a Unix timestamp representing the wall clock time at which this step began. The number may be a floating point number for sub-second precision. `begin_time_monotonic` is the same, but must contain a timestamp obtained from the monotonic clock instead of the wall clock. It is recommended that subprocesses create this file, not `begin_time`, because the monotonic clock is not influenced by clock skews (e.g. daylight savings, time zone changes or NTP updates). Because not all programming languages allow access to the monotonic clock, SpawningKit allows both mechanisms. * Either `end_time` or `end_time_monotonic` (the latter is preferred) must exist. The purpose of these files are anologous to `begin_time`/`begin_time_monotonic`, but instead record the time at which this step ended. ## Error reporting When something goes wrong during spawning, SpawningKit generates an error report. This report contains all the details you need to pinpoint the source of the problem, and is represented by the SpawnException class. A report contains the following information: * A broad **category** in which the problem belongs. Is it an internal program error? An operating system (system call) error? A filesystem error? An I/O error? * A **summary** of the problem. This typically consists of a single line line and is in plain text format. * A detailed **problem description**, in HTML format. This is a longer piece of narrative that is typically structured in two parts: a high-level description of the problem, meant for beginners; as well as a **advanced problem details** part that aids debugging. For example, if a file system permission error was encountered, the high-level description could explain what a file system permission is and that it's not Passenger's fault. The advanced information part could display the filename in question, as well as the OS error code and OS error message. * A detailed **solution description**, in HTML format. This is a longer piece of narrative that explains in a detailed manner how to solve the problem. * Various **auxiliary details** which are not directly related to the error, but may be useful or necessary for the purpose of debugging the problem: stdout and stderr output so far; ulimits; environment variables; UID, GID of the process in which the error occurred; system metrics such as CPU and RAM; etcetera. * A description of the **journey**: which steps inside the journey have been performed, are in progress, or have failed; and how long each step took. ### Information sources SpawningKit generates an error report (a SpawnException object) by gathering information from multiple sources. One source is SpawningKit itself: if something goes wrong within the preparation step for example, then SpawningKit knows that the error originated from there, and so it will only use internal information to generate an error report. The other source is the subprocess. If something goes wrong during the handshake step, then true source of the problem can either be in SpawningKit itself (e.g. it ran out of file descriptors), or in the subprocess (e.g. the subprocess encountered a filesystem permission error). Subprocesses can communicate to SpawningKit about errors that they have encountered by dumping information into the `response/error/` subdirectory of the work directory. SpawningKit will use this information in generating an error report. ### How an error report is presented The error report is presented in two ways: 1. In the terminal or in log files. 2. In an HTML page. The summary is only meant to be displayed in the terminal or in the log files as a one-liner. It can contain basic details (such as the OS error code) but is not meant to contain finer details such as the subprocess stdout/stderr output, the environment variable dump, etc. Everything else is meant to be displayed in an HTML page. The HTML page explicitly does not include the summary, so the summary must not contain any information that isn't available in all the other fields. The advanced problem details are only displayed in the HTML page if one does not explicitly supply a problem description. See "Generating an error report" for more information. ### How supplied information affects error report generation Only a _category_ and a _journey description_ are required for generating an error report. The SpawnException class is capable of automatically generating an appropriate (albeit generic) summary, problem description and solution description based on the category and which step in the journey failed. If one doesn't supply a problem description, but does supply advanced problem details, then the automatically-generated problem description will include the advanced problem details. The advanced problem details aren't used in any other way, so if one does supply a problem description then one must take care of including the advanced problem details. Inside the SpawningKit codebase, an error report is generated by creating a SpawnException object. Subprocesses such as the preloader, the SpawnEnvSetupper, the wrapper and the app, can aid in generating the error report by providing their own details through the `error/` and `envdump/` subdirectories inside the work directory. In particular: subprocesses can provide the problem description and the solution description in one of two formats: either plain-text or HTML. So e.g. only one of `problem_description.txt` or `problem_description.html` need to exist, not both. ## Mechanism for waiting until the application is up SpawningKit utilizes two mechanisms to wait until the application is up. It invokes both mechanisms at the same time and waits until at least one succeeds. The first mechanism the `response/finish` file in the work directory. A wrapper or a SpawningKit-enabled application can write to that file to tell SpawningKit that it is done, either successfully (by writing `1`) or with an error (by writing `0`). The second mechanism is only activated when the caller has told SpawningKit that the application is generic, or when the caller has told SpawningKit to find a free port for the application. In this case, SpawningKit will wait until the port that it has found can be connected to. SpawningKit/DummySpawner.h 0000644 00000010207 14756456557 0011636 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2011-2018 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef _PASSENGER_SPAWNING_KIT_DUMMY_SPAWNER_H_ #define _PASSENGER_SPAWNING_KIT_DUMMY_SPAWNER_H_ #include <boost/shared_ptr.hpp> #include <boost/atomic.hpp> #include <vector> #include <StaticString.h> #include <StrIntTools/StrIntUtils.h> #include <Core/SpawningKit/Spawner.h> #include <Core/SpawningKit/Exceptions.h> namespace Passenger { namespace SpawningKit { using namespace std; using namespace boost; using namespace oxt; class DummySpawner: public Spawner { private: boost::atomic<unsigned int> count; void setConfigFromAppPoolOptions(Config *config, Json::Value &extraArgs, const AppPoolOptions &options) { Spawner::setConfigFromAppPoolOptions(config, extraArgs, options); config->spawnMethod = P_STATIC_STRING("dummy"); } public: unsigned int cleanCount; DummySpawner(Context *context) : Spawner(context), count(1), cleanCount(0) { } virtual Result spawn(const AppPoolOptions &options) { TRACE_POINT(); possiblyRaiseInternalError(options); if (context->debugSupport != NULL) { syscalls::usleep(context->debugSupport->dummySpawnDelay); } Config config; Json::Value extraArgs; setConfigFromAppPoolOptions(&config, extraArgs, options); unsigned int number = count.fetch_add(1, boost::memory_order_relaxed); Result result; Result::Socket socket; socket.address = "tcp://127.0.0.1:1234"; socket.protocol = "session"; socket.concurrency = 1; socket.acceptHttpRequests = true; if (context->debugSupport != NULL) { socket.concurrency = context->debugSupport->dummyConcurrency; } result.initialize(*context, &config); result.pid = number; result.type = Result::DUMMY; result.gupid = "gupid-" + toString(number); result.spawnEndTime = result.spawnStartTime; result.spawnEndTimeMonotonic = result.spawnStartTimeMonotonic; result.sockets.push_back(socket); vector<StaticString> internalFieldErrors; vector<StaticString> appSuppliedFieldErrors; if (!result.validate(internalFieldErrors, appSuppliedFieldErrors)) { Journey journey(SPAWN_DIRECTLY, !config.genericApp && config.startsUsingWrapper); journey.setStepErrored(SPAWNING_KIT_HANDSHAKE_PERFORM, true); SpawnException e(INTERNAL_ERROR, journey, &config); e.setSummary("Error spawning the web application:" " a bug in " SHORT_PROGRAM_NAME " caused the" " spawn result to be invalid: " + toString(internalFieldErrors) + ", " + toString(appSuppliedFieldErrors)); e.setProblemDescriptionHTML( "Bug: the spawn result is invalid: " + toString(internalFieldErrors) + ", " + toString(appSuppliedFieldErrors)); throw e.finalize(); } return result; } virtual bool cleanable() const { return true; } virtual void cleanup() { cleanCount++; } }; typedef boost::shared_ptr<DummySpawner> DummySpawnerPtr; } // namespace SpawningKit } // namespace Passenger #endif /* _PASSENGER_SPAWNING_KIT_DUMMY_SPAWNER_H_ */ SpawningKit/PipeWatcher.h 0000644 00000012132 14756456557 0011415 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2012-2018 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef _PASSENGER_SPAwNING_KIT_PIPE_WATCHER_H_ #define _PASSENGER_SPAwNING_KIT_PIPE_WATCHER_H_ #include <boost/shared_ptr.hpp> #include <boost/enable_shared_from_this.hpp> #include <boost/thread.hpp> #include <boost/function.hpp> #include <boost/bind/bind.hpp> #include <boost/foreach.hpp> #include <oxt/thread.hpp> #include <oxt/backtrace.hpp> #include <string> #include <vector> #include <sys/types.h> #include <FileDescriptor.h> #include <Constants.h> #include <LoggingKit/LoggingKit.h> #include <Utils.h> #include <StrIntTools/StrIntUtils.h> namespace Passenger { namespace SpawningKit { using namespace boost; /** A PipeWatcher lives until the file descriptor is closed. */ class PipeWatcher: public boost::enable_shared_from_this<PipeWatcher> { private: FileDescriptor fd; StaticString name; string appGroupName; string appLogFile; pid_t pid; bool started; string logFile; boost::mutex startSyncher; boost::condition_variable startCond; size_t bufSize; char *buf; static void threadMain(boost::shared_ptr<PipeWatcher> self) { TRACE_POINT(); self->threadMain(); } void threadMain() { TRACE_POINT(); { boost::unique_lock<boost::mutex> lock(startSyncher); while (!started) { startCond.wait(lock); } } UPDATE_TRACE_POINT(); FILE *f = NULL; if (!logFile.empty()) { f = fopen(logFile.c_str(), "a"); if (f == NULL) { P_ERROR("Cannot open log file " << logFile); return; } } UPDATE_TRACE_POINT(); while (!boost::this_thread::interruption_requested()) { ssize_t ret; buf[0] = '\0'; UPDATE_TRACE_POINT(); ret = syscalls::read(fd, buf, bufSize); if (ret == 0) { break; } else if (ret == -1) { UPDATE_TRACE_POINT(); if (errno == ECONNRESET) { break; } else if (errno != EAGAIN) { int e = errno; P_WARN("Cannot read from process " << pid << " " << name << ": " << strerror(e) << " (errno=" << e << ")"); break; } } else if (ret == 1 && buf[0] == '\n') { UPDATE_TRACE_POINT(); printOrLogAppOutput(f, StaticString()); } else { UPDATE_TRACE_POINT(); vector<StaticString> lines; ssize_t ret2 = ret; if (ret2 > 0 && buf[ret2 - 1] == '\n') { ret2--; } split(StaticString(buf, ret2), '\n', lines); foreach (const StaticString line, lines) { printOrLogAppOutput(f, line); } } } if (f != NULL) { fclose(f); } } void printOrLogAppOutput(FILE *f, const StaticString &line) { if (f == NULL) { LoggingKit::logAppOutput(appGroupName, pid, name, line.data(), line.size(), appLogFile); } else { size_t ret = fwrite(line.data(), 1, line.size(), f); (void) ret; // Avoid compiler warning ret = fwrite("\n", 1, 2, f); (void) ret; // Avoid compiler warning fflush(f); } } public: PipeWatcher(const FileDescriptor &_fd, const StaticString &_name, const string &_appGroupName, const string &_appLogFile, pid_t _pid) : fd(_fd), name(_name), appGroupName(_appGroupName), appLogFile(_appLogFile), pid(_pid), started(false), bufSize(1024 * 8), buf(NULL) { } ~PipeWatcher() { delete[] buf; } void setLogFile(const string &path) { logFile = path; } void initialize() { const char *envMaxLogBytes = getenv("PASSENGER_MAX_LOG_LINE_LENGTH_BYTES"); if (envMaxLogBytes != NULL && *envMaxLogBytes != '\0') { bufSize = atoi(envMaxLogBytes); } buf = new char[bufSize]; oxt::thread(boost::bind(threadMain, shared_from_this()), "PipeWatcher: PID " + toString(pid) + " " + name + ", fd " + toString(fd), POOL_HELPER_THREAD_STACK_SIZE); } void start() { boost::lock_guard<boost::mutex> lock(startSyncher); started = true; startCond.notify_all(); } }; typedef boost::shared_ptr<PipeWatcher> PipeWatcherPtr; } // namespace SpawningKit } // namespace Passenger #endif /* _PASSENGER_SPAwNING_KIT_PIPE_WATCHER_H_ */ SpawningKit/Journey.h 0000644 00000041726 14756456557 0010650 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2017-2018 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef _PASSENGER_SPAWNING_KIT_HANDSHAKE_JOURNEY_H_ #define _PASSENGER_SPAWNING_KIT_HANDSHAKE_JOURNEY_H_ #include <map> #include <utility> #include <oxt/macros.hpp> #include <oxt/backtrace.hpp> #include <jsoncpp/json.h> #include <LoggingKit/LoggingKit.h> #include <StaticString.h> #include <SystemTools/SystemTime.h> #include <JsonTools/JsonUtils.h> #include <StrIntTools/StrIntUtils.h> namespace Passenger { namespace SpawningKit { using namespace std; /** * As explained in README.md, there are three possible journeys, * although each journey can have small variations (based on whether * a wrapper is used or not). */ enum JourneyType { SPAWN_DIRECTLY, START_PRELOADER, SPAWN_THROUGH_PRELOADER }; enum JourneyStep { // Steps in Passenger Core / SpawningKit SPAWNING_KIT_PREPARATION, SPAWNING_KIT_FORK_SUBPROCESS, SPAWNING_KIT_CONNECT_TO_PRELOADER, SPAWNING_KIT_SEND_COMMAND_TO_PRELOADER, SPAWNING_KIT_READ_RESPONSE_FROM_PRELOADER, SPAWNING_KIT_PARSE_RESPONSE_FROM_PRELOADER, SPAWNING_KIT_PROCESS_RESPONSE_FROM_PRELOADER, SPAWNING_KIT_HANDSHAKE_PERFORM, SPAWNING_KIT_FINISH, // Steps in preloader (when spawning a worker process) PRELOADER_PREPARATION, PRELOADER_FORK_SUBPROCESS, PRELOADER_SEND_RESPONSE, PRELOADER_FINISH, // Steps in subprocess SUBPROCESS_BEFORE_FIRST_EXEC, SUBPROCESS_SPAWN_ENV_SETUPPER_BEFORE_SHELL, SUBPROCESS_OS_SHELL, SUBPROCESS_SPAWN_ENV_SETUPPER_AFTER_SHELL, SUBPROCESS_EXEC_WRAPPER, SUBPROCESS_WRAPPER_PREPARATION, SUBPROCESS_APP_LOAD_OR_EXEC, SUBPROCESS_PREPARE_AFTER_FORKING_FROM_PRELOADER, SUBPROCESS_LISTEN, SUBPROCESS_FINISH, // Other UNKNOWN_JOURNEY_STEP }; enum JourneyStepState { /** * This step has not started yet. Will be visualized with an empty * placeholder. */ STEP_NOT_STARTED, /** * This step is currently in progress. Will be visualized with a spinner. */ STEP_IN_PROGRESS, /** * This step has already been performed successfully. Will be * visualized with a green tick. */ STEP_PERFORMED, /** * This step has failed. Will be visualized with a red mark. */ STEP_ERRORED, UNKNOWN_JOURNEY_STEP_STATE }; inline OXT_PURE StaticString journeyTypeToString(JourneyType type); inline OXT_PURE StaticString journeyStepToString(JourneyStep step); inline OXT_PURE string journeyStepToStringLowerCase(JourneyStep step); inline OXT_PURE StaticString journeyStepStateToString(JourneyStepState state); inline OXT_PURE JourneyStepState stringToJourneyStepState(const StaticString &value); inline OXT_PURE JourneyStep getFirstCoreJourneyStep() { return SPAWNING_KIT_PREPARATION; } inline OXT_PURE JourneyStep getLastCoreJourneyStep() { return SPAWNING_KIT_FINISH; } inline OXT_PURE JourneyStep getFirstPreloaderJourneyStep() { return PRELOADER_PREPARATION; } inline OXT_PURE JourneyStep getLastPreloaderJourneyStep() { return PRELOADER_FINISH; } inline OXT_PURE JourneyStep getFirstSubprocessJourneyStep() { return SUBPROCESS_BEFORE_FIRST_EXEC; } inline OXT_PURE JourneyStep getLastSubprocessJourneyStep() { return SUBPROCESS_FINISH; } class JourneyStepInfo { private: MonotonicTimeUsec getEndTime(const JourneyStepInfo *nextStepInfo) const { if (nextStepInfo != NULL && nextStepInfo->beginTime != 0) { return nextStepInfo->beginTime; } else { return endTime; } } public: JourneyStep step, nextStep; JourneyStepState state; MonotonicTimeUsec beginTime; MonotonicTimeUsec endTime; JourneyStepInfo(JourneyStep _step, JourneyStepState _state = STEP_NOT_STARTED) : step(_step), nextStep(UNKNOWN_JOURNEY_STEP), state(_state), beginTime(0), endTime(0) { } unsigned long long usecDuration(const JourneyStepInfo *nextStepInfo) const { if (getEndTime(nextStepInfo) >= beginTime) { return getEndTime(nextStepInfo) - beginTime; } else { return 0; } } Json::Value inspectAsJson(const JourneyStepInfo *nextStepInfo, MonotonicTimeUsec monoNow, unsigned long long now) const { Json::Value doc; doc["state"] = journeyStepStateToString(state).toString(); if (beginTime != 0) { doc["begin_time"] = monoTimeToJson(beginTime, monoNow, now); } if (endTime != 0) { doc["end_time"] = monoTimeToJson(endTime, monoNow, now); doc["duration"] = usecDuration(nextStepInfo) / 1000000.0; } return doc; } }; /** * For an introduction see README.md, sections: * * - "The Journey class" * - "Subprocess journey logging" */ class Journey { public: typedef map<JourneyStep, JourneyStepInfo> Map; private: JourneyType type; bool usingWrapper; Map steps; void insertStep(JourneyStep step, bool first = false) { steps.insert(make_pair(step, JourneyStepInfo(step))); if (!first) { Map::iterator prev = steps.end(); prev--; prev--; prev->second.nextStep = step; } } void fillInStepsForSpawnDirectlyJourney() { insertStep(SPAWNING_KIT_PREPARATION, true); insertStep(SPAWNING_KIT_FORK_SUBPROCESS); insertStep(SPAWNING_KIT_HANDSHAKE_PERFORM); insertStep(SPAWNING_KIT_FINISH); insertStep(SUBPROCESS_BEFORE_FIRST_EXEC, true); insertStep(SUBPROCESS_SPAWN_ENV_SETUPPER_BEFORE_SHELL); insertStep(SUBPROCESS_OS_SHELL); insertStep(SUBPROCESS_SPAWN_ENV_SETUPPER_AFTER_SHELL); if (usingWrapper) { insertStep(SUBPROCESS_EXEC_WRAPPER); insertStep(SUBPROCESS_WRAPPER_PREPARATION); } insertStep(SUBPROCESS_APP_LOAD_OR_EXEC); insertStep(SUBPROCESS_LISTEN); insertStep(SUBPROCESS_FINISH); } void fillInStepsForPreloaderStartJourney() { insertStep(SPAWNING_KIT_PREPARATION, true); insertStep(SPAWNING_KIT_FORK_SUBPROCESS); insertStep(SPAWNING_KIT_HANDSHAKE_PERFORM); insertStep(SPAWNING_KIT_FINISH); insertStep(SUBPROCESS_BEFORE_FIRST_EXEC, true); insertStep(SUBPROCESS_SPAWN_ENV_SETUPPER_BEFORE_SHELL); insertStep(SUBPROCESS_OS_SHELL); insertStep(SUBPROCESS_SPAWN_ENV_SETUPPER_AFTER_SHELL); if (usingWrapper) { insertStep(SUBPROCESS_EXEC_WRAPPER); insertStep(SUBPROCESS_WRAPPER_PREPARATION); } insertStep(SUBPROCESS_APP_LOAD_OR_EXEC); insertStep(SUBPROCESS_LISTEN); insertStep(SUBPROCESS_FINISH); } void fillInStepsForSpawnThroughPreloaderJourney() { insertStep(SPAWNING_KIT_PREPARATION, true); insertStep(SPAWNING_KIT_CONNECT_TO_PRELOADER); insertStep(SPAWNING_KIT_SEND_COMMAND_TO_PRELOADER); insertStep(SPAWNING_KIT_READ_RESPONSE_FROM_PRELOADER); insertStep(SPAWNING_KIT_PARSE_RESPONSE_FROM_PRELOADER); insertStep(SPAWNING_KIT_PROCESS_RESPONSE_FROM_PRELOADER); insertStep(SPAWNING_KIT_HANDSHAKE_PERFORM); insertStep(SPAWNING_KIT_FINISH); insertStep(PRELOADER_PREPARATION, true); insertStep(PRELOADER_FORK_SUBPROCESS); insertStep(PRELOADER_SEND_RESPONSE); insertStep(PRELOADER_FINISH); insertStep(SUBPROCESS_PREPARE_AFTER_FORKING_FROM_PRELOADER, true); insertStep(SUBPROCESS_LISTEN); insertStep(SUBPROCESS_FINISH); } JourneyStepInfo &getStepInfoMutable(JourneyStep step) { Map::iterator it = steps.find(step); if (it == steps.end()) { throw RuntimeException("Invalid step " + journeyStepToString(step)); } return it->second; } public: Journey(JourneyType _type, bool _usingWrapper) : type(_type), usingWrapper(_usingWrapper) { switch (_type) { case SPAWN_DIRECTLY: fillInStepsForSpawnDirectlyJourney(); break; case START_PRELOADER: fillInStepsForPreloaderStartJourney(); break; case SPAWN_THROUGH_PRELOADER: fillInStepsForSpawnThroughPreloaderJourney(); break; default: P_BUG("Unknown journey type " << toString((int) _type)); break; } } JourneyType getType() const { return type; } bool isUsingWrapper() const { return usingWrapper; } bool hasStep(JourneyStep step) const { Map::const_iterator it = steps.find(step); return it != steps.end(); } const JourneyStepInfo &getStepInfo(JourneyStep step) const { Map::const_iterator it = steps.find(step); if (it == steps.end()) { throw RuntimeException("Invalid step " + journeyStepToString(step)); } return it->second; } JourneyStep getFirstFailedStep() const { Map::const_iterator it, end = steps.end(); for (it = steps.begin(); it != end; it++) { if (it->second.state == STEP_ERRORED) { return it->first; } } return UNKNOWN_JOURNEY_STEP; } void setStepNotStarted(JourneyStep step, bool force = false) { JourneyStepInfo &info = getStepInfoMutable(step); if (info.state == STEP_NOT_STARTED || info.state == STEP_IN_PROGRESS || force) { info.state = STEP_NOT_STARTED; info.beginTime = 0; info.endTime = 0; } else { throw RuntimeException("Unable to change state for journey step " + journeyStepToString(step) + " because it wasn't already in progress"); } } void setStepInProgress(JourneyStep step, bool force = false) { JourneyStepInfo &info = getStepInfoMutable(step); if (info.state == STEP_IN_PROGRESS) { return; } else if (info.state == STEP_NOT_STARTED || force) { info.state = STEP_IN_PROGRESS; // When `force` is true, we don't want to overwrite the previous endTime. if (info.endTime == 0) { info.beginTime = SystemTime::getMonotonicUsecWithGranularity<SystemTime::GRAN_10MSEC>(); } } else { throw RuntimeException("Unable to change state for journey step " + journeyStepToString(step) + " because it was already in progress or completed"); } } void setStepPerformed(JourneyStep step, bool force = false) { JourneyStepInfo &info = getStepInfoMutable(step); if (info.state == STEP_PERFORMED) { return; } else if (info.state == STEP_IN_PROGRESS || true) { info.state = STEP_PERFORMED; // When `force` is true, we don't want to overwrite the previous endTime. if (info.endTime == 0) { info.endTime = SystemTime::getMonotonicUsecWithGranularity<SystemTime::GRAN_10MSEC>(); if (info.beginTime == 0) { info.beginTime = info.endTime; } } } else { throw RuntimeException("Unable to change state for journey step " + journeyStepToString(step) + " because it wasn't already in progress"); } } void setStepErrored(JourneyStep step, bool force = false) { JourneyStepInfo &info = getStepInfoMutable(step); if (info.state == STEP_ERRORED) { return; } else if (info.state == STEP_IN_PROGRESS || force) { info.state = STEP_ERRORED; // When `force` is true, we don't want to overwrite the previous endTime. if (info.endTime == 0) { info.endTime = SystemTime::getMonotonicUsecWithGranularity<SystemTime::GRAN_10MSEC>(); if (info.beginTime == 0) { info.beginTime = info.endTime; } } } else { throw RuntimeException("Unable to change state for journey step " + journeyStepToString(step) + " because it wasn't already in progress"); } } void setStepBeginTime(JourneyStep step, MonotonicTimeUsec timestamp) { JourneyStepInfo &info = getStepInfoMutable(step); info.beginTime = timestamp; } void setStepEndTime(JourneyStep step, MonotonicTimeUsec timestamp) { JourneyStepInfo &info = getStepInfoMutable(step); info.endTime = timestamp; } void reset() { Map::iterator it, end = steps.end(); for (it = steps.begin(); it != end; it++) { it->second.state = STEP_NOT_STARTED; it->second.beginTime = 0; it->second.endTime = 0; } } Json::Value inspectAsJson() const { Json::Value doc, steps; MonotonicTimeUsec monoNow = SystemTime::getMonotonicUsec(); unsigned long long now = SystemTime::getUsec(); doc["type"] = journeyTypeToString(type).toString(); Map::const_iterator it, end = this->steps.end(); for (it = this->steps.begin(); it != end; it++) { const JourneyStep step = it->first; const JourneyStepInfo &info = it->second; const JourneyStepInfo *nextStepInfo = NULL; if (info.nextStep != UNKNOWN_JOURNEY_STEP) { nextStepInfo = &this->steps.find(info.nextStep)->second; } steps[journeyStepToString(step).toString()] = info.inspectAsJson(nextStepInfo, monoNow, now); } doc["steps"] = steps; return doc; } }; inline OXT_PURE StaticString journeyTypeToString(JourneyType type) { switch (type) { case SPAWN_DIRECTLY: return P_STATIC_STRING("SPAWN_DIRECTLY"); case START_PRELOADER: return P_STATIC_STRING("START_PRELOADER"); case SPAWN_THROUGH_PRELOADER: return P_STATIC_STRING("SPAWN_THROUGH_PRELOADER"); default: return P_STATIC_STRING("UNKNOWN_JOURNEY_TYPE"); } } inline OXT_PURE StaticString journeyStepToString(JourneyStep step) { switch (step) { case SPAWNING_KIT_PREPARATION: return P_STATIC_STRING("SPAWNING_KIT_PREPARATION"); case SPAWNING_KIT_FORK_SUBPROCESS: return P_STATIC_STRING("SPAWNING_KIT_FORK_SUBPROCESS"); case SPAWNING_KIT_CONNECT_TO_PRELOADER: return P_STATIC_STRING("SPAWNING_KIT_CONNECT_TO_PRELOADER"); case SPAWNING_KIT_SEND_COMMAND_TO_PRELOADER: return P_STATIC_STRING("SPAWNING_KIT_SEND_COMMAND_TO_PRELOADER"); case SPAWNING_KIT_READ_RESPONSE_FROM_PRELOADER: return P_STATIC_STRING("SPAWNING_KIT_READ_RESPONSE_FROM_PRELOADER"); case SPAWNING_KIT_PARSE_RESPONSE_FROM_PRELOADER: return P_STATIC_STRING("SPAWNING_KIT_PARSE_RESPONSE_FROM_PRELOADER"); case SPAWNING_KIT_PROCESS_RESPONSE_FROM_PRELOADER: return P_STATIC_STRING("SPAWNING_KIT_PROCESS_RESPONSE_FROM_PRELOADER"); case SPAWNING_KIT_HANDSHAKE_PERFORM: return P_STATIC_STRING("SPAWNING_KIT_HANDSHAKE_PERFORM"); case SPAWNING_KIT_FINISH: return P_STATIC_STRING("SPAWNING_KIT_FINISH"); case PRELOADER_PREPARATION: return P_STATIC_STRING("PRELOADER_PREPARATION"); case PRELOADER_FORK_SUBPROCESS: return P_STATIC_STRING("PRELOADER_FORK_SUBPROCESS"); case PRELOADER_SEND_RESPONSE: return P_STATIC_STRING("PRELOADER_SEND_RESPONSE"); case PRELOADER_FINISH: return P_STATIC_STRING("PRELOADER_FINISH"); case SUBPROCESS_BEFORE_FIRST_EXEC: return P_STATIC_STRING("SUBPROCESS_BEFORE_FIRST_EXEC"); case SUBPROCESS_SPAWN_ENV_SETUPPER_BEFORE_SHELL: return P_STATIC_STRING("SUBPROCESS_SPAWN_ENV_SETUPPER_BEFORE_SHELL"); case SUBPROCESS_OS_SHELL: return P_STATIC_STRING("SUBPROCESS_OS_SHELL"); case SUBPROCESS_SPAWN_ENV_SETUPPER_AFTER_SHELL: return P_STATIC_STRING("SUBPROCESS_SPAWN_ENV_SETUPPER_AFTER_SHELL"); case SUBPROCESS_EXEC_WRAPPER: return P_STATIC_STRING("SUBPROCESS_EXEC_WRAPPER"); case SUBPROCESS_WRAPPER_PREPARATION: return P_STATIC_STRING("SUBPROCESS_WRAPPER_PREPARATION"); case SUBPROCESS_APP_LOAD_OR_EXEC: return P_STATIC_STRING("SUBPROCESS_APP_LOAD_OR_EXEC"); case SUBPROCESS_PREPARE_AFTER_FORKING_FROM_PRELOADER: return P_STATIC_STRING("SUBPROCESS_PREPARE_AFTER_FORKING_FROM_PRELOADER"); case SUBPROCESS_LISTEN: return P_STATIC_STRING("SUBPROCESS_LISTEN"); case SUBPROCESS_FINISH: return P_STATIC_STRING("SUBPROCESS_FINISH"); default: return P_STATIC_STRING("UNKNOWN_JOURNEY_STEP"); } } inline OXT_PURE string journeyStepToStringLowerCase(JourneyStep step) { StaticString stepString = journeyStepToString(step); DynamicBuffer stepStringLcBuffer(stepString.size()); convertLowerCase((const unsigned char *) stepString.data(), (unsigned char *) stepStringLcBuffer.data, stepString.size()); return string(stepStringLcBuffer.data, stepString.size()); } inline OXT_PURE StaticString journeyStepStateToString(JourneyStepState state) { switch (state) { case STEP_NOT_STARTED: return P_STATIC_STRING("STEP_NOT_STARTED"); case STEP_IN_PROGRESS: return P_STATIC_STRING("STEP_IN_PROGRESS"); case STEP_PERFORMED: return P_STATIC_STRING("STEP_PERFORMED"); case STEP_ERRORED: return P_STATIC_STRING("STEP_ERRORED"); default: return P_STATIC_STRING("UNKNOWN_JOURNEY_STEP_STATE"); } } inline OXT_PURE JourneyStepState stringToJourneyStepState(const StaticString &value) { if (value == P_STATIC_STRING("STEP_NOT_STARTED")) { return STEP_NOT_STARTED; } else if (value == P_STATIC_STRING("STEP_IN_PROGRESS")) { return STEP_IN_PROGRESS; } else if (value == P_STATIC_STRING("STEP_PERFORMED")) { return STEP_PERFORMED; } else if (value == P_STATIC_STRING("STEP_ERRORED")) { return STEP_ERRORED; } else { return UNKNOWN_JOURNEY_STEP_STATE; } } } // namespace SpawningKit } // namespace Passenger #endif /* _PASSENGER_SPAWNING_KIT_HANDSHAKE_JOURNEY_H_ */ SpawningKit/DirectSpawner.h 0000644 00000022132 14756456557 0011755 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2011-2018 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef _PASSENGER_SPAWNING_KIT_DIRECT_SPAWNER_H_ #define _PASSENGER_SPAWNING_KIT_DIRECT_SPAWNER_H_ #include <stdexcept> #include <Core/SpawningKit/Spawner.h> #include <Core/SpawningKit/Handshake/Session.h> #include <Core/SpawningKit/Handshake/Prepare.h> #include <Core/SpawningKit/Handshake/Perform.h> #include <ProcessManagement/Utils.h> #include <Constants.h> #include <LoggingKit/LoggingKit.h> #include <LveLoggingDecorator.h> #include <IOTools/IOUtils.h> #include <Utils/AsyncSignalSafeUtils.h> #include <limits.h> // for PTHREAD_STACK_MIN #include <pthread.h> #include <unistd.h> #include <adhoc_lve.h> namespace Passenger { namespace SpawningKit { using namespace std; using namespace boost; using namespace oxt; class DirectSpawner: public Spawner { private: static int startBackgroundThread(void *(*mainFunction)(void *), void *arg) { // Using raw pthread API because we don't want to register such // trivial threads on the oxt::thread list. pthread_t thr; pthread_attr_t attr; size_t stack_size = 96 * 1024; unsigned long min_stack_size; bool stack_min_size_defined; bool round_stack_size; int ret; #ifdef PTHREAD_STACK_MIN // PTHREAD_STACK_MIN may not be a constant macro so we need // to evaluate it dynamically. min_stack_size = PTHREAD_STACK_MIN; stack_min_size_defined = true; #else // Assume minimum stack size is 128 KB. min_stack_size = 128 * 1024; stack_min_size_defined = false; #endif if (stack_size != 0 && stack_size < min_stack_size) { stack_size = min_stack_size; round_stack_size = !stack_min_size_defined; } else { round_stack_size = true; } if (round_stack_size) { // Round stack size up to page boundary. long page_size; #if defined(_SC_PAGESIZE) page_size = sysconf(_SC_PAGESIZE); #elif defined(_SC_PAGE_SIZE) page_size = sysconf(_SC_PAGE_SIZE); #elif defined(PAGESIZE) page_size = sysconf(PAGESIZE); #elif defined(PAGE_SIZE) page_size = sysconf(PAGE_SIZE); #else page_size = getpagesize(); #endif if (stack_size % page_size != 0) { stack_size = stack_size - (stack_size % page_size) + page_size; } } pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr, 1); pthread_attr_setstacksize(&attr, stack_size); ret = pthread_create(&thr, &attr, mainFunction, arg); pthread_attr_destroy(&attr); return ret; } static void *detachProcessMain(void *arg) { boost::this_thread::disable_syscall_interruption dsi; pid_t pid = (pid_t) (long) arg; syscalls::waitpid(pid, NULL, 0); return NULL; } void detachProcess(pid_t pid) { startBackgroundThread(detachProcessMain, (void *) (long) pid); } void setConfigFromAppPoolOptions(Config *config, Json::Value &extraArgs, const AppPoolOptions &options) { Spawner::setConfigFromAppPoolOptions(config, extraArgs, options); config->spawnMethod = P_STATIC_STRING("direct"); } Result internalSpawn(const AppPoolOptions &options, Config &config, HandshakeSession &session, const Json::Value &extraArgs, JourneyStep &stepToMarkAsErrored) { TRACE_POINT(); Pipe stdinChannel = createPipe(__FILE__, __LINE__); Pipe stdoutAndErrChannel = createPipe(__FILE__, __LINE__); adhoc_lve::LveEnter scopedLveEnter(LveLoggingDecorator::lveInitOnce(), session.uid, config.lveMinUid, LveLoggingDecorator::lveExitCallback); LveLoggingDecorator::logLveEnter(scopedLveEnter, session.uid, config.lveMinUid); string agentFilename = context->resourceLocator ->findSupportBinary(AGENT_EXE); session.journey.setStepPerformed(SPAWNING_KIT_PREPARATION); session.journey.setStepInProgress(SPAWNING_KIT_FORK_SUBPROCESS); session.journey.setStepInProgress(SUBPROCESS_BEFORE_FIRST_EXEC); stepToMarkAsErrored = SPAWNING_KIT_FORK_SUBPROCESS; pid_t pid = syscalls::fork(); if (pid == 0) { int e; char buf[1024]; const char *end = buf + sizeof(buf); namespace ASSU = AsyncSignalSafeUtils; resetSignalHandlersAndMask(); disableMallocDebugging(); int stdinCopy = dup2(stdinChannel.first, 3); int stdoutAndErrCopy = dup2(stdoutAndErrChannel.second, 4); dup2(stdinCopy, 0); dup2(stdoutAndErrCopy, 1); dup2(stdoutAndErrCopy, 2); closeAllFileDescriptors(2); execlp(agentFilename.c_str(), agentFilename.c_str(), "spawn-env-setupper", session.workDir->getPath().c_str(), "--before", (char *) 0); char *pos = buf; e = errno; pos = ASSU::appendData(pos, end, "Cannot execute \""); pos = ASSU::appendData(pos, end, agentFilename.data(), agentFilename.size()); pos = ASSU::appendData(pos, end, "\": "); pos = ASSU::appendData(pos, end, ASSU::limitedStrerror(e)); pos = ASSU::appendData(pos, end, " (errno="); pos = ASSU::appendInteger<int, 10>(pos, end, e); pos = ASSU::appendData(pos, end, ")\n"); ASSU::printError(buf, pos - buf); _exit(1); } else if (pid == -1) { int e = errno; session.journey.setStepErrored(SPAWNING_KIT_FORK_SUBPROCESS); SpawnException ex(OPERATING_SYSTEM_ERROR, session.journey, &config); ex.setSummary(StaticString("Cannot fork a new process: ") + strerror(e) + " (errno=" + toString(e) + ")"); ex.setAdvancedProblemDetails(StaticString("Cannot fork a new process: ") + strerror(e) + " (errno=" + toString(e) + ")"); throw ex.finalize(); } else { UPDATE_TRACE_POINT(); session.journey.setStepPerformed(SPAWNING_KIT_FORK_SUBPROCESS); session.journey.setStepInProgress(SPAWNING_KIT_HANDSHAKE_PERFORM); stepToMarkAsErrored = SPAWNING_KIT_HANDSHAKE_PERFORM; scopedLveEnter.exit(); P_LOG_FILE_DESCRIPTOR_PURPOSE(stdinChannel.second, "App " << pid << " (" << options.appRoot << ") stdin"); P_LOG_FILE_DESCRIPTOR_PURPOSE(stdoutAndErrChannel.first, "App " << pid << " (" << options.appRoot << ") stdoutAndErr"); UPDATE_TRACE_POINT(); ScopeGuard guard(boost::bind(nonInterruptableKillAndWaitpid, pid)); P_DEBUG("Process forked for appRoot=" << options.appRoot << ": PID " << pid); stdinChannel.first.close(); stdoutAndErrChannel.second.close(); HandshakePerform(session, pid, stdinChannel.second, stdoutAndErrChannel.first).execute(); UPDATE_TRACE_POINT(); detachProcess(session.result.pid); guard.clear(); session.journey.setStepPerformed(SPAWNING_KIT_HANDSHAKE_PERFORM); P_DEBUG("Process spawning done: appRoot=" << options.appRoot << ", pid=" << session.result.pid); return session.result; } } public: DirectSpawner(Context *context) : Spawner(context) { } virtual Result spawn(const AppPoolOptions &options) { TRACE_POINT(); boost::this_thread::disable_interruption di; boost::this_thread::disable_syscall_interruption dsi; P_DEBUG("Spawning new process: appRoot=" << options.appRoot); possiblyRaiseInternalError(options); UPDATE_TRACE_POINT(); Config config; Json::Value extraArgs; try { setConfigFromAppPoolOptions(&config, extraArgs, options); } catch (const std::exception &originalException) { UPDATE_TRACE_POINT(); Journey journey(SPAWN_THROUGH_PRELOADER, true); journey.setStepErrored(SPAWNING_KIT_PREPARATION, true); SpawnException e(originalException, journey, &config); throw e.finalize(); } UPDATE_TRACE_POINT(); HandshakeSession session(*context, config, SPAWN_DIRECTLY); session.journey.setStepInProgress(SPAWNING_KIT_PREPARATION); HandshakePrepare(session, extraArgs).execute(); JourneyStep stepToMarkAsErrored = SPAWNING_KIT_PREPARATION; UPDATE_TRACE_POINT(); try { return internalSpawn(options, config, session, extraArgs, stepToMarkAsErrored); } catch (const SpawnException &) { throw; } catch (const std::exception &originalException) { UPDATE_TRACE_POINT(); session.journey.setStepErrored(stepToMarkAsErrored, true); throw SpawnException(originalException, session.journey, &config).finalize(); } } }; } // namespace SpawningKit } // namespace Passenger #endif /* _PASSENGER_SPAWNING_KIT_DIRECT_SPAWNER_H_ */ SpawningKit/SmartSpawner.h 0000644 00000133445 14756456557 0011643 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2011-2018 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef _PASSENGER_SPAWNING_KIT_SMART_SPAWNER_H_ #define _PASSENGER_SPAWNING_KIT_SMART_SPAWNER_H_ #include <oxt/thread.hpp> #include <oxt/system_calls.hpp> #include <boost/bind/bind.hpp> #include <boost/make_shared.hpp> #include <string> #include <vector> #include <map> #include <stdexcept> #include <dirent.h> #include <sys/types.h> #include <sys/wait.h> #include <sys/stat.h> #include <signal.h> #include <unistd.h> #include <cstring> #include <cassert> #include <adhoc_lve.h> #include <LoggingKit/Logging.h> #include <Constants.h> #include <Exceptions.h> #include <DataStructures/StringKeyTable.h> #include <ProcessManagement/Utils.h> #include <SystemTools/ProcessMetricsCollector.h> #include <SystemTools/SystemTime.h> #include <FileTools/FileManip.h> #include <IOTools/BufferedIO.h> #include <JsonTools/JsonUtils.h> #include <Utils/ScopeGuard.h> #include <Utils/AsyncSignalSafeUtils.h> #include <LveLoggingDecorator.h> #include <Core/SpawningKit/Spawner.h> #include <Core/SpawningKit/Exceptions.h> #include <Core/SpawningKit/PipeWatcher.h> #include <Core/SpawningKit/Handshake/Session.h> #include <Core/SpawningKit/Handshake/Prepare.h> #include <Core/SpawningKit/Handshake/Perform.h> #include <Core/SpawningKit/Handshake/BackgroundIOCapturer.h> namespace Passenger { namespace SpawningKit { using namespace std; using namespace boost; using namespace oxt; class SmartSpawner: public Spawner { private: const string preloaderCommandString; string preloaderEnvvars; string preloaderUserInfo; string preloaderUlimits; StringKeyTable<string> preloaderAnnotations; AppPoolOptions options; // Protects m_lastUsed and pid. mutable boost::mutex simpleFieldSyncher; // Protects everything else. mutable boost::mutex syncher; // Preloader information. pid_t pid; FileDescriptor preloaderStdin; string socketAddress; unsigned long long m_lastUsed; /** * Behaves like <tt>waitpid(pid, status, WNOHANG)</tt>, but waits at most * <em>timeout</em> miliseconds for the process to exit. */ static int timedWaitpid(pid_t pid, int *status, unsigned long long timeout) { Timer<SystemTime::GRAN_10MSEC> timer; int ret; do { ret = syscalls::waitpid(pid, status, WNOHANG); if (ret > 0 || ret == -1) { return ret; } else { syscalls::usleep(10000); } } while (timer.elapsed() < timeout); return 0; // timed out } static bool osProcessExists(pid_t pid) { if (syscalls::kill(pid, 0) == 0) { /* On some environments, e.g. Heroku, the init process does * not properly reap adopted zombie processes, which can interfere * with our process existance check. To work around this, we * explicitly check whether or not the process has become a zombie. */ return !isZombie(pid); } else { return errno != ESRCH; } } static bool isZombie(pid_t pid) { string filename = "/proc/" + toString(pid) + "/status"; FILE *f = fopen(filename.c_str(), "r"); if (f == NULL) { // Don't know. return false; } bool result = false; while (!feof(f)) { char buf[512]; const char *line; line = fgets(buf, sizeof(buf), f); if (line == NULL) { break; } if (strcmp(line, "State: Z (zombie)\n") == 0) { // Is a zombie. result = true; break; } } fclose(f); return result; } static string createCommandString(const vector<string> &command) { string result; vector<string>::const_iterator it; vector<string>::const_iterator begin = command.begin(); vector<string>::const_iterator end = command.end(); for (it = begin; it != end; it++) { if (it != begin) { result.append(1, ' '); } result.append(escapeShell(*it)); } return result; } void setConfigFromAppPoolOptions(Config *config, Json::Value &extraArgs, const AppPoolOptions &options) { Spawner::setConfigFromAppPoolOptions(config, extraArgs, options); config->spawnMethod = P_STATIC_STRING("smart"); } struct StdChannelsAsyncOpenState { const int workDirFd; oxt::thread *stdinOpenThread; FileDescriptor stdinFd; int stdinOpenErrno; oxt::thread *stdoutAndErrOpenThread; FileDescriptor stdoutAndErrFd; int stdoutAndErrOpenErrno; BackgroundIOCapturerPtr stdoutAndErrCapturer; StdChannelsAsyncOpenState(int _workDirFd) : workDirFd(_workDirFd), stdinOpenThread(NULL), stdoutAndErrOpenThread(NULL) { } ~StdChannelsAsyncOpenState() { boost::this_thread::disable_interruption di; boost::this_thread::disable_syscall_interruption dsi; if (stdinOpenThread != NULL) { stdinOpenThread->interrupt_and_join(); delete stdinOpenThread; } if (stdoutAndErrOpenThread != NULL) { stdoutAndErrOpenThread->interrupt_and_join(); delete stdoutAndErrOpenThread; } } }; typedef boost::shared_ptr<StdChannelsAsyncOpenState> StdChannelsAsyncOpenStatePtr; StdChannelsAsyncOpenStatePtr openStdChannelsFifosAsynchronously( HandshakeSession &session) { StdChannelsAsyncOpenStatePtr state = boost::make_shared<StdChannelsAsyncOpenState>( session.workDirFd); state->stdinOpenThread = new oxt::thread(boost::bind( openStdinChannel, state, session.workDir->getPath()), "FIFO opener: " + session.workDir->getPath() + "/stdin", 1024 * 128); state->stdoutAndErrOpenThread = new oxt::thread(boost::bind( openStdoutAndErrChannel, state, session.workDir->getPath()), "FIFO opener: " + session.workDir->getPath() + "/stdout_and_err", 1024 * 128); return state; } void waitForStdChannelFifosToBeOpenedByPeer(const StdChannelsAsyncOpenStatePtr &state, HandshakeSession &session, pid_t pid) { TRACE_POINT(); MonotonicTimeUsec startTime = SystemTime::getMonotonicUsec(); ScopeGuard guard(boost::bind(adjustTimeout, startTime, &session.timeoutUsec)); try { if (state->stdinOpenThread->try_join_for( boost::chrono::microseconds(session.timeoutUsec))) { delete state->stdinOpenThread; state->stdinOpenThread = NULL; if (state->stdinFd == -1) { throw SystemException("Error opening FIFO " + session.workDir->getPath() + "/stdin", state->stdinOpenErrno); } else { P_LOG_FILE_DESCRIPTOR_PURPOSE(state->stdinFd, "App " << pid << " (" << options.appRoot << ") stdin"); adjustTimeout(startTime, &session.timeoutUsec); startTime = SystemTime::getMonotonicUsec(); } } else { throw TimeoutException("Timeout opening FIFO " + session.workDir->getPath() + "/stdin"); } UPDATE_TRACE_POINT(); if (state->stdoutAndErrOpenThread->try_join_for( boost::chrono::microseconds(session.timeoutUsec))) { delete state->stdoutAndErrOpenThread; state->stdoutAndErrOpenThread = NULL; if (state->stdoutAndErrFd == -1) { throw SystemException("Error opening FIFO " + session.workDir->getPath() + "/stdout_and_err", state->stdoutAndErrOpenErrno); } else { P_LOG_FILE_DESCRIPTOR_PURPOSE(state->stdoutAndErrFd, "App " << pid << " (" << options.appRoot << ") stdoutAndErr"); } } else { throw TimeoutException("Timeout opening FIFO " + session.workDir->getPath() + "/stdout_and_err"); } state->stdoutAndErrCapturer = boost::make_shared<BackgroundIOCapturer>( state->stdoutAndErrFd, pid, session.config->appGroupName, session.config->logFile); state->stdoutAndErrCapturer->start(); } catch (const boost::system::system_error &e) { throw SystemException(e.what(), e.code().value()); } } static void openStdinChannel(StdChannelsAsyncOpenStatePtr state, const string &workDir) { int fd = syscalls::openat(state->workDirFd, "stdin", O_WRONLY | O_APPEND | O_NOFOLLOW); int e = errno; state->stdinFd.assign(fd, __FILE__, __LINE__); state->stdinOpenErrno = e; } static void openStdoutAndErrChannel(StdChannelsAsyncOpenStatePtr state, const string &workDir) { int fd = syscalls::openat(state->workDirFd, "stdout_and_err", O_RDONLY | O_NOFOLLOW); int e = errno; state->stdoutAndErrFd.assign(fd, __FILE__, __LINE__); state->stdoutAndErrOpenErrno = e; } bool preloaderStarted() const { return pid != -1; } void startPreloader() { TRACE_POINT(); boost::this_thread::disable_interruption di; boost::this_thread::disable_syscall_interruption dsi; assert(!preloaderStarted()); P_DEBUG("Spawning new preloader: appRoot=" << options.appRoot); Config config; Json::Value extraArgs; try { setConfigFromAppPoolOptions(&config, extraArgs, options); config.startCommand = preloaderCommandString; } catch (const std::exception &originalException) { Journey journey(SPAWN_THROUGH_PRELOADER, true); journey.setStepErrored(SPAWNING_KIT_PREPARATION, true); throw SpawnException(originalException, journey, &config).finalize(); } HandshakeSession session(*context, config, START_PRELOADER); session.journey.setStepInProgress(SPAWNING_KIT_PREPARATION); try { internalStartPreloader(config, session, extraArgs); } catch (const SpawnException &) { throw; } catch (const std::exception &originalException) { session.journey.setStepErrored(SPAWNING_KIT_PREPARATION); throw SpawnException(originalException, session.journey, &config).finalize(); } } void internalStartPreloader(Config &config, HandshakeSession &session, const Json::Value &extraArgs) { TRACE_POINT(); HandshakePrepare(session, extraArgs).execute(); Pipe stdinChannel = createPipe(__FILE__, __LINE__); Pipe stdoutAndErrChannel = createPipe(__FILE__, __LINE__); adhoc_lve::LveEnter scopedLveEnter(LveLoggingDecorator::lveInitOnce(), session.uid, config.lveMinUid, LveLoggingDecorator::lveExitCallback); LveLoggingDecorator::logLveEnter(scopedLveEnter, session.uid, config.lveMinUid); string agentFilename = context->resourceLocator ->findSupportBinary(AGENT_EXE); session.journey.setStepPerformed(SPAWNING_KIT_PREPARATION); session.journey.setStepInProgress(SPAWNING_KIT_FORK_SUBPROCESS); session.journey.setStepInProgress(SUBPROCESS_BEFORE_FIRST_EXEC); pid_t pid = syscalls::fork(); if (pid == 0) { int e; char buf[1024]; const char *end = buf + sizeof(buf); namespace ASSU = AsyncSignalSafeUtils; resetSignalHandlersAndMask(); disableMallocDebugging(); int stdinCopy = dup2(stdinChannel.first, 3); int stdoutAndErrCopy = dup2(stdoutAndErrChannel.second, 4); dup2(stdinCopy, 0); dup2(stdoutAndErrCopy, 1); dup2(stdoutAndErrCopy, 2); closeAllFileDescriptors(2); execlp(agentFilename.c_str(), agentFilename.c_str(), "spawn-env-setupper", session.workDir->getPath().c_str(), "--before", (char *) 0); char *pos = buf; e = errno; pos = ASSU::appendData(pos, end, "Cannot execute \""); pos = ASSU::appendData(pos, end, agentFilename.data(), agentFilename.size()); pos = ASSU::appendData(pos, end, "\": "); pos = ASSU::appendData(pos, end, ASSU::limitedStrerror(e)); pos = ASSU::appendData(pos, end, " (errno="); pos = ASSU::appendInteger<int, 10>(pos, end, e); pos = ASSU::appendData(pos, end, ")\n"); ASSU::printError(buf, pos - buf); _exit(1); } else if (pid == -1) { int e = errno; UPDATE_TRACE_POINT(); session.journey.setStepErrored(SPAWNING_KIT_FORK_SUBPROCESS); SpawnException ex(OPERATING_SYSTEM_ERROR, session.journey, &config); ex.setSummary(StaticString("Cannot fork a new process: ") + strerror(e) + " (errno=" + toString(e) + ")"); ex.setAdvancedProblemDetails(StaticString("Cannot fork a new process: ") + strerror(e) + " (errno=" + toString(e) + ")"); throw ex.finalize(); } else { UPDATE_TRACE_POINT(); session.journey.setStepPerformed(SPAWNING_KIT_FORK_SUBPROCESS); session.journey.setStepInProgress(SPAWNING_KIT_HANDSHAKE_PERFORM); scopedLveEnter.exit(); P_LOG_FILE_DESCRIPTOR_PURPOSE(stdinChannel.second, "Preloader " << pid << " (" << options.appRoot << ") stdin"); P_LOG_FILE_DESCRIPTOR_PURPOSE(stdoutAndErrChannel.first, "Preloader " << pid << " (" << options.appRoot << ") stdoutAndErr"); UPDATE_TRACE_POINT(); ScopeGuard guard(boost::bind(nonInterruptableKillAndWaitpid, pid)); P_DEBUG("Preloader process forked for appRoot=" << options.appRoot << ": PID " << pid); stdinChannel.first.close(); stdoutAndErrChannel.second.close(); HandshakePerform(session, pid, stdinChannel.second, stdoutAndErrChannel.first).execute(); string envvars, userInfo, ulimits; // If a new output variable was added to this function, // then don't forget to also update these locations: // - the critical section below // - bottom of stopPreloader() // - addPreloaderEnvDumps() HandshakePerform::loadBasicInfoFromEnvDumpDir(session.envDumpDir, session.envDumpDirFd, envvars, userInfo, ulimits); string socketAddress = findPreloaderCommandSocketAddress(session); { boost::lock_guard<boost::mutex> l(simpleFieldSyncher); this->pid = pid; this->socketAddress = socketAddress; this->preloaderStdin = stdinChannel.second; this->preloaderEnvvars = envvars; this->preloaderUserInfo = userInfo; this->preloaderUlimits = ulimits; this->preloaderAnnotations = loadAnnotationsFromEnvDumpDir( session.envDumpDir, session.envDumpAnnotationsDirFd); } PipeWatcherPtr watcher = boost::make_shared<PipeWatcher>( stdoutAndErrChannel.first, "output", config.appGroupName, config.logFile, pid); watcher->initialize(); watcher->start(); UPDATE_TRACE_POINT(); guard.clear(); session.journey.setStepPerformed(SPAWNING_KIT_HANDSHAKE_PERFORM); P_INFO("Preloader for " << options.appRoot << " started on PID " << pid << ", listening on " << socketAddress); } } void stopPreloader() { TRACE_POINT(); boost::this_thread::disable_interruption di; boost::this_thread::disable_syscall_interruption dsi; if (!preloaderStarted()) { return; } preloaderStdin.close(false); if (timedWaitpid(pid, NULL, 5000) == 0) { P_DEBUG("Preloader did not exit in time, killing it..."); syscalls::kill(pid, SIGKILL); syscalls::waitpid(pid, NULL, 0); } // Delete socket after the process has exited so that it // doesn't crash upon deleting a nonexistant file. if (getSocketAddressType(socketAddress) == SAT_UNIX) { string filename = parseUnixSocketAddress(socketAddress); syscalls::unlink(filename.c_str()); } { boost::lock_guard<boost::mutex> l(simpleFieldSyncher); pid = -1; socketAddress.clear(); preloaderEnvvars.clear(); preloaderUserInfo.clear(); preloaderUlimits.clear(); preloaderAnnotations.clear(); } } FileDescriptor connectToPreloader(HandshakeSession &session) { TRACE_POINT(); FileDescriptor fd(connectToServer(socketAddress, __FILE__, __LINE__), NULL, 0); P_LOG_FILE_DESCRIPTOR_PURPOSE(fd, "Preloader " << pid << " (" << session.config->appRoot << ") connection"); return fd; } struct ForkResult { pid_t pid; FileDescriptor stdinFd; FileDescriptor stdoutAndErrFd; string alreadyReadStdoutAndErrData; ForkResult() : pid(-1) { } ForkResult(pid_t _pid, const FileDescriptor &_stdinFd, const FileDescriptor &_stdoutAndErrFd, const string &_alreadyReadStdoutAndErrData) : pid(_pid), stdinFd(_stdinFd), stdoutAndErrFd(_stdoutAndErrFd), alreadyReadStdoutAndErrData(_alreadyReadStdoutAndErrData) { } }; struct PreloaderCrashed { SystemException *systemException; IOException *ioException; PreloaderCrashed(const SystemException &e) : systemException(new SystemException(e)), ioException(NULL) { } PreloaderCrashed(const IOException &e) : systemException(NULL), ioException(new IOException(e)) { } ~PreloaderCrashed() { delete systemException; delete ioException; } const oxt::tracable_exception &getException() const { if (systemException != NULL) { return *systemException; } else { return *ioException; } } }; ForkResult invokeForkCommand(HandshakeSession &session, JourneyStep &stepToMarkAsErrored) { TRACE_POINT(); P_ASSERT_EQ(session.journey.getStepInfo(SPAWNING_KIT_PREPARATION).state, STEP_PERFORMED); try { StdChannelsAsyncOpenStatePtr stdChannelsAsyncOpenState = openStdChannelsFifosAsynchronously(session); return internalInvokeForkCommand(session, stdChannelsAsyncOpenState, stepToMarkAsErrored); } catch (const PreloaderCrashed &crashException1) { UPDATE_TRACE_POINT(); P_WARN("An error occurred while spawning an application process: " << crashException1.getException().what()); P_WARN("The application preloader seems to have crashed," " restarting it and trying again..."); session.journey.reset(); try { stopPreloader(); } catch (const SpawnException &) { throw; } catch (const std::exception &originalException) { session.journey.setStepErrored(SPAWNING_KIT_PREPARATION, true); SpawnException e(originalException, session.journey, session.config); e.setSummary(StaticString("Error stopping a crashed preloader: ") + originalException.what()); e.setProblemDescriptionHTML( "<p>The " PROGRAM_NAME " application server tried" " to start the web application by communicating with a" " helper process that we call a \"preloader\". However," " this helper process crashed unexpectedly. " SHORT_PROGRAM_NAME " then tried to restart it, but" " encountered the following error while trying to" " stop the preloader:</p>" "<pre>" + escapeHTML(originalException.what()) + "</pre>"); throw e.finalize(); } UPDATE_TRACE_POINT(); startPreloader(); session.journey.reset(); session.journey.setStepPerformed(SPAWNING_KIT_PREPARATION, true); UPDATE_TRACE_POINT(); try { StdChannelsAsyncOpenStatePtr stdChannelsAsyncOpenState = openStdChannelsFifosAsynchronously(session); return internalInvokeForkCommand(session, stdChannelsAsyncOpenState, stepToMarkAsErrored); } catch (const PreloaderCrashed &crashException2) { UPDATE_TRACE_POINT(); session.journey.reset(); session.journey.setStepErrored(SPAWNING_KIT_PREPARATION, true); try { stopPreloader(); } catch (const SpawnException &) { throw; } catch (const std::exception &originalException) { SpawnException e(originalException, session.journey, session.config); e.setSummary(StaticString("Error stopping a crashed preloader: ") + originalException.what()); e.setProblemDescriptionHTML( "<p>The " PROGRAM_NAME " application server tried" " to start the web application by communicating with a" " helper process that we call a \"preloader\". However," " this helper process crashed unexpectedly. " SHORT_PROGRAM_NAME " then tried to restart it, but" " encountered the following error while trying to" " stop the preloader:</p>" "<pre>" + escapeHTML(originalException.what()) + "</pre>"); throw e.finalize(); } SpawnException e(crashException2.getException(), session.journey, session.config); e.setSummary(StaticString("An application preloader crashed: ") + crashException2.getException().what()); e.setProblemDescriptionHTML( "<p>The " PROGRAM_NAME " application server tried" " to start the web application by communicating with a" " helper process that we call a \"preloader\". However," " this helper process crashed unexpectedly:</p>" "<pre>" + escapeHTML(crashException2.getException().what()) + "</pre>"); throw e.finalize(); } } } ForkResult internalInvokeForkCommand(HandshakeSession &session, const StdChannelsAsyncOpenStatePtr &stdChannelsAsyncOpenState, JourneyStep &stepToMarkAsErrored) { TRACE_POINT(); P_ASSERT_EQ(session.journey.getStepInfo(SPAWNING_KIT_PREPARATION).state, STEP_PERFORMED); session.journey.setStepInProgress(SPAWNING_KIT_CONNECT_TO_PRELOADER); stepToMarkAsErrored = SPAWNING_KIT_CONNECT_TO_PRELOADER; FileDescriptor fd; string line; Json::Value doc; try { fd = connectToPreloader(session); } catch (const SystemException &e) { throw PreloaderCrashed(e); } catch (const IOException &e) { throw PreloaderCrashed(e); } session.journey.setStepPerformed(SPAWNING_KIT_CONNECT_TO_PRELOADER); session.journey.setStepInProgress(SPAWNING_KIT_SEND_COMMAND_TO_PRELOADER); stepToMarkAsErrored = SPAWNING_KIT_SEND_COMMAND_TO_PRELOADER; try { sendForkCommand(session, fd); } catch (const SystemException &e) { throw PreloaderCrashed(e); } catch (const IOException &e) { throw PreloaderCrashed(e); } session.journey.setStepPerformed(SPAWNING_KIT_SEND_COMMAND_TO_PRELOADER); session.journey.setStepInProgress(SPAWNING_KIT_READ_RESPONSE_FROM_PRELOADER); stepToMarkAsErrored = SPAWNING_KIT_READ_RESPONSE_FROM_PRELOADER; try { line = readForkCommandResponse(session, fd); } catch (const SystemException &e) { throw PreloaderCrashed(e); } catch (const IOException &e) { throw PreloaderCrashed(e); } session.journey.setStepPerformed(SPAWNING_KIT_READ_RESPONSE_FROM_PRELOADER); session.journey.setStepInProgress(SPAWNING_KIT_PARSE_RESPONSE_FROM_PRELOADER); stepToMarkAsErrored = SPAWNING_KIT_PARSE_RESPONSE_FROM_PRELOADER; doc = parseForkCommandResponse(session, line); session.journey.setStepPerformed(SPAWNING_KIT_PARSE_RESPONSE_FROM_PRELOADER); session.journey.setStepInProgress(SPAWNING_KIT_PROCESS_RESPONSE_FROM_PRELOADER); stepToMarkAsErrored = SPAWNING_KIT_PROCESS_RESPONSE_FROM_PRELOADER; return handleForkCommandResponse(session, stdChannelsAsyncOpenState, doc); } void sendForkCommand(HandshakeSession &session, const FileDescriptor &fd) { TRACE_POINT(); Json::Value doc; doc["command"] = "spawn"; doc["work_dir"] = session.workDir->getPath(); writeExact(fd, Json::FastWriter().write(doc), &session.timeoutUsec); } string readForkCommandResponse(HandshakeSession &session, const FileDescriptor &fd) { TRACE_POINT(); BufferedIO io(fd); try { return io.readLine(10240, &session.timeoutUsec); } catch (const SecurityException &) { session.journey.setStepErrored(SPAWNING_KIT_READ_RESPONSE_FROM_PRELOADER); SpawnException e(INTERNAL_ERROR, session.journey, session.config); addPreloaderEnvDumps(e); e.setSummary("The preloader process sent a response that exceeds the maximum size limit."); e.setProblemDescriptionHTML( "<p>The " PROGRAM_NAME " application server tried" " to start the web application by communicating with a" " helper process that we call a \"preloader\". However," " this helper process sent a response that exceeded the" " internally-defined maximum size limit.</p>"); e.setSolutionDescriptionHTML( "<p class=\"sole-solution\">" "This is probably a bug in the preloader process. Please " "<a href=\"" SUPPORT_URL "\">" "report this bug</a>." "</p>"); throw e.finalize(); } } Json::Value parseForkCommandResponse(HandshakeSession &session, const string &data) { TRACE_POINT(); Json::Value doc; Json::Reader reader; if (!reader.parse(data, doc)) { session.journey.setStepErrored(SPAWNING_KIT_PARSE_RESPONSE_FROM_PRELOADER); SpawnException e(INTERNAL_ERROR, session.journey, session.config); addPreloaderEnvDumps(e); e.setSummary("The preloader process sent an unparseable response: " + data); e.setProblemDescriptionHTML( "<p>The " PROGRAM_NAME " application server tried" " to start the web application by communicating with a" " helper process that we call a \"preloader\". However," " this helper process sent a response that looks like" " gibberish.</p>" "<p>The response is as follows:</p>" "<pre>" + escapeHTML(data) + "</pre>"); e.setSolutionDescriptionHTML( "<p class=\"sole-solution\">" "This is probably a bug in the preloader process. Please " "<a href=\"" SUPPORT_URL "\">" "report this bug</a>." "</p>"); throw e.finalize(); } UPDATE_TRACE_POINT(); if (!validateForkCommandResponse(doc)) { session.journey.setStepErrored(SPAWNING_KIT_PARSE_RESPONSE_FROM_PRELOADER); SpawnException e(INTERNAL_ERROR, session.journey, session.config); addPreloaderEnvDumps(e); e.setSummary("The preloader process sent a response that does not" " match the expected structure: " + stringifyJson(doc)); e.setProblemDescriptionHTML( "<p>The " PROGRAM_NAME " application server tried" " to start the web application by communicating with a" " helper process that we call a \"preloader\". However," " this helper process sent a response that does not match" " the structure that " SHORT_PROGRAM_NAME " expects.</p>" "<p>The response is as follows:</p>" "<pre>" + escapeHTML(doc.toStyledString()) + "</pre>"); e.setSolutionDescriptionHTML( "<p class=\"sole-solution\">" "This is probably a bug in the preloader process. Please " "<a href=\"" SUPPORT_URL "\">" "report this bug</a>." "</p>"); throw e.finalize(); } return doc; } bool validateForkCommandResponse(const Json::Value &doc) const { if (!doc.isObject()) { return false; } if (!doc.isMember("result") || !doc["result"].isString()) { return false; } if (doc["result"].asString() == "ok") { if (!doc.isMember("pid") || !doc["pid"].isInt()) { return false; } return true; } else if (doc["result"].asString() == "error") { if (!doc.isMember("message") || !doc["message"].isString()) { return false; } return true; } else { return false; } } ForkResult handleForkCommandResponse(HandshakeSession &session, const StdChannelsAsyncOpenStatePtr &stdChannelsAsyncOpenState, const Json::Value &doc) { TRACE_POINT(); if (doc["result"].asString() == "ok") { return handleForkCommandResponseSuccess(session, stdChannelsAsyncOpenState, doc); } else { P_ASSERT_EQ(doc["result"].asString(), "error"); return handleForkCommandResponseError(session, doc); } } ForkResult handleForkCommandResponseSuccess(HandshakeSession &session, const StdChannelsAsyncOpenStatePtr &stdChannelsAsyncOpenState, const Json::Value &doc) { TRACE_POINT(); pid_t spawnedPid = doc["pid"].asInt(); // How do we know the preloader actually forked a process // instead of reporting the PID of a random other existing process? // For security reasons we perform a bunch of sanity checks, // including checking the PID's UID. if (spawnedPid < 1) { UPDATE_TRACE_POINT(); session.journey.setStepErrored(SPAWNING_KIT_PROCESS_RESPONSE_FROM_PRELOADER); SpawnException e(INTERNAL_ERROR, session.journey, session.config); addPreloaderEnvDumps(e); e.setSummary("The the preloader said it spawned a process with PID " + toString(spawnedPid) + ", which is not allowed."); e.setSubprocessPid(spawnedPid); e.setStdoutAndErrData(getBackgroundIOCapturerData( stdChannelsAsyncOpenState->stdoutAndErrCapturer)); e.setProblemDescriptionHTML( "<h2>Application process has unexpected PID</h2>" "<p>The " PROGRAM_NAME " application server tried" " to start the web application by communicating with a" " helper process that we call a \"preloader\". However," " the preloader reported that it started a process with" " a PID of " + toString(spawnedPid) + ", which is not" " allowed.</p>"); if (!session.config->genericApp && session.config->startsUsingWrapper && session.config->wrapperSuppliedByThirdParty) { e.setSolutionDescriptionHTML( "<h2>Please report this bug</h2>" "<p class=\"sole-solution\">" "This is probably a bug in the preloader process. The preloader " "wrapper program is not written by the " PROGRAM_NAME " authors, " "but by a third party. Please report this bug to the author of " "the preloader wrapper program." "</p>"); } else { e.setSolutionDescriptionHTML( "<h2>Please report this bug</h2>" "<p class=\"sole-solution\">" "This is probably a bug in the preloader process. The preloader " "is an internal tool part of " PROGRAM_NAME ". Please " "<a href=\"" SUPPORT_URL "\">" "report this bug</a>." "</p>"); } throw e.finalize(); } UPDATE_TRACE_POINT(); uid_t spawnedUid = getProcessUid(session, spawnedPid, stdChannelsAsyncOpenState->stdoutAndErrCapturer); if (spawnedUid != session.uid) { UPDATE_TRACE_POINT(); session.journey.setStepErrored(SPAWNING_KIT_PROCESS_RESPONSE_FROM_PRELOADER); SpawnException e(INTERNAL_ERROR, session.journey, session.config); addPreloaderEnvDumps(e); e.setSummary("The process that the preloader said it spawned, PID " + toString(spawnedPid) + ", has UID " + toString(spawnedUid) + ", but the expected UID is " + toString(session.uid)); e.setSubprocessPid(spawnedPid); e.setStdoutAndErrData(getBackgroundIOCapturerData( stdChannelsAsyncOpenState->stdoutAndErrCapturer)); e.setProblemDescriptionHTML( "<h2>Application process has unexpected UID</h2>" "<p>The " PROGRAM_NAME " application server tried" " to start the web application by communicating with a" " helper process that we call a \"preloader\". However," " the web application process that the preloader started" " belongs to the wrong user. The UID of the web" " application process should be " + toString(session.uid) + ", but is actually " + toString(session.uid) + ".</p>"); if (!session.config->genericApp && session.config->startsUsingWrapper && session.config->wrapperSuppliedByThirdParty) { e.setSolutionDescriptionHTML( "<h2>Please report this bug</h2>" "<p class=\"sole-solution\">" "This is probably a bug in the preloader process. The preloader " "wrapper program is not written by the " PROGRAM_NAME " authors, " "but by a third party. Please report this bug to the author of " "the preloader wrapper program." "</p>"); } else { e.setSolutionDescriptionHTML( "<h2>Please report this bug</h2>" "<p class=\"sole-solution\">" "This is probably a bug in the preloader process. The preloader " "is an internal tool part of " PROGRAM_NAME ". Please " "<a href=\"" SUPPORT_URL "\">" "report this bug</a>." "</p>"); } throw e.finalize(); } UPDATE_TRACE_POINT(); ScopeGuard guard(boost::bind(nonInterruptableKillAndWaitpid, spawnedPid)); waitForStdChannelFifosToBeOpenedByPeer(stdChannelsAsyncOpenState, session, spawnedPid); UPDATE_TRACE_POINT(); string alreadyReadStdoutAndErrData; if (stdChannelsAsyncOpenState->stdoutAndErrCapturer != NULL) { stdChannelsAsyncOpenState->stdoutAndErrCapturer->stop(); alreadyReadStdoutAndErrData = stdChannelsAsyncOpenState->stdoutAndErrCapturer->getData(); } guard.clear(); return ForkResult(spawnedPid, stdChannelsAsyncOpenState->stdinFd, stdChannelsAsyncOpenState->stdoutAndErrFd, alreadyReadStdoutAndErrData); } ForkResult handleForkCommandResponseError(HandshakeSession &session, const Json::Value &doc) { session.journey.setStepErrored(SPAWNING_KIT_PROCESS_RESPONSE_FROM_PRELOADER); SpawnException e(INTERNAL_ERROR, session.journey, session.config); addPreloaderEnvDumps(e); e.setSummary("An error occured while starting the web application: " + doc["message"].asString()); e.setProblemDescriptionHTML( "<p>The " PROGRAM_NAME " application server tried to" " start the web application by communicating with a" " helper process that we call a \"preloader\". However, " " this helper process reported an error:</p>" "<pre>" + escapeHTML(doc["message"].asString()) + "</pre>"); e.setSolutionDescriptionHTML( "<p class=\"sole-solution\">" "Please try troubleshooting the problem by studying the" " <strong>error message</strong> and the" " <strong>diagnostics</strong> reports. You can also" " consult <a href=\"" SUPPORT_URL "\">the " SHORT_PROGRAM_NAME " support resources</a> for help.</p>"); throw e.finalize(); } void createStdChannelFifos(const HandshakeSession &session) { const string &workDir = session.workDir->getPath(); createFifo(session, workDir + "/stdin"); createFifo(session, workDir + "/stdout_and_err"); } void createFifo(const HandshakeSession &session, const string &path) { int ret; do { ret = mkfifo(path.c_str(), 0600); } while (ret == -1 && errno == EAGAIN); if (ret == -1) { int e = errno; throw FileSystemException("Cannot create FIFO file " + path, e, path); } ret = syscalls::chown(path.c_str(), session.uid, session.gid); if (ret == -1) { int e = errno; throw FileSystemException("Cannot change owner and group on FIFO file " + path, e, path); } } string getBackgroundIOCapturerData(const BackgroundIOCapturerPtr &capturer) const { if (capturer != NULL) { // Sleep shortly to allow the child process to finish writing logs. syscalls::usleep(50000); return capturer->getData(); } else { return string(); } } uid_t getProcessUid(HandshakeSession &session, pid_t pid, const BackgroundIOCapturerPtr &stdoutAndErrCapturer) { TRACE_POINT(); uid_t uid = (uid_t) -1; try { vector<pid_t> pids; pids.push_back(pid); ProcessMetricMap result = ProcessMetricsCollector().collect(pids); uid = result[pid].uid; } catch (const ParseException &) { HandshakePerform::loadJourneyStateFromResponseDir(session, pid, stdoutAndErrCapturer); session.journey.setStepErrored(SPAWNING_KIT_PROCESS_RESPONSE_FROM_PRELOADER); SpawnException e(INTERNAL_ERROR, session.journey, session.config); addPreloaderEnvDumps(e); e.setSummary("Unable to query the UID of spawned application process " + toString(pid) + ": error parsing 'ps' output"); e.setSubprocessPid(pid); e.setProblemDescriptionHTML( "<h2>Unable to use 'ps' to query PID " + toString(pid) + "</h2>" "<p>The " PROGRAM_NAME " application server tried" " to start the web application. As part of the starting" " procedure, " SHORT_PROGRAM_NAME " also tried to query" " the system user ID of the web application process" " using the operating system's \"ps\" tool. However," " this tool returned output that " SHORT_PROGRAM_NAME " could not understand.</p>"); e.setSolutionDescriptionHTML( createSolutionDescriptionForProcessMetricsCollectionError()); throw e.finalize(); } catch (const SystemException &originalException) { HandshakePerform::loadJourneyStateFromResponseDir(session, pid, stdoutAndErrCapturer); session.journey.setStepErrored(SPAWNING_KIT_PROCESS_RESPONSE_FROM_PRELOADER); SpawnException e(OPERATING_SYSTEM_ERROR, session.journey, session.config); addPreloaderEnvDumps(e); e.setSummary("Unable to query the UID of spawned application process " + toString(pid) + "; error capturing 'ps' output: " + originalException.what()); e.setSubprocessPid(pid); e.setProblemDescriptionHTML( "<h2>Error capturing 'ps' output for PID " + toString(pid) + "</h2>" "<p>The " PROGRAM_NAME " application server tried" " to start the web application. As part of the starting" " procedure, " SHORT_PROGRAM_NAME " also tried to query" " the system user ID of the web application process." " This is done by using the operating system's \"ps\"" " tool and by querying operating system APIs and special" " files. However, an error was encountered while doing" " one of those things.</p>" "<p>The error returned by the operating system is as follows:</p>" "<pre>" + escapeHTML(originalException.what()) + "</pre>"); e.setSolutionDescriptionHTML( createSolutionDescriptionForProcessMetricsCollectionError()); throw e.finalize(); } UPDATE_TRACE_POINT(); if (uid == (uid_t) -1) { if (osProcessExists(pid)) { HandshakePerform::loadJourneyStateFromResponseDir(session, pid, stdoutAndErrCapturer); session.journey.setStepErrored(SPAWNING_KIT_PROCESS_RESPONSE_FROM_PRELOADER); SpawnException e(INTERNAL_ERROR, session.journey, session.config); addPreloaderEnvDumps(e); e.setSummary("Unable to query the UID of spawned application process " + toString(pid) + ": 'ps' did not report information" " about this process"); e.setSubprocessPid(pid); e.setProblemDescriptionHTML( "<h2>'ps' did not return any information about PID " + toString(pid) + "</h2>" "<p>The " PROGRAM_NAME " application server tried" " to start the web application. As part of the starting" " procedure, " SHORT_PROGRAM_NAME " also tried to query" " the system user ID of the web application process" " using the operating system's \"ps\" tool. However," " this tool did not return any information about" " the web application process.</p>"); e.setSolutionDescriptionHTML( createSolutionDescriptionForProcessMetricsCollectionError()); throw e.finalize(); } else { HandshakePerform::loadJourneyStateFromResponseDir(session, pid, stdoutAndErrCapturer); session.journey.setStepErrored(SPAWNING_KIT_PROCESS_RESPONSE_FROM_PRELOADER); SpawnException e(INTERNAL_ERROR, session.journey, session.config); addPreloaderEnvDumps(e); e.setSummary("The application process spawned from the preloader" " seems to have exited prematurely"); e.setSubprocessPid(pid); e.setStdoutAndErrData(getBackgroundIOCapturerData(stdoutAndErrCapturer)); e.setProblemDescriptionHTML( "<h2>Application process exited prematurely</h2>" "<p>The " PROGRAM_NAME " application server tried" " to start the web application. As part of the starting" " procedure, " SHORT_PROGRAM_NAME " also tried to query" " the system user ID of the web application process" " using the operating system's \"ps\" tool. However," " this tool did not return any information about" " the web application process.</p>"); e.setSolutionDescriptionHTML( createSolutionDescriptionForProcessMetricsCollectionError()); throw e.finalize(); } } else { return uid; } } static string createSolutionDescriptionForProcessMetricsCollectionError() { const char *path = getenv("PATH"); if (path == NULL || path[0] == '\0') { path = "(empty)"; } return "<div class=\"multiple-solutions\">" "<h3>Check whether the \"ps\" tool is installed and accessible by " SHORT_PROGRAM_NAME "</h3>" "<p>Maybe \"ps\" is not installed. Or maybe it is installed, but " SHORT_PROGRAM_NAME " cannot find it inside its PATH. Or" " maybe filesystem permissions disallow " SHORT_PROGRAM_NAME " from accessing \"ps\". Please check all these factors and" " fix them if necessary.</p>" "<p>" SHORT_PROGRAM_NAME "'s PATH is:</p>" "<pre>" + escapeHTML(path) + "</pre>" "<h3>Check whether the server is low on resources</h3>" "<p>Maybe the server is currently low on resources. This would" " cause the \"ps\" tool to encounter errors. Please study the" " <em>error message</em> and the <em>diagnostics reports</em> to" " verify whether this is the case. Key things to check for:</p>" "<ul>" "<li>Excessive CPU usage</li>" "<li>Memory and swap</li>" "<li>Ulimits</li>" "</ul>" "<p>If the server is indeed low on resources, find a way to" " free up some resources.</p>" "<h3>Check whether /proc is mounted</h3>" "<p>On many operating systems including Linux and FreeBSD, \"ps\"" " only works if /proc is mounted. Please check this.</p>" "<h3>Still no luck?</h3>" "<p>Please try troubleshooting the problem by studying the" " <em>diagnostics</em> reports.</p>" "</div>"; } static void adjustTimeout(MonotonicTimeUsec startTime, unsigned long long *timeout) { boost::this_thread::disable_interruption di; boost::this_thread::disable_syscall_interruption dsi; MonotonicTimeUsec now = SystemTime::getMonotonicUsec(); assert(now >= startTime); MonotonicTimeUsec diff = now - startTime; if (*timeout >= diff) { *timeout -= diff; } else { *timeout = 0; } } static void doClosedir(DIR *dir) { closedir(dir); } static string findPreloaderCommandSocketAddress(const HandshakeSession &session) { const vector<Result::Socket> &sockets = session.result.sockets; vector<Result::Socket>::const_iterator it, end = sockets.end(); for (it = sockets.begin(); it != end; it++) { if (it->protocol == "preloader") { return it->address; } } return string(); } static StringKeyTable<string> loadAnnotationsFromEnvDumpDir(const string &envDumpDir, int envDumpAnnotationsDirFd) { string path = envDumpDir + "/annotations"; DIR *dir = opendir(path.c_str()); if (dir == NULL) { return StringKeyTable<string>(); } ScopeGuard guard(boost::bind(doClosedir, dir)); StringKeyTable<string> result; struct dirent *ent; while ((ent = readdir(dir)) != NULL) { if (ent->d_name[0] != '.') { result.insert(ent->d_name, strip(safeReadFile(envDumpAnnotationsDirFd, ent->d_name, SPAWNINGKIT_MAX_SUBPROCESS_ENVDUMP_SIZE).first), true); } } result.compact(); return result; } void addPreloaderEnvDumps(SpawnException &e) const { e.setPreloaderPid(pid); e.setPreloaderEnvvars(preloaderEnvvars); e.setPreloaderUserInfo(preloaderUserInfo); e.setPreloaderUlimits(preloaderUlimits); if (e.getSubprocessEnvvars().empty()) { e.setSubprocessEnvvars(preloaderEnvvars); } if (e.getSubprocessUserInfo().empty()) { e.setSubprocessUserInfo(preloaderUserInfo); } if (e.getSubprocessUlimits().empty()) { e.setSubprocessUlimits(preloaderUlimits); } StringKeyTable<string>::ConstIterator it(preloaderAnnotations); while (*it != NULL) { e.setAnnotation(it.getKey(), it.getValue(), false); it.next(); } } public: SmartSpawner(Context *context, const vector<string> &preloaderCommand, const AppPoolOptions &_options) : Spawner(context), preloaderCommandString(createCommandString(preloaderCommand)) { if (preloaderCommand.size() < 2) { throw ArgumentException("preloaderCommand must have at least 2 elements"); } options = _options.copyAndPersist(); pid = -1; m_lastUsed = SystemTime::getUsec(); } virtual ~SmartSpawner() { boost::lock_guard<boost::mutex> l(syncher); stopPreloader(); } virtual Result spawn(const AppPoolOptions &options) { TRACE_POINT(); P_ASSERT_EQ(options.appType, this->options.appType); P_ASSERT_EQ(options.appRoot, this->options.appRoot); P_DEBUG("Spawning new process: appRoot=" << options.appRoot); possiblyRaiseInternalError(options); { boost::lock_guard<boost::mutex> l(simpleFieldSyncher); m_lastUsed = SystemTime::getUsec(); } UPDATE_TRACE_POINT(); boost::lock_guard<boost::mutex> l(syncher); if (!preloaderStarted()) { UPDATE_TRACE_POINT(); startPreloader(); } UPDATE_TRACE_POINT(); Config config; Json::Value extraArgs; try { setConfigFromAppPoolOptions(&config, extraArgs, options); } catch (const std::exception &originalException) { Journey journey(SPAWN_THROUGH_PRELOADER, true); journey.setStepErrored(SPAWNING_KIT_PREPARATION, true); SpawnException e(originalException, journey, &config); addPreloaderEnvDumps(e); throw e.finalize(); } UPDATE_TRACE_POINT(); HandshakeSession session(*context, config, SPAWN_THROUGH_PRELOADER); session.journey.setStepInProgress(SPAWNING_KIT_PREPARATION); JourneyStep stepToMarkAsErrored = SPAWNING_KIT_PREPARATION; try { UPDATE_TRACE_POINT(); HandshakePrepare prepare(session, extraArgs); prepare.execute(); createStdChannelFifos(session); prepare.finalize(); session.journey.setStepPerformed(SPAWNING_KIT_PREPARATION, true); UPDATE_TRACE_POINT(); ForkResult forkResult = invokeForkCommand(session, stepToMarkAsErrored); UPDATE_TRACE_POINT(); ScopeGuard guard(boost::bind(nonInterruptableKillAndWaitpid, forkResult.pid)); P_DEBUG("Process forked for appRoot=" << options.appRoot << ": PID " << forkResult.pid); UPDATE_TRACE_POINT(); session.journey.setStepPerformed(SPAWNING_KIT_PROCESS_RESPONSE_FROM_PRELOADER); session.journey.setStepInProgress(PRELOADER_PREPARATION); session.journey.setStepInProgress(SPAWNING_KIT_HANDSHAKE_PERFORM); stepToMarkAsErrored = SPAWNING_KIT_HANDSHAKE_PERFORM; HandshakePerform(session, forkResult.pid, forkResult.stdinFd, forkResult.stdoutAndErrFd, forkResult.alreadyReadStdoutAndErrData). execute(); guard.clear(); session.journey.setStepPerformed(SPAWNING_KIT_HANDSHAKE_PERFORM); P_DEBUG("Process spawning done: appRoot=" << options.appRoot << ", pid=" << forkResult.pid); return session.result; } catch (SpawnException &e) { addPreloaderEnvDumps(e); throw e; } catch (const std::exception &originalException) { session.journey.setStepErrored(stepToMarkAsErrored, true); SpawnException e(originalException, session.journey, &config); addPreloaderEnvDumps(e); throw e.finalize(); } } virtual bool cleanable() const { return true; } virtual void cleanup() { TRACE_POINT(); { boost::lock_guard<boost::mutex> l(simpleFieldSyncher); m_lastUsed = SystemTime::getUsec(); } boost::lock_guard<boost::mutex> lock(syncher); stopPreloader(); } virtual unsigned long long lastUsed() const { boost::lock_guard<boost::mutex> lock(simpleFieldSyncher); return m_lastUsed; } pid_t getPreloaderPid() const { boost::lock_guard<boost::mutex> lock(simpleFieldSyncher); return pid; } }; } // namespace SpawningKit } // namespace Passenger #endif /* _PASSENGER_SPAWNING_KIT_SMART_SPAWNER_H_ */ SpawningKit/Handshake/Prepare.h 0000644 00000043337 14756456557 0012501 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2016-2018 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef _PASSENGER_SPAWNING_KIT_HANDSHAKE_PREPARE_H_ #define _PASSENGER_SPAWNING_KIT_HANDSHAKE_PREPARE_H_ #include <oxt/backtrace.hpp> #include <boost/thread.hpp> #include <boost/scoped_array.hpp> #include <string> #include <vector> #include <stdexcept> #include <algorithm> #include <utility> #include <cerrno> #include <cstddef> #include <cassert> #include <sys/types.h> #include <sys/stat.h> #include <sys/un.h> #include <sys/socket.h> #include <pwd.h> #include <grp.h> #include <unistd.h> #include <limits.h> #include <jsoncpp/json.h> #include <Constants.h> #include <LoggingKit/LoggingKit.h> #include <StaticString.h> #include <Exceptions.h> #include <FileTools/FileManip.h> #include <FileTools/PathManip.h> #include <SystemTools/UserDatabase.h> #include <SystemTools/SystemTime.h> #include <Utils/Timer.h> #include <IOTools/IOUtils.h> #include <StrIntTools/StrIntUtils.h> #include <Core/SpawningKit/Context.h> #include <Core/SpawningKit/Config.h> #include <Core/SpawningKit/Journey.h> #include <Core/SpawningKit/Exceptions.h> #include <Core/SpawningKit/Handshake/Session.h> #include <Core/SpawningKit/Handshake/WorkDir.h> namespace Passenger { namespace SpawningKit { using namespace std; using namespace oxt; /** * For an introduction see README.md, section * "The preparation and the HandshakePrepare class". */ class HandshakePrepare { private: HandshakeSession &session; Context * const context; Config * const config; Json::Value args; Timer<SystemTime::GRAN_10MSEC> timer; void resolveUserAndGroup() { TRACE_POINT(); string username = config->user.toString(); // null terminate string string groupname = config->group.toString(); // null terminate string OsUser osUser; OsGroup osGroup; if (lookupSystemUserByName(username, osUser)) { session.uid = osUser.pwd.pw_uid; session.shell = osUser.pwd.pw_shell; session.homedir = osUser.pwd.pw_dir; } else { if (looksLikePositiveNumber(username)) { P_WARN("OS user account '" << username << "' does not exist." " Will assume that this is a UID."); session.uid = (uid_t) atoi(username); } else { throw RuntimeException("OS user account '" + username + "' does not exist"); } } if (lookupSystemGroupByName(groupname, osGroup)) { session.gid = osGroup.grp.gr_gid; } else { if (looksLikePositiveNumber(groupname)) { P_WARN("OS group account '" << groupname << "' does not exist." " Will assume that this is a GID."); session.gid = (gid_t) atoi(groupname); } else { throw RuntimeException("OS group account '" + groupname + "' does not exist"); } } } void createWorkDir() { TRACE_POINT(); session.workDir.reset(new HandshakeWorkDir(context->spawnDir)); session.envDumpDir = session.workDir->getPath() + "/envdump"; makeDirTree(session.envDumpDir, "u=rwx,g=,o=", session.uid, session.gid); makeDirTree(session.envDumpDir + "/annotations", "u=rwx,g=,o=", session.uid, session.gid); session.responseDir = session.workDir->getPath() + "/response"; makeDirTree(session.responseDir, "u=rwx,g=,o=", session.uid, session.gid); createFifo(session.responseDir + "/finish"); makeDirTree(session.responseDir + "/error", "u=rwx,g=,o=", session.uid, session.gid); makeDirTree(session.responseDir + "/steps", "u=rwx,g=,o=", session.uid, session.gid); createJourneyStepDirs(getFirstSubprocessJourneyStep(), getLastSubprocessJourneyStep()); createJourneyStepDirs(getFirstPreloaderJourneyStep(), // Also create directory for PRELOADER_FINISH; // the preloader will want to write there. JourneyStep((int) getLastPreloaderJourneyStep() + 1)); } void createJourneyStepDirs(JourneyStep firstStep, JourneyStep lastStep) { JourneyStep step; for (step = firstStep; step < lastStep; step = JourneyStep((int) step + 1)) { if (!session.journey.hasStep(step)) { continue; } string stepString = journeyStepToStringLowerCase(step); string stepDir = session.responseDir + "/steps/" + stepString; makeDirTree(stepDir, "u=rwx,g=,o=", session.uid, session.gid); } } void createFifo(const string &path) { int ret; do { ret = mkfifo(path.c_str(), 0600); } while (ret == -1 && errno == EINTR); if (ret == -1) { int e = errno; throw FileSystemException("Cannot create FIFO file " + path, e, path); } ret = syscalls::chown(path.c_str(), session.uid, session.gid); if (ret == -1) { int e = errno; throw FileSystemException( "Cannot change ownership for FIFO file " + path, e, path); } } // Open various workdir subdirectories because we'll use these file descriptors later in // safeReadFile() calls. void openWorkDirSubdirFds() { session.workDirFd = openDirFd(session.workDir->getPath()); session.responseDirFd = openDirFd(session.responseDir); session.responseErrorDirFd = openDirFd(session.responseDir + "/error"); session.envDumpDirFd = openDirFd(session.envDumpDir); session.envDumpAnnotationsDirFd = openDirFd(session.envDumpDir + "/annotations"); openJourneyStepDirFds(getFirstSubprocessJourneyStep(), getLastSubprocessJourneyStep()); openJourneyStepDirFds(getFirstPreloaderJourneyStep(), JourneyStep((int) getLastPreloaderJourneyStep() + 1)); } void openJourneyStepDirFds(JourneyStep firstStep, JourneyStep lastStep) { JourneyStep step; for (step = firstStep; step < lastStep; step = JourneyStep((int) step + 1)) { if (!session.journey.hasStep(step)) { continue; } string stepString = journeyStepToStringLowerCase(step); string stepDir = session.responseDir + "/steps/" + stepString; session.stepDirFds.insert(make_pair(step, openDirFd(stepDir))); } } int openDirFd(const string &path) { int fd = open(path.c_str(), O_RDONLY); if (fd == -1) { int e = errno; throw FileSystemException("Cannot open " + path, e, path); } return fd; } void initializeResult() { session.result.initialize(*context, config); } void preparePredefinedArgs() { TRACE_POINT(); struct sockaddr_un addr; args["passenger_root"] = context->resourceLocator->getInstallSpec(); args["passenger_version"] = PASSENGER_VERSION; args["passenger_agent_path"] = context->resourceLocator->findSupportBinary(AGENT_EXE); args["ruby_libdir"] = context->resourceLocator->getRubyLibDir(); args["node_libdir"] = context->resourceLocator->getNodeLibDir(); args["integration_mode"] = context->integrationMode; args["gupid"] = session.result.gupid; args["UNIX_PATH_MAX"] = (Json::UInt64) sizeof(addr.sun_path) - 1; if (config->preloadBundler) { args["preload_bundler"] = config->preloadBundler; } if (config->genericApp || config->findFreePort) { args["expected_start_port"] = session.expectedStartPort; } if (!config->apiKey.empty()) { args["connect_password"] = config->apiKey.toString(); } if (!context->instanceDir.empty()) { args["instance_dir"] = context->instanceDir; args["socket_dir"] = context->instanceDir + "/apps.s"; } } void prepareArgsFromAppConfig() { TRACE_POINT(); const Json::Value appConfigJson = config->getConfidentialFieldsToPassToApp(); Json::Value::const_iterator it, end = appConfigJson.end(); for (it = appConfigJson.begin(); it != end; it++) { args[it.name()] = *it; } } void absolutizeKeyArgPaths() { TRACE_POINT(); args["app_root"] = absolutizePath(args["app_root"].asString()); if (args.isMember("startup_file")) { args["startup_file"] = absolutizePath(args["startup_file"].asString(), args["app_root"].asString()); } } void dumpArgsIntoWorkDir() { TRACE_POINT(); P_DEBUG("[App spawn arg] " << args.toStyledString()); // The workDir is a new random dir. the files that we create here // should not exist, so if any exist then have createFile() // throw an error because it could be a bug or an attack. createFile(session.workDir->getPath() + "/args.json", args.toStyledString(), 0600, session.uid, session.gid, false, __FILE__, __LINE__); const string dir = session.workDir->getPath() + "/args"; makeDirTree(dir, "u=rwx,g=,o=", session.uid, session.gid); const Json::Value &constArgs = const_cast<const Json::Value &>(args); Json::Value::const_iterator it, end = constArgs.end(); for (it = constArgs.begin(); it != end; it++) { const Json::Value &value = *it; switch (value.type()) { case Json::nullValue: case Json::intValue: case Json::uintValue: case Json::realValue: case Json::stringValue: case Json::booleanValue: createFile(dir + "/" + it.name(), jsonValueToString(*it), 0600, session.uid, session.gid, false, __FILE__, __LINE__); break; default: createFile(dir + "/" + it.name() + ".json", jsonValueToString(*it), 0600, session.uid, session.gid, false, __FILE__, __LINE__); break; } } } string jsonValueToString(const Json::Value &value) const { switch (value.type()) { case Json::nullValue: return string(); case Json::intValue: return toString(value.asInt64()); case Json::uintValue: return toString(value.asUInt64()); case Json::realValue: return toString(value.asDouble()); case Json::stringValue: return value.asString(); case Json::booleanValue: if (value.asBool()) { return "true"; } else { return "false"; } default: return value.toStyledString(); } } #if 0 void inferApplicationInfo() const { TRACE_POINT(); session.result.codeRevision = readFromRevisionFile(); if (session.result.codeRevision.empty()) { session.result.codeRevision = inferCodeRevisionFromCapistranoSymlink(); } } string readFromRevisionFile() const { TRACE_POINT(); string filename = config->appRoot + "/REVISION"; try { if (fileExists(filename)) { return strip(readAll(filename)); } } catch (const SystemException &e) { P_WARN("Cannot access " << filename << ": " << e.what()); } return string(); } string inferCodeRevisionFromCapistranoSymlink() const { TRACE_POINT(); if (extractBaseName(config->appRoot) == "current") { string appRoot = config->appRoot.toString(); // null terminate string char buf[PATH_MAX + 1]; ssize_t ret; do { ret = readlink(appRoot.c_str(), buf, PATH_MAX); } while (ret == -1 && errno == EINTR); if (ret == -1) { if (errno == EINVAL) { return string(); } else { int e = errno; P_WARN("Cannot read symlink " << appRoot << ": " << strerror(e)); } } buf[ret] = '\0'; return extractBaseName(buf); } else { return string(); } } #endif void findFreePortOrSocketFile() { TRACE_POINT(); session.expectedStartPort = findFreePort(); if (session.expectedStartPort == 0) { throwSpawnExceptionBecauseOfFailureToFindFreePort(); } // TODO: support Unix domain sockets in the future // session.expectedStartSocketFile = findFreeSocketFile(); } unsigned int findFreePort() { TRACE_POINT(); unsigned int tryCount = 1; unsigned int maxTries; while (true) { unsigned int port; boost::this_thread::interruption_point(); { boost::lock_guard<boost::mutex> l(context->syncher); port = context->nextPort; context->nextPort++; if (context->nextPort > context->maxPortRange) { context->nextPort = context->minPortRange; } maxTries = context->maxPortRange - context->minPortRange + 1; } unsigned long long timeout1 = 100000; unsigned long long timeout2 = 100000; if (!pingTcpServer("127.0.0.1", port, &timeout1) && !pingTcpServer("0.0.0.0", port, &timeout2)) { return port; } else if (tryCount >= maxTries) { return 0; } else if (timer.usecElapsed() >= session.timeoutUsec) { throwSpawnExceptionBecauseOfPortFindingTimeout(); } // else: try again } } void adjustTimeout() { unsigned long long elapsed = timer.usecElapsed(); if (elapsed >= session.timeoutUsec) { session.timeoutUsec = 0; } else { session.timeoutUsec -= elapsed; } } void throwSpawnExceptionBecauseOfPortFindingTimeout() { assert(config->genericApp || config->findFreePort); SpawnException e(TIMEOUT_ERROR, session.journey, config); e.setProblemDescriptionHTML( "<p>The " PROGRAM_NAME " application server tried" " to look for a free TCP port for the web application" " to start on. But this took too much time, so " SHORT_PROGRAM_NAME " put a stop to that.</p>"); unsigned int minPortRange, maxPortRange; { boost::lock_guard<boost::mutex> l(context->syncher); minPortRange = context->minPortRange; maxPortRange = context->maxPortRange; } e.setSolutionDescriptionHTML( "<div class=\"multiple-solutions\">" "<h3>Check whether the server is low on resources</h3>" "<p>Maybe the server is currently so low on resources that" " all the work that needed to be done, could not finish within" " the given time limit." " Please inspect the server resource utilization statistics" " in the <em>diagnostics</em> section to verify" " whether server is indeed low on resources.</p>" "<p>If so, then either increase the spawn timeout (currently" " configured at " + toString(config->startTimeoutMsec / 1000) + " sec), or find a way to lower the server's resource" " utilization.</p>" "<h3>Limit the port range that " SHORT_PROGRAM_NAME " searches in</h3>" "<p>Maybe the port range in which " SHORT_PROGRAM_NAME " tried to search for a free port for the application is" " large, and at the same time there were very few free ports" " available.</p>" "<p>If this is the case, then please configure the " SHORT_PROGRAM_NAME " application spawning port range" " to a range that is known to have many free ports. The port" " range is currently configured at " + toString(minPortRange) + "-" + toString(maxPortRange) + ".</p>" "</div>" ); throw e.finalize(); } void throwSpawnExceptionBecauseOfFailureToFindFreePort() { assert(config->genericApp || config->findFreePort); unsigned int minPortRange, maxPortRange; { boost::lock_guard<boost::mutex> l(context->syncher); minPortRange = context->minPortRange; maxPortRange = context->maxPortRange; } SpawnException e(INTERNAL_ERROR, session.journey, config); e.setSummary("Could not find a free port to spawn the application on."); e.setProblemDescriptionHTML( "<p>The " PROGRAM_NAME " application server tried" " to look for a free TCP port for the web application" " to start on, but was unable to find one.</p>"); e.setSolutionDescriptionHTML( "<div class=\"sole-solutions\">" "<p>Maybe the port range in which " SHORT_PROGRAM_NAME " tried to search for a free port, had very few or no" " free ports.</p>" "<p>If this is the case, then please configure the " SHORT_PROGRAM_NAME " application spawning port range" " to a range that is known to have many free ports. The port" " range is currently configured at " + toString(minPortRange) + "-" + toString(maxPortRange) + ".</p>" "</div>"); throw e.finalize(); } public: struct DebugSupport { virtual ~DebugSupport() { } virtual void beforeAdjustTimeout() { } }; DebugSupport *debugSupport; HandshakePrepare(HandshakeSession &_session, const Json::Value &extraArgs = Json::Value()) : session(_session), context(_session.context), config(_session.config), args(extraArgs), timer(false), debugSupport(NULL) { assert(_session.context != NULL); assert(_session.context->isFinalized()); assert(_session.config != NULL); } HandshakePrepare &execute() { TRACE_POINT(); // We do not set SPAWNING_KIT_PREPARATION to the IN_PROGRESS or // PERFORMED state here. That will be done by the caller because // it may want to perform additional preparation. try { timer.start(); resolveUserAndGroup(); createWorkDir(); openWorkDirSubdirFds(); initializeResult(); UPDATE_TRACE_POINT(); // Disabled to fix CVE-2017-16355 //inferApplicationInfo(); if (config->genericApp || config->findFreePort) { findFreePortOrSocketFile(); } UPDATE_TRACE_POINT(); preparePredefinedArgs(); prepareArgsFromAppConfig(); absolutizeKeyArgPaths(); dumpArgsIntoWorkDir(); if (debugSupport != NULL) { debugSupport->beforeAdjustTimeout(); } adjustTimeout(); } catch (const SpawnException &) { session.journey.setStepErrored(SPAWNING_KIT_PREPARATION); throw; } catch (const std::exception &e) { session.journey.setStepErrored(SPAWNING_KIT_PREPARATION); throw SpawnException(e, session.journey, config).finalize(); } return *this; } void finalize() { session.workDir->finalize(session.uid, session.gid); } }; } // namespace SpawningKit } // namespace Passenger #endif /* _PASSENGER_SPAWNING_KIT_HANDSHAKE_PREPARE_H_ */ SpawningKit/Handshake/Session.h 0000644 00000007124 14756456557 0012520 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2016-2018 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef _PASSENGER_SPAWNING_KIT_HANDSHAKE_SESSION_H_ #define _PASSENGER_SPAWNING_KIT_HANDSHAKE_SESSION_H_ #include <boost/scoped_ptr.hpp> #include <string> #include <map> #include <Utils.h> #include <Core/SpawningKit/Context.h> #include <Core/SpawningKit/Config.h> #include <Core/SpawningKit/Journey.h> #include <Core/SpawningKit/Result.h> #include <Core/SpawningKit/Handshake/WorkDir.h> namespace Passenger { namespace SpawningKit { using namespace std; struct HandshakeSession { Context *context; Config *config; boost::scoped_ptr<HandshakeWorkDir> workDir; string responseDir; string envDumpDir; int workDirFd; int responseDirFd; int responseErrorDirFd; int envDumpDirFd; int envDumpAnnotationsDirFd; map<JourneyStep, int> stepDirFds; Journey journey; Result result; uid_t uid; gid_t gid; string homedir; string shell; unsigned long long timeoutUsec; /** * The port that the application is expected to start on. Only meaningful * if `config->genericApp || config->findFreePort`. */ unsigned int expectedStartPort; HandshakeSession(Context &_context, Config &_config, JourneyType journeyType) : context(&_context), config(&_config), workDirFd(-1), responseDirFd(-1), responseErrorDirFd(-1), envDumpDirFd(-1), envDumpAnnotationsDirFd(-1), journey(journeyType, !_config.genericApp && _config.startsUsingWrapper), uid(USER_NOT_GIVEN), gid(GROUP_NOT_GIVEN), timeoutUsec(_config.startTimeoutMsec * 1000), expectedStartPort(0) { } ~HandshakeSession() { if (workDirFd != -1) { safelyClose(workDirFd, true); } if (responseDirFd != -1) { safelyClose(responseDirFd, true); } if (responseErrorDirFd != -1) { safelyClose(responseErrorDirFd, true); } if (envDumpDirFd != -1) { safelyClose(envDumpDirFd, true); } if (envDumpAnnotationsDirFd != -1) { safelyClose(envDumpAnnotationsDirFd, true); } map<JourneyStep, int>::iterator it, end = stepDirFds.end(); for (it = stepDirFds.begin(); it != end; it++) { safelyClose(it->second); } if (config->debugWorkDir && workDir != NULL) { string path = workDir->dontRemoveOnDestruction(); P_NOTICE("Work directory " << path << " preserved for debugging"); } } }; } // namespace SpawningKit } // namespace Passenger #endif /* _PASSENGER_SPAWNING_KIT_HANDSHAKE_SESSION_H_ */ SpawningKit/Handshake/WorkDir.h 0000644 00000006713 14756456557 0012461 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2011-2018 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef _PASSENGER_SPAWNING_KIT_HANDSHAKE_WORKDIR_H_ #define _PASSENGER_SPAWNING_KIT_HANDSHAKE_WORKDIR_H_ #include <oxt/system_calls.hpp> #include <string> #include <cerrno> #include <sys/types.h> #include <limits.h> #include <unistd.h> #include <Exceptions.h> #include <Utils.h> #include <StrIntTools/StrIntUtils.h> namespace Passenger { namespace SpawningKit { using namespace std; /** * A temporary directory for handshaking with a child process * during spawning. It is removed after spawning is finished * or has failed. */ class HandshakeWorkDir { private: string path; public: HandshakeWorkDir(const std::string& spawn_dir) { char buf[PATH_MAX + 1]; char *pos = buf; const char *end = buf + PATH_MAX; if (spawn_dir.empty()) throw RuntimeException("spawn_dir is not set"); pos = appendData(pos, end, spawn_dir.c_str()); pos = appendData(pos, end, "/passenger.spawn.XXXXXXXXXX"); *pos = '\0'; const char *result = mkdtemp(buf); if (result == NULL) { int e = errno; throw SystemException("Cannot create a temporary directory " "in the format of '" + StaticString(buf) + "'", e); } else { path = result; } } ~HandshakeWorkDir() { if (!path.empty()) { removeDirTree(path); } } const string &getPath() const { return path; } void finalize(uid_t uid, gid_t gid) { finalize(path, uid, gid); } string dontRemoveOnDestruction() { string result = path; path.clear(); return result; } static void finalize(const string &path, uid_t uid, gid_t gid) { // We do not chown() the work dir until: // // - HandshakePrepare is done populating the work dir, // - SpawnEnvSetupperMain is done reading from and modifying the work dir // // This way, the application user cannot perform symlink attacks // inside the work dir until we are done (at which point the // follow-up code will only perform read/write operations after // dropping root privileges). boost::this_thread::disable_interruption di; boost::this_thread::disable_syscall_interruption dsi; syscalls::chown(path.c_str(), uid, gid); } }; } // namespace SpawningKit } // namespace Passenger #endif /* _PASSENGER_SPAWNING_KIT_HANDSHAKE_WORKDIR_H_ */ SpawningKit/Handshake/Perform.h 0000644 00000174134 14756456557 0012515 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2016-2018 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef _PASSENGER_SPAWNING_KIT_HANDSHAKE_PERFORM_H_ #define _PASSENGER_SPAWNING_KIT_HANDSHAKE_PERFORM_H_ #include <boost/thread.hpp> #include <boost/make_shared.hpp> #include <boost/bind/bind.hpp> #include <oxt/thread.hpp> #include <oxt/system_calls.hpp> #include <oxt/backtrace.hpp> #include <string> #include <vector> #include <stdexcept> #include <cstddef> #include <cstdlib> #include <cerrno> #include <cassert> #include <sys/types.h> #include <dirent.h> #include <jsoncpp/json.h> #include <Constants.h> #include <Exceptions.h> #include <FileDescriptor.h> #include <FileTools/FileManip.h> #include <FileTools/PathManip.h> #include <Utils.h> #include <Utils/ScopeGuard.h> #include <SystemTools/SystemTime.h> #include <StrIntTools/StrIntUtils.h> #include <Core/SpawningKit/Config.h> #include <Core/SpawningKit/Exceptions.h> #include <Core/SpawningKit/Handshake/BackgroundIOCapturer.h> #include <Core/SpawningKit/Handshake/Session.h> namespace Passenger { namespace SpawningKit { using namespace std; using namespace oxt; /** * For an introduction see README.md, section * "The handshake and the HandshakePerform class". */ class HandshakePerform { private: enum FinishState { // The app hasn't finished spawning yet. NOT_FINISHED, // The app has successfully finished spawning. FINISH_SUCCESS, // The app has finished spawning with an error. FINISH_ERROR, // An internal error occurred in watchFinishSignal(). FINISH_INTERNAL_ERROR }; HandshakeSession &session; Config * const config; const pid_t pid; const FileDescriptor stdinFd; const FileDescriptor stdoutAndErrFd; const string alreadyReadStdoutAndErrData; /** * These objects captures the process's stdout and stderr while handshake is * in progress. If handshaking fails, then any output captured by these objects * will be stored into the resulting SpawnException's error page. */ BackgroundIOCapturerPtr stdoutAndErrCapturer; boost::mutex syncher; boost::condition_variable cond; oxt::thread *processExitWatcher; oxt::thread *finishSignalWatcher; bool processExited; FinishState finishState; string finishSignalWatcherErrorMessage; ErrorCategory finishSignalWatcherErrorCategory; oxt::thread *socketPingabilityWatcher; bool socketIsNowPingable; void initializeStdchannelsCapturing() { if (stdoutAndErrFd != -1) { stdoutAndErrCapturer = boost::make_shared<BackgroundIOCapturer>( stdoutAndErrFd, pid, "output", alreadyReadStdoutAndErrData); stdoutAndErrCapturer->setEndReachedCallback(boost::bind( &HandshakePerform::wakeupEventLoop, this)); stdoutAndErrCapturer->start(); } } void startWatchingProcessExit() { processExitWatcher = new oxt::thread( boost::bind(&HandshakePerform::watchProcessExit, this), "SpawningKit: process exit watcher", 64 * 1024); } void watchProcessExit() { TRACE_POINT(); int ret = syscalls::waitpid(pid, NULL, 0); if (ret >= 0 || errno == EPERM) { boost::lock_guard<boost::mutex> l(syncher); processExited = true; wakeupEventLoop(); } } void startWatchingFinishSignal() { finishSignalWatcher = new oxt::thread( boost::bind(&HandshakePerform::watchFinishSignal, this), "SpawningKit: finish signal watcher", 64 * 1024); } void watchFinishSignal() { TRACE_POINT(); try { string path = session.responseDir + "/finish"; int fd = syscalls::openat(session.responseDirFd, "finish", O_RDONLY | O_NOFOLLOW); if (fd == -1) { int e = errno; throw FileSystemException("Error opening FIFO " + path, e, path); } FdGuard guard(fd, __FILE__, __LINE__); char buf = '0'; ssize_t ret = syscalls::read(fd, &buf, 1); if (ret == -1) { int e = errno; throw FileSystemException("Error reading from FIFO " + path, e, path); } guard.runNow(); boost::lock_guard<boost::mutex> l(syncher); if (buf == '1') { finishState = FINISH_SUCCESS; } else { finishState = FINISH_ERROR; } wakeupEventLoop(); } catch (const std::exception &e) { boost::lock_guard<boost::mutex> l(syncher); finishState = FINISH_INTERNAL_ERROR; finishSignalWatcherErrorMessage = e.what(); finishSignalWatcherErrorCategory = inferErrorCategoryFromAnotherException(e, SPAWNING_KIT_HANDSHAKE_PERFORM); wakeupEventLoop(); } } void startWatchingSocketPingability() { socketPingabilityWatcher = new oxt::thread( boost::bind(&HandshakePerform::watchSocketPingability, this), "SpawningKit: socket pingability watcher", 64 * 1024); } void watchSocketPingability() { TRACE_POINT(); while (true) { unsigned long long timeout = 100000; if (pingTcpServer("127.0.0.1", session.expectedStartPort, &timeout)) { boost::lock_guard<boost::mutex> l(syncher); socketIsNowPingable = true; finishState = FINISH_SUCCESS; wakeupEventLoop(); break; } else { syscalls::usleep(50000); } } } void waitUntilSpawningFinished(boost::unique_lock<boost::mutex> &l) { TRACE_POINT(); bool done; do { boost::this_thread::interruption_point(); done = checkCurrentState(); if (!done) { MonotonicTimeUsec begin = SystemTime::getMonotonicUsec(); cond.timed_wait(l, posix_time::microseconds(session.timeoutUsec)); MonotonicTimeUsec end = SystemTime::getMonotonicUsec(); if (end - begin > session.timeoutUsec) { session.timeoutUsec = 0; } else { session.timeoutUsec -= end - begin; } } } while (!done); } bool checkCurrentState() { TRACE_POINT(); if ((stdoutAndErrCapturer != NULL && stdoutAndErrCapturer->isStopped()) || processExited) { UPDATE_TRACE_POINT(); sleepShortlyToCaptureMoreStdoutStderr(); loadJourneyStateFromResponseDir(); if (session.journey.getFirstFailedStep() == UNKNOWN_JOURNEY_STEP) { session.journey.setStepErrored(bestGuessSubprocessFailedStep(), true); } SpawnException e( inferErrorCategoryFromResponseDir(INTERNAL_ERROR), session.journey, config); e.setSummary("The application process exited prematurely."); e.setSubprocessPid(pid); e.setStdoutAndErrData(getStdoutErrData()); loadSubprocessErrorMessagesAndEnvDump(e); throw e.finalize(); } if (session.timeoutUsec == 0) { UPDATE_TRACE_POINT(); sleepShortlyToCaptureMoreStdoutStderr(); loadJourneyStateFromResponseDir(); session.journey.setStepErrored(SPAWNING_KIT_HANDSHAKE_PERFORM); SpawnException e(TIMEOUT_ERROR, session.journey, config); e.setSubprocessPid(pid); e.setStdoutAndErrData(getStdoutErrData()); loadSubprocessErrorMessagesAndEnvDump(e); throw e.finalize(); } return (config->genericApp && socketIsNowPingable) || (!config->genericApp && finishState != NOT_FINISHED); } Result handleResponse() { TRACE_POINT(); switch (finishState) { case FINISH_SUCCESS: return handleSuccessResponse(); case FINISH_ERROR: handleErrorResponse(); return Result(); // Never reached, shut up compiler warning. case FINISH_INTERNAL_ERROR: handleInternalError(); return Result(); // Never reached, shut up compiler warning. default: P_BUG("Unknown finishState " + toString((int) finishState)); return Result(); // Never reached, shut up compiler warning. } } Result handleSuccessResponse() { TRACE_POINT(); Result &result = session.result; vector<StaticString> internalFieldErrors, appSuppliedFieldErrors; result.pid = pid; result.stdinFd = stdinFd; result.stdoutAndErrFd = stdoutAndErrFd; result.spawnEndTime = SystemTime::getUsec(); result.spawnEndTimeMonotonic = SystemTime::getMonotonicUsec(); setResultType(result); if (socketIsNowPingable) { assert(config->genericApp || config->findFreePort); result.sockets.push_back(Result::Socket()); Result::Socket &socket = result.sockets.back(); socket.address = "tcp://127.0.0.1:" + toString(session.expectedStartPort); socket.protocol = "http"; socket.concurrency = -1; socket.acceptHttpRequests = true; } UPDATE_TRACE_POINT(); if (fileExists(session.responseDir + "/properties.json")) { loadResultPropertiesFromResponseDir(!socketIsNowPingable); UPDATE_TRACE_POINT(); if (session.journey.getType() == START_PRELOADER && !resultHasSocketWithPreloaderProtocol()) { throwSpawnExceptionBecauseAppDidNotProvidePreloaderProtocolSockets(); } else if (session.journey.getType() != START_PRELOADER && !resultHasSocketThatAcceptsHttpRequests()) { throwSpawnExceptionBecauseAppDidNotProvideSocketsThatAcceptRequests(); } } UPDATE_TRACE_POINT(); if (result.validate(internalFieldErrors, appSuppliedFieldErrors)) { return result; } else { throwSpawnExceptionBecauseOfResultValidationErrors(internalFieldErrors, appSuppliedFieldErrors); abort(); // never reached, shut up compiler warning } } void handleErrorResponse() { TRACE_POINT(); sleepShortlyToCaptureMoreStdoutStderr(); loadJourneyStateFromResponseDir(); if (session.journey.getFirstFailedStep() == UNKNOWN_JOURNEY_STEP) { session.journey.setStepErrored(bestGuessSubprocessFailedStep(), true); } SpawnException e( inferErrorCategoryFromResponseDir(INTERNAL_ERROR), session.journey, config); e.setSummary("The web application aborted with an error during startup."); e.setSubprocessPid(pid); e.setStdoutAndErrData(getStdoutErrData()); loadSubprocessErrorMessagesAndEnvDump(e); throw e.finalize(); } void handleInternalError() { TRACE_POINT(); sleepShortlyToCaptureMoreStdoutStderr(); loadJourneyStateFromResponseDir(); session.journey.setStepErrored(SPAWNING_KIT_HANDSHAKE_PERFORM); SpawnException e( finishSignalWatcherErrorCategory, session.journey, config); e.setSummary("An internal error occurred while spawning an application process: " + finishSignalWatcherErrorMessage); e.setAdvancedProblemDetails(finishSignalWatcherErrorMessage); e.setSubprocessPid(pid); e.setStdoutAndErrData(getStdoutErrData()); throw e.finalize(); } void loadResultPropertiesFromResponseDir(bool socketsRequired) { TRACE_POINT(); Result &result = session.result; string path = session.responseDir + "/properties.json"; Json::Reader reader; Json::Value doc; vector<string> errors; // We already checked whether properties.json exists before invoking // this method, so if safeReadFile() fails then we can't be sure that // it's an application problem. This is why we want the SystemException // to propagate to higher layers so that there it can be turned into // a generic filesystem-related or IO-related SpawnException, as opposed // to one about this problem specifically. UPDATE_TRACE_POINT(); pair<string, bool> jsonContent = safeReadFile(session.responseDirFd, "properties.json", SPAWNINGKIT_MAX_PROPERTIES_JSON_SIZE); if (!jsonContent.second) { errors.push_back("Error parsing " + path + ": file bigger than " + toString(SPAWNINGKIT_MAX_PROPERTIES_JSON_SIZE) + " bytes"); throwSpawnExceptionBecauseOfResultValidationErrors(vector<string>(), errors); } if (!reader.parse(jsonContent.first, doc)) { errors.push_back("Error parsing " + path + ": " + reader.getFormattedErrorMessages()); throwSpawnExceptionBecauseOfResultValidationErrors(vector<string>(), errors); } UPDATE_TRACE_POINT(); validateResultPropertiesFile(doc, socketsRequired, errors); if (!errors.empty()) { errors.insert(errors.begin(), "The following errors were detected in " + path + ":"); throwSpawnExceptionBecauseOfResultValidationErrors(vector<string>(), errors); } if (!socketsRequired && (!doc.isMember("sockets") || doc["sockets"].empty())) { return; } UPDATE_TRACE_POINT(); Json::Value::iterator it, end = doc["sockets"].end(); for (it = doc["sockets"].begin(); it != end; it++) { const Json::Value &socketDoc = *it; result.sockets.push_back(Result::Socket()); Result::Socket &socket = result.sockets.back(); socket.address = socketDoc["address"].asString(); socket.protocol = socketDoc["protocol"].asString(); socket.concurrency = socketDoc["concurrency"].asInt(); if (socketDoc.isMember("accept_http_requests")) { socket.acceptHttpRequests = socketDoc["accept_http_requests"].asBool(); } if (socketDoc.isMember("description")) { socket.description = socketDoc["description"].asString(); } } } void validateResultPropertiesFile(const Json::Value &doc, bool socketsRequired, vector<string> &errors) const { TRACE_POINT(); if (!doc.isMember("sockets")) { if (socketsRequired) { errors.push_back("'sockets' must be specified"); } return; } if (!doc["sockets"].isArray()) { errors.push_back("'sockets' must be an array"); return; } if (socketsRequired && doc["sockets"].empty()) { errors.push_back("'sockets' must be non-empty"); return; } UPDATE_TRACE_POINT(); Json::Value::const_iterator it, end = doc["sockets"].end(); for (it = doc["sockets"].begin(); it != end; it++) { const Json::Value &socketDoc = *it; if (!socketDoc.isObject()) { errors.push_back("'sockets[" + toString(it.index()) + "]' must be an object"); continue; } validateResultPropertiesFileSocketField(socketDoc, "address", Json::stringValue, it.index(), true, true, errors); validateResultPropertiesFileSocketField(socketDoc, "protocol", Json::stringValue, it.index(), true, true, errors); validateResultPropertiesFileSocketField(socketDoc, "description", Json::stringValue, it.index(), false, true, errors); validateResultPropertiesFileSocketField(socketDoc, "concurrency", Json::intValue, it.index(), true, false, errors); validateResultPropertiesFileSocketField(socketDoc, "accept_http_requests", Json::booleanValue, it.index(), false, false, errors); validateResultPropertiesFileSocketAddress(socketDoc, it.index(), errors); } } void validateResultPropertiesFileSocketField(const Json::Value &doc, const char *key, Json::ValueType type, unsigned int index, bool required, bool requireNonEmpty, vector<string> &errors) const { if (!doc.isMember(key)) { if (required) { errors.push_back("'sockets[" + toString(index) + "]." + key + "' must be specified"); } } else if (doc[key].type() != type) { const char *typeDesc; switch (type) { case Json::stringValue: typeDesc = "a string"; break; case Json::intValue: typeDesc = "an integer"; break; case Json::booleanValue: typeDesc = "a boolean"; break; default: typeDesc = "(unknown type)"; break; } errors.push_back("'sockets[" + toString(index) + "]." + key + "' must be " + typeDesc); } else if (requireNonEmpty && doc[key].asString().empty()) { errors.push_back("'sockets[" + toString(index) + "]." + key + "' must be non-empty"); } } void validateResultPropertiesFileSocketAddress(const Json::Value &doc, unsigned int index, vector<string> &errors) const { TRACE_POINT(); if (!doc["address"].isString() || getSocketAddressType(doc["address"].asString()) != SAT_UNIX) { return; } string filename = parseUnixSocketAddress(doc["address"].asString()); if (filename.empty()) { errors.push_back("'sockets[" + toString(index) + "].address' contains an empty Unix domain socket filename"); return; } if (filename[0] != '/') { errors.push_back("'sockets[" + toString(index) + "].address' when referring to a Unix domain socket, must be" " an absolute path (given path: " + filename + ")"); return; } // If any of the parent directories is writable by a normal user // (Joe) that is not the app's user (Jane), then Joe can swap that // directory with something else, with contents controlled by Joe. // That way, Joe can cause Passenger to connect to (and forward // Jane's traffic to) a process that does not actually belong to // Jane. // // To mitigate this risk, we insist that the socket be placed in a // directory that we know is safe (instanceDir + "/apps.s"). // We don't rely on isPathProbablySecureForRootUse() because that // function cannot be 100% sure that it is correct. UPDATE_TRACE_POINT(); // instanceDir is only empty in tests if (!session.context->instanceDir.empty()) { StaticString actualDir = extractDirNameStatic(filename); string expectedDir = session.context->instanceDir + "/apps.s"; if (actualDir != expectedDir) { errors.push_back("'sockets[" + toString(index) + "].address', when referring to a Unix domain socket," " must be an absolute path to a file in '" + expectedDir + "' (given path: " + filename + ")"); return; } } UPDATE_TRACE_POINT(); struct stat s; int ret; do { ret = lstat(filename.c_str(), &s); } while (ret == -1 && errno == EAGAIN); if (ret == -1) { int e = errno; if (e == EEXIST) { errors.push_back("'sockets[" + toString(index) + "].address' refers to a non-existant Unix domain" " socket file (given path: " + filename + ")"); return; } else { throw FileSystemException("Cannot stat " + filename, e, filename); } } // We only check the UID, not the GID, because the socket // may be automatically made with a different GID than // the creating process's due to the setgid bit being set // the directory that contains the socket. Furthermore, // on macOS it seems that all directories behave as if // they have the setgid bit set. UPDATE_TRACE_POINT(); if (s.st_uid != session.uid) { errors.push_back("'sockets[" + toString(index) + "].address', when referring to a Unix domain socket file," " must be owned by user " + lookupSystemUsernameByUid(session.uid) + " (actual owner: " + lookupSystemUsernameByUid(s.st_uid) + ")"); } } bool resultHasSocketWithPreloaderProtocol() const { const vector<Result::Socket> &sockets = session.result.sockets; vector<Result::Socket>::const_iterator it, end = sockets.end(); for (it = sockets.begin(); it != end; it++) { if (it->protocol == "preloader") { return true; } } return false; } bool resultHasSocketThatAcceptsHttpRequests() const { const vector<Result::Socket> &sockets = session.result.sockets; vector<Result::Socket>::const_iterator it, end = sockets.end(); for (it = sockets.begin(); it != end; it++) { if (it->acceptHttpRequests) { return true; } } return false; } void wakeupEventLoop() { cond.notify_all(); } string getStdoutErrData() const { return getStdoutErrData(stdoutAndErrCapturer); } static string getStdoutErrData(const BackgroundIOCapturerPtr &stdoutAndErrCapturer) { if (stdoutAndErrCapturer != NULL) { return stdoutAndErrCapturer->getData(); } else { return "(not available)"; } } void sleepShortlyToCaptureMoreStdoutStderr() const { syscalls::usleep(50000); } void throwSpawnExceptionBecauseAppDidNotProvidePreloaderProtocolSockets() { TRACE_POINT(); assert(!config->genericApp); sleepShortlyToCaptureMoreStdoutStderr(); if (!config->genericApp && config->startsUsingWrapper) { UPDATE_TRACE_POINT(); loadJourneyStateFromResponseDir(); session.journey.setStepErrored(SUBPROCESS_WRAPPER_PREPARATION, true); SpawnException e(INTERNAL_ERROR, session.journey, config); e.setSubprocessPid(pid); e.setStdoutAndErrData(getStdoutErrData()); loadBasicInfoFromEnvDumpDir(e); loadAnnotationsFromEnvDumpDir(e); if (config->wrapperSuppliedByThirdParty) { e.setSummary("Error spawning the web application:" " a third-party application wrapper did not" " report any sockets to receive preloader commands on."); } else { e.setSummary("Error spawning the web application:" " a " SHORT_PROGRAM_NAME "-internal application" " wrapper did not report any sockets to receive" " preloader commands on."); } if (config->wrapperSuppliedByThirdParty) { e.setProblemDescriptionHTML( "<p>The " PROGRAM_NAME " application server tried" " to start the web application through a helper tool " " called the \"wrapper\". This helper tool is not part of " SHORT_PROGRAM_NAME ". " SHORT_PROGRAM_NAME " expected" " the helper tool to report a socket to receive preloader" " commands on, but the helper tool finished its startup" " procedure without reporting such a socket.</p>"); } else { e.setProblemDescriptionHTML( "<p>The " PROGRAM_NAME " application server tried" " to start the web application through a " SHORT_PROGRAM_NAME "-internal helper tool called the \"wrapper\"," " but " SHORT_PROGRAM_NAME " encountered a bug" " in this helper tool. " SHORT_PROGRAM_NAME " expected" " the helper tool to report a socket to receive preloader" " commands on, but the helper tool finished its startup" " procedure without reporting such a socket.</p>"); } if (config->wrapperSuppliedByThirdParty) { e.setSolutionDescriptionHTML( "<p class=\"sole-solution\">" "This is a bug in the wrapper, so please contact the author of" " the wrapper. This problem is outside " SHORT_PROGRAM_NAME "'s control. Below follows the command that " SHORT_PROGRAM_NAME " tried to execute, so that you can infer" " which wrapper was used:</p>" "<pre>" + escapeHTML(config->startCommand) + "</pre>"); } else { e.setSolutionDescriptionHTML( "<p class=\"sole-solution\">" "This is a bug in " SHORT_PROGRAM_NAME "." " <a href=\"" SUPPORT_URL "\">Please report this bug</a>" " to the " SHORT_PROGRAM_NAME " authors.</p>"); } throw e.finalize(); } else { UPDATE_TRACE_POINT(); loadJourneyStateFromResponseDir(); session.journey.setStepErrored(SUBPROCESS_APP_LOAD_OR_EXEC, true); SpawnException e(INTERNAL_ERROR, session.journey, config); e.setSubprocessPid(pid); e.setStdoutAndErrData(getStdoutErrData()); loadBasicInfoFromEnvDumpDir(e); loadAnnotationsFromEnvDumpDir(e); e.setSummary("Error spawning the web application: the application" " did not report any sockets to receive preloader commands on."); e.setProblemDescriptionHTML( "<p>The " PROGRAM_NAME " application server tried" " to start the web application, but encountered a bug" " in the application. " SHORT_PROGRAM_NAME " expected" " the application to report a socket to receive preloader" " commands on, but the application finished its startup" " procedure without reporting such a socket.</p>"); e.setSolutionDescriptionHTML( "<p class=\"sole-solution\">" "Since this is a bug in the web application, please " "report this problem to the application's developer. " "This problem is outside " SHORT_PROGRAM_NAME "'s " "control.</p>"); throw e.finalize(); } } void throwSpawnExceptionBecauseAppDidNotProvideSocketsThatAcceptRequests() { TRACE_POINT(); assert(!config->genericApp); sleepShortlyToCaptureMoreStdoutStderr(); if (!config->genericApp && config->startsUsingWrapper) { UPDATE_TRACE_POINT(); loadJourneyStateFromResponseDir(); switch (session.journey.getType()) { case SPAWN_DIRECTLY: case START_PRELOADER: session.journey.setStepErrored(SUBPROCESS_WRAPPER_PREPARATION, true); break; case SPAWN_THROUGH_PRELOADER: session.journey.setStepErrored(SUBPROCESS_PREPARE_AFTER_FORKING_FROM_PRELOADER, true); break; default: P_BUG("Unknown journey type " << (int) session.journey.getType()); } SpawnException e(INTERNAL_ERROR, session.journey, config); e.setSubprocessPid(pid); e.setStdoutAndErrData(getStdoutErrData()); loadBasicInfoFromEnvDumpDir(e); loadAnnotationsFromEnvDumpDir(e); if (config->wrapperSuppliedByThirdParty) { e.setSummary("Error spawning the web application:" " a third-party application wrapper did not" " report any sockets to receive requests on."); } else { e.setSummary("Error spawning the web application:" " a " SHORT_PROGRAM_NAME "-internal application" " wrapper did not report any sockets to receive" " requests on."); } if (config->wrapperSuppliedByThirdParty) { e.setProblemDescriptionHTML( "<p>The " PROGRAM_NAME " application server tried" " to start the web application through a helper tool" " called the \"wrapper\". This helper tool is not part of " SHORT_PROGRAM_NAME ". " SHORT_PROGRAM_NAME " expected" " the helper tool to report a socket to receive requests" " on, but the helper tool finished its startup procedure" " without reporting such a socket.</p>"); } else { e.setProblemDescriptionHTML( "<p>The " PROGRAM_NAME " application server tried" " to start the web application through a " SHORT_PROGRAM_NAME "-internal helper tool called the \"wrapper\"," " but " SHORT_PROGRAM_NAME " encountered a bug" " in this helper tool. " SHORT_PROGRAM_NAME " expected" " the helper tool to report a socket to receive requests" " on, but the helper tool finished its startup procedure" " without reporting such a socket.</p>"); } if (config->wrapperSuppliedByThirdParty) { e.setSolutionDescriptionHTML( "<p class=\"sole-solution\">" "This is a bug in the wrapper, so please contact the author of" " the wrapper. This problem is outside " SHORT_PROGRAM_NAME "'s control. Below follows the command that " SHORT_PROGRAM_NAME " tried to execute, so that you can infer" " which wrapper was used:</p>" "<pre>" + escapeHTML(config->startCommand) + "</pre>"); } else { e.setSolutionDescriptionHTML( "<p class=\"sole-solution\">" "This is a bug in " SHORT_PROGRAM_NAME "." " <a href=\"" SUPPORT_URL "\">Please report this bug</a>" " to the " SHORT_PROGRAM_NAME " authors.</p>"); } throw e.finalize(); } else { UPDATE_TRACE_POINT(); loadJourneyStateFromResponseDir(); switch (session.journey.getType()) { case SPAWN_DIRECTLY: case START_PRELOADER: session.journey.setStepErrored(SUBPROCESS_APP_LOAD_OR_EXEC, true); break; case SPAWN_THROUGH_PRELOADER: session.journey.setStepErrored(SUBPROCESS_PREPARE_AFTER_FORKING_FROM_PRELOADER, true); break; default: P_BUG("Unknown journey type " << (int) session.journey.getType()); } SpawnException e(INTERNAL_ERROR, session.journey, config); e.setSubprocessPid(pid); e.setStdoutAndErrData(getStdoutErrData()); loadBasicInfoFromEnvDumpDir(e); loadAnnotationsFromEnvDumpDir(e); e.setSummary("Error spawning the web application: the application" " did not report any sockets to receive requests on."); e.setProblemDescriptionHTML( "<p>The " PROGRAM_NAME " application server tried" " to start the web application, but encountered a bug" " in the application. " SHORT_PROGRAM_NAME " expected" " the application to report a socket to receive requests" " on, but the application finished its startup procedure" " without reporting such a socket.</p>"); e.setSolutionDescriptionHTML( "<p class=\"sole-solution\">" "Since this is a bug in the web application, please " "report this problem to the application's developer. " "This problem is outside " SHORT_PROGRAM_NAME "'s " "control.</p>"); throw e.finalize(); } } template<typename StringType> void throwSpawnExceptionBecauseOfResultValidationErrors( const vector<StringType> &internalFieldErrors, const vector<StringType> &appSuppliedFieldErrors) { TRACE_POINT(); string message; typename vector<StringType>::const_iterator it, end; sleepShortlyToCaptureMoreStdoutStderr(); if (!internalFieldErrors.empty()) { UPDATE_TRACE_POINT(); loadJourneyStateFromResponseDir(); session.journey.setStepErrored(SPAWNING_KIT_HANDSHAKE_PERFORM, true); SpawnException e(INTERNAL_ERROR, session.journey, config); e.setSubprocessPid(pid); e.setStdoutAndErrData(getStdoutErrData()); e.setAdvancedProblemDetails(toString(internalFieldErrors)); loadBasicInfoFromEnvDumpDir(e); loadAnnotationsFromEnvDumpDir(e); e.setSummary("Error spawning the web application:" " a bug in " SHORT_PROGRAM_NAME " caused the" " spawn result to be invalid: " + toString(internalFieldErrors)); message = "<p>The " PROGRAM_NAME " application server tried" " to start the web application, but encountered a bug" " in " SHORT_PROGRAM_NAME " itself. The errors are as" " follows:</p>" "<ul>"; end = internalFieldErrors.end(); for (it = internalFieldErrors.begin(); it != end; it++) { message.append("<li>" + escapeHTML(*it) + "</li>"); } message.append("</ul>"); e.setProblemDescriptionHTML(message); e.setSolutionDescriptionHTML( "<p class=\"sole-solution\">" "This is a bug in " SHORT_PROGRAM_NAME "." " <a href=\"" SUPPORT_URL "\">Please report this bug</a>" " to the " SHORT_PROGRAM_NAME " authors.</p>"); throw e.finalize(); } else if (!config->genericApp && config->startsUsingWrapper) { UPDATE_TRACE_POINT(); loadJourneyStateFromResponseDir(); switch (session.journey.getType()) { case SPAWN_DIRECTLY: case START_PRELOADER: session.journey.setStepErrored(SUBPROCESS_WRAPPER_PREPARATION, true); break; case SPAWN_THROUGH_PRELOADER: session.journey.setStepErrored(SUBPROCESS_PREPARE_AFTER_FORKING_FROM_PRELOADER, true); break; default: P_BUG("Unknown journey type " << (int) session.journey.getType()); } SpawnException e(INTERNAL_ERROR, session.journey, config); e.setSubprocessPid(pid); e.setStdoutAndErrData(getStdoutErrData()); e.setAdvancedProblemDetails(toString(appSuppliedFieldErrors)); loadBasicInfoFromEnvDumpDir(e); loadAnnotationsFromEnvDumpDir(e); if (config->wrapperSuppliedByThirdParty) { e.setSummary("Error spawning the web application:" " a bug in a third-party application wrapper caused" " the spawn result to be invalid: " + toString(appSuppliedFieldErrors)); } else { e.setSummary("Error spawning the web application:" " a bug in a " SHORT_PROGRAM_NAME "-internal" " application wrapper caused the" " spawn result to be invalid: " + toString(appSuppliedFieldErrors)); } if (config->wrapperSuppliedByThirdParty) { message = "<p>The " PROGRAM_NAME " application server tried" " to start the web application through a helper tool" " called the \"wrapper\". This helper tool is not part of " SHORT_PROGRAM_NAME ". " SHORT_PROGRAM_NAME " expected" " the helper tool to communicate back various information" " about the application's startup procedure, but the tool" " did not communicate back correctly." " The errors are as follows:</p>" "<ul>"; } else { message = "<p>The " PROGRAM_NAME " application server tried" " to start the web application through a " SHORT_PROGRAM_NAME "-internal helper tool (called the \"wrapper\")," " but " SHORT_PROGRAM_NAME " encountered a bug" " in this helper tool. " SHORT_PROGRAM_NAME " expected" " the helper tool to communicate back various information" " about the application's startup procedure, but the tool" " did not communicate back correctly." " The errors are as follows:</p>" "<ul>"; } end = appSuppliedFieldErrors.end(); for (it = appSuppliedFieldErrors.begin(); it != end; it++) { message.append("<li>" + escapeHTML(*it) + "</li>"); } message.append("</ul>"); e.setProblemDescriptionHTML(message); if (config->wrapperSuppliedByThirdParty) { e.setSolutionDescriptionHTML( "<p class=\"sole-solution\">" "This is a bug in the wrapper, so please contact the author of" " the wrapper. This problem is outside " SHORT_PROGRAM_NAME "'s control. Below follows the command that " SHORT_PROGRAM_NAME " tried to execute, so that you can infer" " which wrapper was used:</p>" "<pre>" + escapeHTML(config->startCommand) + "</pre>"); } else { e.setSolutionDescriptionHTML( "<p class=\"sole-solution\">" "This is a bug in " SHORT_PROGRAM_NAME "." " <a href=\"" SUPPORT_URL "\">Please report this bug</a>" " to the " SHORT_PROGRAM_NAME " authors.</p>"); } throw e.finalize(); } else { UPDATE_TRACE_POINT(); loadJourneyStateFromResponseDir(); session.journey.setStepErrored(SUBPROCESS_APP_LOAD_OR_EXEC, true); SpawnException e(INTERNAL_ERROR, session.journey, config); e.setSummary("Error spawning the web application:" " the application's spawn response is invalid: " + toString(appSuppliedFieldErrors)); e.setAdvancedProblemDetails(toString(appSuppliedFieldErrors)); e.setSubprocessPid(pid); e.setStdoutAndErrData(getStdoutErrData()); loadBasicInfoFromEnvDumpDir(e); loadAnnotationsFromEnvDumpDir(e); message = "<p>The " PROGRAM_NAME " application server tried" " to start the web application, but encountered a bug" " in the application. " SHORT_PROGRAM_NAME " expected" " the application to communicate back various information" " about its startup procedure, but the application" " did not communicate back that correctly." " The errors are as follows:</p>" "<ul>"; end = appSuppliedFieldErrors.end(); for (it = appSuppliedFieldErrors.begin(); it != end; it++) { message.append("<li>" + escapeHTML(*it) + "</li>"); } message.append("</ul>"); e.setProblemDescriptionHTML(message); if (config->genericApp) { e.setSolutionDescriptionHTML( "<p class=\"sole-solution\">" "Since this is a bug in the web application, please " "report this problem to the application's developer. " "This problem is outside " SHORT_PROGRAM_NAME "'s " "control.</p>"); } else { e.setSolutionDescriptionHTML( "<p class=\"sole-solution\">" "This is a bug in " SHORT_PROGRAM_NAME "." " <a href=\"" SUPPORT_URL "\">Please report this bug</a>" " to the " SHORT_PROGRAM_NAME " authors.</p>"); } throw e.finalize(); } } ErrorCategory inferErrorCategoryFromResponseDir(ErrorCategory defaultValue) const { TRACE_POINT(); if (fileExists(session.responseDir + "/error/category")) { string value = strip(safeReadFile(session.responseErrorDirFd, "category", SPAWNINGKIT_MAX_ERROR_CATEGORY_SIZE).first); ErrorCategory category = stringToErrorCategory(value); if (category == UNKNOWN_ERROR_CATEGORY) { SpawnException e(INTERNAL_ERROR, session.journey, config); e.setStdoutAndErrData(getStdoutErrData()); e.setSubprocessPid(pid); loadBasicInfoFromEnvDumpDir(e); loadAnnotationsFromEnvDumpDir(e); if (!config->genericApp && config->startsUsingWrapper) { if (config->wrapperSuppliedByThirdParty) { e.setSummary( "An error occurred while spawning an application process: " "the application wrapper (which is not part of " SHORT_PROGRAM_NAME ") reported an invalid error category: " + value); } else { e.setSummary( "An error occurred while spawning an application process: " "the application wrapper (which is internal to " SHORT_PROGRAM_NAME ") reported an invalid error category: " + value); } } else { e.setSummary( "An error occurred while spawning an application process: " "the application reported an invalid error category: " + value); } if (!config->genericApp && config->startsUsingWrapper) { if (config->wrapperSuppliedByThirdParty) { e.setProblemDescriptionHTML( "<p>The " PROGRAM_NAME " application server tried" " to start the web application through a" " helper tool called the \"wrapper\". This helper tool " " is not part of " SHORT_PROGRAM_NAME ". The tool " " encountered an error, so " SHORT_PROGRAM_NAME " expected the tool to report details about that error." " But the tool communicated back in an invalid format:</p>" "<ul>" "<li>In file: " + escapeHTML(session.responseDir) + "/error/category</li>" "<li>Content: <code>" + escapeHTML(value) + "</code></li>" "</ul>"); e.setSolutionDescriptionHTML( "<p class=\"sole-solution\">" "This is a bug in the wrapper, so please contact the author of" " the wrapper. This problem is outside " SHORT_PROGRAM_NAME "'s control. Below follows the command that " SHORT_PROGRAM_NAME " tried to execute, so that you can infer" " which wrapper was used:</p>" "<pre>" + escapeHTML(config->startCommand) + "</pre>"); } else { e.setProblemDescriptionHTML( "<p>The " PROGRAM_NAME " application server tried" " to start the web application through a " SHORT_PROGRAM_NAME "-internal helper tool called the \"wrapper\"." " The tool encountered an error, so " SHORT_PROGRAM_NAME " expected the tool to report" " details about that error. But the tool communicated back" " in an invalid format:</p>" "<ul>" "<li>In file: " + escapeHTML(session.responseDir) + "/error/category</li>" "<li>Content: <code>" + escapeHTML(value) + "</code></li>" "</ul>"); e.setSolutionDescriptionHTML( "<p class=\"sole-solution\">" "This is a bug in " SHORT_PROGRAM_NAME "." " <a href=\"" SUPPORT_URL "\">Please report this bug</a>" " to the " SHORT_PROGRAM_NAME " authors.</p>"); } } else { e.setProblemDescriptionHTML( "<p>The " PROGRAM_NAME " application server tried" " to start the web application. The application encountered " " an error and tried to report details about the error back to " SHORT_PROGRAM_NAME ". But the application communicated back" " in an invalid format:</p>" "<ul>" "<li>In file: " + escapeHTML(session.responseDir) + "/error/category</li>" "<li>Content: <code>" + escapeHTML(value) + "</code></li>" "</ul>"); e.setSolutionDescriptionHTML( "<p class=\"sole-solution\">" "This is a bug in the web application, please " "report this problem to the application's developer. " "This problem is outside " SHORT_PROGRAM_NAME "'s " "control.</p>"); } throw e.finalize(); } else { return category; } } else { return defaultValue; } } void loadJourneyStateFromResponseDir() { loadJourneyStateFromResponseDir(session, pid, stdoutAndErrCapturer); } static void loadJourneyStateFromResponseDir(HandshakeSession &session, pid_t pid, const BackgroundIOCapturerPtr &stdoutAndErrCapturer, JourneyStep firstStep, JourneyStep lastStep) { TRACE_POINT(); JourneyStep step; for (step = firstStep; step < lastStep; step = JourneyStep((int) step + 1)) { if (!session.journey.hasStep(step)) { continue; } string stepString = journeyStepToStringLowerCase(step); string stepDir = session.responseDir + "/steps/" + stepString; if (!fileExists(stepDir + "/state")) { P_DEBUG("[App " << pid << " journey] Step " << journeyStepToString(step) << ": state file does not exist"); continue; } map<JourneyStep, int>::const_iterator it = session.stepDirFds.find(step); if (it == session.stepDirFds.end()) { P_BUG("No fd opened for step " << stepString); } loadJourneyStateFromResponseDirForSpecificStep( session, pid, stdoutAndErrCapturer, step, stepDir, it->second); } } static void loadJourneyStateFromResponseDirForSpecificStep(HandshakeSession &session, pid_t pid, const BackgroundIOCapturerPtr &stdoutAndErrCapturer, JourneyStep step, const string &stepDir, int stepDirFd) { TRACE_POINT_WITH_DATA(journeyStepToString(step).data()); string summary; string value = strip(safeReadFile(stepDirFd, "state", SPAWNINGKIT_MAX_JOURNEY_STEP_FILE_SIZE).first); JourneyStepState state = stringToJourneyStepState(value); const Config *config = session.config; if (value.empty()) { P_DEBUG("[App " << pid << " journey] Step " << journeyStepToString(step) << ": state file is empty"); return; } P_DEBUG("[App " << pid << " journey] Step " << journeyStepToString(step) << ": setting state to " << value); try { UPDATE_TRACE_POINT(); switch (state) { case STEP_NOT_STARTED: // SpawnEnvSetupper explicitly sets the SUBPROCESS_OS_SHELL // step state to STEP_NOT_STARTED if it determines that it // should not execute the next command through the shell. session.journey.setStepNotStarted(step, true); break; case STEP_IN_PROGRESS: session.journey.setStepInProgress(step, true); break; case STEP_PERFORMED: session.journey.setStepPerformed(step, true); break; case STEP_ERRORED: session.journey.setStepErrored(step, true); break; default: session.journey.setStepErrored(step, true); SpawnException e(INTERNAL_ERROR, session.journey, config); e.setStdoutAndErrData(getStdoutErrData(stdoutAndErrCapturer)); e.setSubprocessPid(pid); loadBasicInfoFromEnvDumpDir(e, session); loadAnnotationsFromEnvDumpDir(e, session); if (!config->genericApp && config->startsUsingWrapper) { if (config->wrapperSuppliedByThirdParty) { e.setSummary( "An error occurred while spawning an application process: " "the application wrapper (which is not part of " SHORT_PROGRAM_NAME ") reported an invalid progress step state for step " + journeyStepToString(step) + ": " + value); } else { e.setSummary( "An error occurred while spawning an application process: " "the application wrapper (which is internal to " SHORT_PROGRAM_NAME ") reported an invalid progress step state for step " + journeyStepToString(step) + ": " + value); } } else { e.setSummary( "An error occurred while spawning an application process: " "the application reported an invalid progress step state for step " + journeyStepToString(step) + ": " + value); } if (!config->genericApp && config->startsUsingWrapper) { if (config->wrapperSuppliedByThirdParty) { e.setProblemDescriptionHTML( "<p>The " PROGRAM_NAME " application server tried" " to start the web application through a" " helper tool called the \"wrapper\". This helper tool" " is not part of " SHORT_PROGRAM_NAME ". " SHORT_PROGRAM_NAME " expected the helper tool to" " report about its startup progress, but the tool" " communicated back an invalid answer:</p>" "<ul>" "<li>In file: " + escapeHTML(stepDir) + "/state</li>" "<li>Content: <code>" + escapeHTML(value) + "</code></li>" "</ul>"); e.setSolutionDescriptionHTML( "<p class=\"sole-solution\">" "This is a bug in the wrapper, so please contact the author of" " the wrapper. This problem is outside " SHORT_PROGRAM_NAME "'s control. Below follows the command that " SHORT_PROGRAM_NAME " tried to execute, so that you can infer" " which wrapper was used:</p>" "<pre>" + escapeHTML(config->startCommand) + "</pre>"); } else { e.setProblemDescriptionHTML( "<p>The " PROGRAM_NAME " application server tried" " to start the web application through a " SHORT_PROGRAM_NAME "-internal helper tool called the \"wrapper\"," " but " SHORT_PROGRAM_NAME " encountered a bug" " in this helper tool. " SHORT_PROGRAM_NAME " expected" " the helper tool to report about its startup progress," " but the tool communicated back an invalid answer:</p>" "<ul>" "<li>In file: " + escapeHTML(stepDir) + "/state</li>" "<li>Content: <code>" + escapeHTML(value) + "</code></li>" "</ul>"); e.setSolutionDescriptionHTML( "<p class=\"sole-solution\">" "This is a bug in " SHORT_PROGRAM_NAME "." " <a href=\"" SUPPORT_URL "\">Please report this bug</a>" " to the " SHORT_PROGRAM_NAME " authors.</p>"); } } else { e.setProblemDescriptionHTML( "<p>The " PROGRAM_NAME " application server tried" " to start the web application, and expected the application" " to report about its startup progress. But the application" " communicated back an invalid answer:</p>" "<ul>" "<li>In file: " + escapeHTML(stepDir) + "/state</li>" "<li>Content: <code>" + escapeHTML(value) + "</code></li>" "</ul>"); e.setSolutionDescriptionHTML( "<p class=\"sole-solution\">" "This is a bug in the web application, please " "report this problem to the application's developer. " "This problem is outside " SHORT_PROGRAM_NAME "'s " "control.</p>"); } throw e.finalize(); break; }; } catch (const RuntimeException &originalException) { UPDATE_TRACE_POINT(); session.journey.setStepErrored(step, true); SpawnException e(INTERNAL_ERROR, session.journey, config); e.setStdoutAndErrData(getStdoutErrData(stdoutAndErrCapturer)); e.setSubprocessPid(pid); loadBasicInfoFromEnvDumpDir(e, session); loadAnnotationsFromEnvDumpDir(e, session); if (!config->genericApp && config->startsUsingWrapper) { if (config->wrapperSuppliedByThirdParty) { e.setSummary("An error occurred while spawning an application process: " "the application wrapper (which is not part of " SHORT_PROGRAM_NAME ") reported an invalid progress step state for step " + journeyStepToString(step) + ": " + StaticString(originalException.what())); } else { e.setSummary("An error occurred while spawning an application process: " "the application wrapper (which is internal to " SHORT_PROGRAM_NAME ") reported an invalid progress step state for step " + journeyStepToString(step) + ": " + StaticString(originalException.what())); } } else { e.setSummary("An error occurred while spawning an application process: " "the application reported an invalid progress step state for step " + journeyStepToString(step) + ": " + StaticString(originalException.what())); } if (!config->genericApp && config->startsUsingWrapper) { if (config->wrapperSuppliedByThirdParty) { e.setProblemDescriptionHTML( "<p>The " PROGRAM_NAME " application server tried" " to start the web application through a " " helper tool called the \"wrapper\". This helper tool" " is not part of " SHORT_PROGRAM_NAME ". " SHORT_PROGRAM_NAME " expected the helper tool to" " report about its startup progress, but the tool" " communicated back an invalid answer:</p>" "<ul>" "<li>In file: " + escapeHTML(stepDir) + "/state</li>" "<li>Error: " + escapeHTML(originalException.what()) + "</li>" "</ul>"); e.setSolutionDescriptionHTML( "<p class=\"sole-solution\">" "This is a bug in the wrapper, so please contact the author of" " the wrapper. This problem is outside " SHORT_PROGRAM_NAME "'s control. Below follows the command that " SHORT_PROGRAM_NAME " tried to execute, so that you can infer" " which wrapper was used:</p>" "<pre>" + escapeHTML(config->startCommand) + "</pre>"); } else { e.setProblemDescriptionHTML( "<p>The " PROGRAM_NAME " application server tried" " to start the web application through a " SHORT_PROGRAM_NAME "-internal helper tool called the \"wrapper\"," " but " SHORT_PROGRAM_NAME " encountered a bug" " in this helper tool. " SHORT_PROGRAM_NAME " expected" " the helper tool to report about its startup progress," " but the tool communicated back an invalid answer:</p>" "<ul>" "<li>In file: " + escapeHTML(stepDir) + "/state</li>" "<li>Error: " + escapeHTML(originalException.what()) + "</li>" "</ul>"); e.setSolutionDescriptionHTML( "<p class=\"sole-solution\">" "This is a bug in " SHORT_PROGRAM_NAME "." " <a href=\"" SUPPORT_URL "\">Please report this bug</a>" " to the " SHORT_PROGRAM_NAME " authors.</p>"); } } else { e.setProblemDescriptionHTML( "<p>The " PROGRAM_NAME " application server tried" " to start the web application, and expected the application" " to report about its startup progress. But the application" " communicated back an invalid answer:</p>" "<ul>" "<li>In file: " + escapeHTML(stepDir) + "/state</li>" "<li>Error: " + escapeHTML(originalException.what()) + "</li>" "</ul>"); e.setSolutionDescriptionHTML( "<p class=\"sole-solution\">" "This is a bug in the web application, please " "report this problem to the application's developer. " "This problem is outside " SHORT_PROGRAM_NAME "'s " "control.</p>"); } throw e.finalize(); } UPDATE_TRACE_POINT(); if (fileExists(stepDir + "/begin_time_monotonic")) { value = safeReadFile(stepDirFd, "begin_time_monotonic", SPAWNINGKIT_MAX_JOURNEY_STEP_FILE_SIZE).first; MonotonicTimeUsec beginTimeMonotonic = atof(value.c_str()) * 1000000; P_DEBUG("[App " << pid << " journey] Step " << journeyStepToString(step) << ": monotonic begin time is \"" << cEscapeString(value) << "\""); session.journey.setStepBeginTime(step, beginTimeMonotonic); } else if (fileExists(stepDir + "/begin_time")) { value = safeReadFile(stepDirFd, "begin_time", SPAWNINGKIT_MAX_JOURNEY_STEP_FILE_SIZE).first; unsigned long long beginTime = atof(value.c_str()) * 1000000; MonotonicTimeUsec beginTimeMonotonic = usecTimestampToMonoTime(beginTime); P_DEBUG("[App " << pid << " journey] Step " << journeyStepToString(step) << ": begin time is \"" << cEscapeString(value) << "\", monotonic conversion is " << doubleToString(beginTimeMonotonic / 1000000.0)); session.journey.setStepBeginTime(step, beginTimeMonotonic); } else { P_DEBUG("[App " << pid << " journey] Step " << journeyStepToString(step) << ": no begin time known"); } UPDATE_TRACE_POINT(); if (fileExists(stepDir + "/end_time_monotonic")) { value = safeReadFile(stepDirFd, "end_time_monotonic", SPAWNINGKIT_MAX_JOURNEY_STEP_FILE_SIZE).first; MonotonicTimeUsec endTimeMonotonic = atof(value.c_str()) * 1000000; P_DEBUG("[App " << pid << " journey] Step " << journeyStepToString(step) << ": monotonic end time is \"" << cEscapeString(value) << "\""); session.journey.setStepEndTime(step, endTimeMonotonic); } else if (fileExists(stepDir + "/end_time")) { value = safeReadFile(stepDirFd, "end_time", SPAWNINGKIT_MAX_JOURNEY_STEP_FILE_SIZE).first; unsigned long long endTime = atof(value.c_str()) * 1000000; MonotonicTimeUsec endTimeMonotonic = usecTimestampToMonoTime(endTime); P_DEBUG("[App " << pid << " journey] Step " << journeyStepToString(step) << ": end time is \"" << cEscapeString(value) << "\", monotonic conversion is " << doubleToString(endTimeMonotonic / 1000000.0)); session.journey.setStepEndTime(step, endTimeMonotonic); } else { P_DEBUG("[App " << pid << " journey] Step " << journeyStepToString(step) << ": no end time known"); } } static MonotonicTimeUsec usecTimestampToMonoTime(unsigned long long timestamp) { unsigned long long now = SystemTime::getUsec(); MonotonicTimeUsec nowMono = SystemTime::getMonotonicUsec(); unsigned long long diff; if (now > nowMono) { diff = now - nowMono; return timestamp - diff; } else { diff = nowMono - now; return timestamp + diff; } } void loadSubprocessErrorMessagesAndEnvDump(SpawnException &e) const { TRACE_POINT(); const string &responseDir = session.responseDir; if (fileExists(responseDir + "/error/summary")) { e.setSummary(strip(safeReadFile(session.responseErrorDirFd, "summary", SPAWNINGKIT_MAX_SUBPROCESS_ERROR_MESSAGE_SIZE).first)); } if (e.getAdvancedProblemDetails().empty() && fileExists(responseDir + "/error/advanced_problem_details")) { e.setAdvancedProblemDetails(strip(safeReadFile(session.responseErrorDirFd, "advanced_problem_details", SPAWNINGKIT_MAX_SUBPROCESS_ERROR_MESSAGE_SIZE).first)); } if (fileExists(responseDir + "/error/problem_description.html")) { e.setProblemDescriptionHTML(safeReadFile(session.responseErrorDirFd, "problem_description.html", SPAWNINGKIT_MAX_SUBPROCESS_ERROR_MESSAGE_SIZE).first); } else if (fileExists(responseDir + "/error/problem_description.txt")) { e.setProblemDescriptionHTML(escapeHTML(strip(safeReadFile(session.responseErrorDirFd, "problem_description.txt", SPAWNINGKIT_MAX_SUBPROCESS_ERROR_MESSAGE_SIZE).first))); } if (fileExists(responseDir + "/error/solution_description.html")) { e.setSolutionDescriptionHTML(safeReadFile(session.responseErrorDirFd, "solution_description.html", SPAWNINGKIT_MAX_SUBPROCESS_ERROR_MESSAGE_SIZE).first); } else if (fileExists(responseDir + "/error/solution_description.txt")) { e.setSolutionDescriptionHTML(escapeHTML(strip(safeReadFile(session.responseErrorDirFd, "solution_description.txt", SPAWNINGKIT_MAX_SUBPROCESS_ERROR_MESSAGE_SIZE).first))); } loadBasicInfoFromEnvDumpDir(e); loadAnnotationsFromEnvDumpDir(e); } void loadBasicInfoFromEnvDumpDir(SpawnException &e) const { loadBasicInfoFromEnvDumpDir(e, session); } static void loadBasicInfoFromEnvDumpDir(SpawnException &e, HandshakeSession &session) { string envvars, userInfo, ulimits; loadBasicInfoFromEnvDumpDir(session.envDumpDir, session.envDumpDirFd, envvars, userInfo, ulimits); e.setSubprocessEnvvars(envvars); e.setSubprocessUserInfo(userInfo); e.setSubprocessUlimits(ulimits); } static void doClosedir(DIR *dir) { closedir(dir); } void loadAnnotationsFromEnvDumpDir(SpawnException &e) const { loadAnnotationsFromEnvDumpDir(e, session); } static void loadAnnotationsFromEnvDumpDir(SpawnException &e, HandshakeSession &session) { TRACE_POINT(); string path = session.envDumpDir + "/annotations"; DIR *dir = opendir(path.c_str()); if (dir == NULL) { return; } ScopeGuard guard(boost::bind(doClosedir, dir)); struct dirent *ent; while ((ent = readdir(dir)) != NULL) { if (ent->d_name[0] != '.') { e.setAnnotation(ent->d_name, strip( safeReadFile(session.envDumpAnnotationsDirFd, ent->d_name, SPAWNINGKIT_MAX_SUBPROCESS_ENVDUMP_SIZE).first )); } } } void cleanup() { boost::this_thread::disable_interruption di; boost::this_thread::disable_syscall_interruption dsi; TRACE_POINT(); if (processExitWatcher != NULL) { processExitWatcher->interrupt_and_join(); delete processExitWatcher; processExitWatcher = NULL; } if (finishSignalWatcher != NULL) { finishSignalWatcher->interrupt_and_join(); delete finishSignalWatcher; finishSignalWatcher = NULL; } if (socketPingabilityWatcher != NULL) { socketPingabilityWatcher->interrupt_and_join(); delete socketPingabilityWatcher; socketPingabilityWatcher = NULL; } if (stdoutAndErrCapturer != NULL) { stdoutAndErrCapturer->stop(); } } JourneyStep bestGuessSubprocessFailedStep() const { JourneyStep step = getFirstSubprocessJourneyStepWithState(STEP_IN_PROGRESS); if (step != UNKNOWN_JOURNEY_STEP) { return step; } if (allSubprocessJourneyStepsHaveState(STEP_PERFORMED)) { return getLastSubprocessJourneyStepFrom(session.journey); } else { JourneyStep step = getLastSubprocessJourneyStepWithState(STEP_PERFORMED); if (step == UNKNOWN_JOURNEY_STEP) { return getFirstSubprocessJourneyStepFrom(session.journey); } else { assert(step != getLastSubprocessJourneyStepFrom(session.journey)); return JourneyStep((int) step + 1); } } } JourneyStep getFirstSubprocessJourneyStepFrom(const Journey &journey) const { JourneyStep firstStep = getFirstSubprocessJourneyStep(); JourneyStep lastStep = getLastSubprocessJourneyStep(); JourneyStep step; for (step = firstStep; step <= lastStep; step = JourneyStep((int) step + 1)) { if (session.journey.hasStep(step)) { return step; } } P_BUG("Never reached"); return UNKNOWN_JOURNEY_STEP; } JourneyStep getLastSubprocessJourneyStepFrom(const Journey &journey) const { JourneyStep firstStep = getFirstSubprocessJourneyStep(); JourneyStep lastStep = getLastSubprocessJourneyStep(); JourneyStep result = UNKNOWN_JOURNEY_STEP; JourneyStep step; for (step = firstStep; step <= lastStep; step = JourneyStep((int) step + 1)) { if (session.journey.hasStep(step)) { result = step; } } return result; } bool allSubprocessJourneyStepsHaveState(JourneyStepState state) const { JourneyStep firstStep = getFirstSubprocessJourneyStep(); JourneyStep lastStep = getLastSubprocessJourneyStep(); JourneyStep step; for (step = firstStep; step <= lastStep; step = JourneyStep((int) step + 1)) { if (!session.journey.hasStep(step)) { continue; } if (session.journey.getStepInfo(step).state != state) { return false; } } return true; } JourneyStep getFirstSubprocessJourneyStepWithState(JourneyStepState state) const { JourneyStep firstStep = getFirstSubprocessJourneyStep(); JourneyStep lastStep = getLastSubprocessJourneyStep(); JourneyStep step; for (step = firstStep; step <= lastStep; step = JourneyStep((int) step + 1)) { if (!session.journey.hasStep(step)) { continue; } if (session.journey.getStepInfo(step).state == state) { return step; } } return UNKNOWN_JOURNEY_STEP; } JourneyStep getLastSubprocessJourneyStepWithState(JourneyStepState state) const { JourneyStep firstStep = getFirstSubprocessJourneyStep(); JourneyStep lastStep = getLastSubprocessJourneyStep(); JourneyStep step; JourneyStep result = UNKNOWN_JOURNEY_STEP; for (step = firstStep; step <= lastStep; step = JourneyStep((int) step + 1)) { if (!session.journey.hasStep(step)) { continue; } if (session.journey.getStepInfo(step).state == state) { result = step; } } return result; } void setResultType(Result &result) const { if (config->genericApp) { result.type = Result::GENERIC; } else if (config->startsUsingWrapper) { result.type = Result::AUTO_SUPPORTED; } else { result.type = Result::KURIA; } } public: struct DebugSupport { virtual ~DebugSupport() { } virtual void beginWaitUntilSpawningFinished() { } }; DebugSupport *debugSupport; HandshakePerform(HandshakeSession &_session, pid_t _pid, const FileDescriptor &_stdinFd = FileDescriptor(), const FileDescriptor &_stdoutAndErrFd = FileDescriptor(), const string &_alreadyReadStdoutAndErrData = string()) : session(_session), config(session.config), pid(_pid), stdinFd(_stdinFd), stdoutAndErrFd(_stdoutAndErrFd), alreadyReadStdoutAndErrData(_alreadyReadStdoutAndErrData), processExitWatcher(NULL), finishSignalWatcher(NULL), processExited(false), finishState(NOT_FINISHED), socketPingabilityWatcher(NULL), socketIsNowPingable(false), debugSupport(NULL) { assert(_session.context != NULL); assert(_session.context->isFinalized()); assert(_session.config != NULL); } Result execute() { TRACE_POINT(); ScopeGuard guard(boost::bind(&HandshakePerform::cleanup, this)); // We do not set SPAWNING_KIT_HANDSHAKE_PERFORM to the IN_PROGRESS or // PERFORMED state here. That will be done by the caller because // it may want to perform additional preparation. try { initializeStdchannelsCapturing(); startWatchingProcessExit(); if (config->genericApp || config->findFreePort) { startWatchingSocketPingability(); } if (!config->genericApp) { startWatchingFinishSignal(); } } catch (const SpawnException &) { throw; } catch (const std::exception &originalException) { sleepShortlyToCaptureMoreStdoutStderr(); loadJourneyStateFromResponseDir(); session.journey.setStepErrored(SPAWNING_KIT_HANDSHAKE_PERFORM); SpawnException e(originalException, session.journey, config); e.setStdoutAndErrData(getStdoutErrData()); e.setSubprocessPid(pid); throw e.finalize(); } UPDATE_TRACE_POINT(); try { boost::unique_lock<boost::mutex> l(syncher); if (debugSupport != NULL) { debugSupport->beginWaitUntilSpawningFinished(); } waitUntilSpawningFinished(l); Result result = handleResponse(); loadJourneyStateFromResponseDir(); return result; } catch (const SpawnException &) { throw; } catch (const std::exception &originalException) { sleepShortlyToCaptureMoreStdoutStderr(); loadJourneyStateFromResponseDir(); session.journey.setStepErrored(SPAWNING_KIT_HANDSHAKE_PERFORM); SpawnException e(originalException, session.journey, config); e.setSubprocessPid(pid); e.setStdoutAndErrData(getStdoutErrData()); throw e.finalize(); } } static void loadJourneyStateFromResponseDir(HandshakeSession &session, pid_t pid, const BackgroundIOCapturerPtr &stdoutAndErrCapturer) { TRACE_POINT(); P_DEBUG("[App " << pid << " journey] Loading state from " << session.responseDir); loadJourneyStateFromResponseDir(session, pid, stdoutAndErrCapturer, getFirstSubprocessJourneyStep(), getLastSubprocessJourneyStep()); UPDATE_TRACE_POINT(); loadJourneyStateFromResponseDir(session, pid, stdoutAndErrCapturer, getFirstPreloaderJourneyStep(), // Also load state from PRELOADER_FINISH since the // preloader writes there. JourneyStep((int) getLastPreloaderJourneyStep() + 1)); } static void loadBasicInfoFromEnvDumpDir(const string &envDumpDir, int envDumpDirFd, string &envvars, string &userInfo, string &ulimits) { if (fileExists(envDumpDir + "/envvars")) { envvars = safeReadFile(envDumpDirFd, "envvars", SPAWNINGKIT_MAX_SUBPROCESS_ENVDUMP_SIZE).first; } if (fileExists(envDumpDir + "/user_info")) { userInfo = safeReadFile(envDumpDirFd, "user_info", SPAWNINGKIT_MAX_SUBPROCESS_ENVDUMP_SIZE).first; } if (fileExists(envDumpDir + "/ulimits")) { ulimits = safeReadFile(envDumpDirFd, "ulimits", SPAWNINGKIT_MAX_SUBPROCESS_ENVDUMP_SIZE).first; } } }; } // namespace SpawningKit } // namespace Passenger #endif /* _PASSENGER_SPAWNING_KIT_HANDSHAKE_PERFORM_H_ */ SpawningKit/Handshake/BackgroundIOCapturer.h 0000644 00000012511 14756456557 0015106 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2011-2018 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef _PASSENGER_SPAWNING_KIT_BACKGROUND_IO_CAPTURER_H_ #define _PASSENGER_SPAWNING_KIT_BACKGROUND_IO_CAPTURER_H_ #include <boost/thread.hpp> #include <boost/bind/bind.hpp> #include <boost/function.hpp> #include <boost/foreach.hpp> #include <oxt/backtrace.hpp> #include <oxt/system_calls.hpp> #include <string> #include <vector> #include <cstring> #include <sys/types.h> #include <LoggingKit/LoggingKit.h> #include <FileDescriptor.h> #include <StaticString.h> #include <Utils.h> #include <StrIntTools/StrIntUtils.h> namespace Passenger { namespace SpawningKit { using namespace std; /** * Given a file descriptor, captures its output in a background thread * and also forwards it immediately to a target file descriptor. * Call stop() to stop the background thread and to obtain the captured * output so far. */ class BackgroundIOCapturer { private: const FileDescriptor fd; const pid_t pid; const string appGroupName; const string appLogFile; const StaticString channelName; mutable boost::mutex dataSyncher; string data; oxt::thread *thr; boost::function<void ()> endReachedCallback; bool stopped; void capture() { TRACE_POINT(); while (!boost::this_thread::interruption_requested()) { char buf[1024 * 8]; ssize_t ret; UPDATE_TRACE_POINT(); ret = syscalls::read(fd, buf, sizeof(buf)); int e = errno; boost::this_thread::disable_syscall_interruption dsi; if (ret == 0) { break; } else if (ret == -1) { if (e != EAGAIN && e != EWOULDBLOCK) { P_WARN("Background I/O capturer error: " << strerror(e) << " (errno=" << e << ")"); break; } } else { { boost::lock_guard<boost::mutex> l(dataSyncher); data.append(buf, ret); } UPDATE_TRACE_POINT(); if (ret == 1 && buf[0] == '\n') { LoggingKit::logAppOutput(appGroupName, pid, channelName, "", 0, appLogFile); } else { vector<StaticString> lines; if (ret > 0 && buf[ret - 1] == '\n') { ret--; } split(StaticString(buf, ret), '\n', lines); foreach (const StaticString line, lines) { LoggingKit::logAppOutput(appGroupName, pid, channelName, line.data(), line.size(), appLogFile); } } } } { boost::lock_guard<boost::mutex> l(dataSyncher); stopped = true; } if (endReachedCallback) { endReachedCallback(); } } public: BackgroundIOCapturer(const FileDescriptor &_fd, pid_t _pid, const string &_appGroupName, const string &_appLogFile, const StaticString &_channelName = P_STATIC_STRING("output"), const StaticString &_data = StaticString()) : fd(_fd), pid(_pid), appGroupName(_appGroupName), appLogFile(_appLogFile), channelName(_channelName), data(_data.data(), _data.size()), thr(NULL), stopped(false) { } ~BackgroundIOCapturer() { TRACE_POINT(); if (thr != NULL) { boost::this_thread::disable_interruption di; boost::this_thread::disable_syscall_interruption dsi; thr->interrupt_and_join(); delete thr; thr = NULL; } } const FileDescriptor &getFd() const { return fd; } void start() { assert(thr == NULL); thr = new oxt::thread(boost::bind(&BackgroundIOCapturer::capture, this), "Background I/O capturer", 64 * 1024); } void stop() { TRACE_POINT(); if (thr != NULL) { boost::this_thread::disable_interruption di; boost::this_thread::disable_syscall_interruption dsi; thr->interrupt_and_join(); delete thr; thr = NULL; } } void setEndReachedCallback(const boost::function<void ()> &callback) { endReachedCallback = callback; } void appendToBuffer(const StaticString &dataToAdd) { TRACE_POINT(); boost::lock_guard<boost::mutex> l(dataSyncher); data.append(dataToAdd.data(), dataToAdd.size()); } string getData() const { boost::lock_guard<boost::mutex> l(dataSyncher); return data; } bool isStopped() const { boost::lock_guard<boost::mutex> l(dataSyncher); return stopped; } }; typedef boost::shared_ptr<BackgroundIOCapturer> BackgroundIOCapturerPtr; } // namespace SpawningKit } // namespace Passenger #endif /* _PASSENGER_SPAWNING_KIT_BACKGROUND_IO_CAPTURER_H_ */ SpawningKit/Exceptions.h 0000644 00000116212 14756456557 0011327 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2011-2018 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef _PASSENGER_SPAWNING_KIT_EXCEPTIONS_H_ #define _PASSENGER_SPAWNING_KIT_EXCEPTIONS_H_ #include <oxt/tracable_exception.hpp> #include <string> #include <stdexcept> #include <limits> #include <Constants.h> #include <Exceptions.h> #include <LoggingKit/LoggingKit.h> #include <DataStructures/StringKeyTable.h> #include <ProcessManagement/Spawn.h> #include <SystemTools/SystemMetricsCollector.h> #include <StrIntTools/StrIntUtils.h> #include <Core/SpawningKit/Config.h> #include <Core/SpawningKit/Journey.h> extern "C" { extern char **environ; } namespace Passenger { namespace SpawningKit { using namespace std; using namespace oxt; enum ErrorCategory { INTERNAL_ERROR, FILE_SYSTEM_ERROR, OPERATING_SYSTEM_ERROR, IO_ERROR, TIMEOUT_ERROR, UNKNOWN_ERROR_CATEGORY }; inline StaticString errorCategoryToString(ErrorCategory category); inline ErrorCategory inferErrorCategoryFromAnotherException(const std::exception &e, JourneyStep failedJourneyStep); /** * For an introduction see README.md, section "Error reporting". */ class SpawnException: public oxt::tracable_exception { private: struct EnvDump { pid_t pid; string envvars; string userInfo; string ulimits; EnvDump() : pid(-1) { } }; ErrorCategory category; Journey journey; Config config; string summary; string advancedProblemDetails; string problemDescription; string solutionDescription; string stdoutAndErrData; string id; EnvDump parentProcessEnvDump; EnvDump preloaderEnvDump; EnvDump subprocessEnvDump; string systemMetrics; StringKeyTable<string> annotations; static string createDefaultSummary(ErrorCategory category, const Journey &journey, const StaticString &advancedProblemDetails) { string message; switch (category) { case TIMEOUT_ERROR: // We only return a single error message instead of a customized // one based on the failed step, because the timeout // applies to the entire journey, not just to a specific step. // A timeout at a specific step could be the result of a previous // step taking too much time. // The way to debug a timeout error is by looking at the timings // of each step. switch (journey.getType()) { case START_PRELOADER: message = "A timeout occurred while starting a preloader process"; break; default: message = "A timeout occurred while spawning an application process"; break; } break; default: string categoryPhraseWithIndefiniteArticle = getErrorCategoryPhraseWithIndefiniteArticle( category, true); switch (journey.getType()) { case START_PRELOADER: switch (journey.getFirstFailedStep()) { case SPAWNING_KIT_PREPARATION: message = categoryPhraseWithIndefiniteArticle + " occurred while preparing to start a preloader process"; break; default: message = categoryPhraseWithIndefiniteArticle + " occurred while starting a preloader process"; break; } break; default: switch (journey.getFirstFailedStep()) { case SPAWNING_KIT_PREPARATION: message = categoryPhraseWithIndefiniteArticle + " occurred while preparing to spawn an application process"; break; case SPAWNING_KIT_FORK_SUBPROCESS: message = categoryPhraseWithIndefiniteArticle + " occurred while creating (forking) subprocess"; break; case SPAWNING_KIT_CONNECT_TO_PRELOADER: message = categoryPhraseWithIndefiniteArticle + " occurred while connecting to the preloader process"; break; case SPAWNING_KIT_SEND_COMMAND_TO_PRELOADER: message = categoryPhraseWithIndefiniteArticle + " occurred while sending a command to the preloader process"; break; case SPAWNING_KIT_READ_RESPONSE_FROM_PRELOADER: message = categoryPhraseWithIndefiniteArticle + " occurred while receiving a response from the preloader process"; break; case SPAWNING_KIT_PARSE_RESPONSE_FROM_PRELOADER: message = categoryPhraseWithIndefiniteArticle + " occurred while parsing a response from the preloader process"; break; case SPAWNING_KIT_PROCESS_RESPONSE_FROM_PRELOADER: message = categoryPhraseWithIndefiniteArticle + " occurred while processing a response from the preloader process"; break; default: message = categoryPhraseWithIndefiniteArticle + " occurred while spawning an application process"; break; } break; } break; } if (advancedProblemDetails.empty()) { message.append("."); } else { message.append(": "); message.append(advancedProblemDetails); } return message; } static string createDefaultProblemDescription(ErrorCategory category, const Journey &journey, const Config &config, const StaticString &advancedProblemDetails, const StaticString &stdoutAndErrData) { StaticString categoryStringWithIndefiniteArticle = getErrorCategoryPhraseWithIndefiniteArticle(category, false); switch (category) { case INTERNAL_ERROR: case OPERATING_SYSTEM_ERROR: case IO_ERROR: switch (journey.getType()) { case START_PRELOADER: switch (journey.getFirstFailedStep()) { case SPAWNING_KIT_PREPARATION: return wrapInParaAndMaybeAddErrorMessages( "The " PROGRAM_NAME " application server tried to" " start the web application. In doing so, " SHORT_PROGRAM_NAME " had to first start an internal" " helper tool called the \"preloader\". But " SHORT_PROGRAM_NAME " encountered " + categoryStringWithIndefiniteArticle + " while performing this preparation work", category, advancedProblemDetails, stdoutAndErrData); case SPAWNING_KIT_FORK_SUBPROCESS: return wrapInParaAndMaybeAddErrorMessages( "The " PROGRAM_NAME " application server tried to" " start the web application. But " SHORT_PROGRAM_NAME " encountered " + categoryStringWithIndefiniteArticle + " while creating a subprocess", category, advancedProblemDetails, stdoutAndErrData); case SPAWNING_KIT_HANDSHAKE_PERFORM: return wrapInParaAndMaybeAddErrorMessages( "The " PROGRAM_NAME " application server tried to" " start the web application. In doing so, " SHORT_PROGRAM_NAME " first started an internal" " helper tool called the \"preloader\". But " SHORT_PROGRAM_NAME " encountered " + categoryStringWithIndefiniteArticle + " while communicating with" " this tool about its startup", category, advancedProblemDetails, stdoutAndErrData); case SUBPROCESS_BEFORE_FIRST_EXEC: return wrapInParaAndMaybeAddErrorMessages( "The " PROGRAM_NAME " application server tried to" " start the web application. In doing so, " SHORT_PROGRAM_NAME " had to first start an internal" " helper tool called the \"preloader\". But" " the subprocess which was supposed to execute this" " preloader encountered " + categoryStringWithIndefiniteArticle, category, advancedProblemDetails, stdoutAndErrData); case SUBPROCESS_OS_SHELL: return wrapInParaAndMaybeAddErrorMessages( "The " PROGRAM_NAME " application server tried to" " start the web application. In doing so, " SHORT_PROGRAM_NAME " had to first start an internal" " helper tool called the \"preloader\", which" " in turn had to be started through the operating" " system (OS) shell. But the OS shell encountered " + categoryStringWithIndefiniteArticle, category, advancedProblemDetails, stdoutAndErrData); case SUBPROCESS_SPAWN_ENV_SETUPPER_BEFORE_SHELL: case SUBPROCESS_SPAWN_ENV_SETUPPER_AFTER_SHELL: return wrapInParaAndMaybeAddErrorMessages( "The " PROGRAM_NAME " application server tried to" " start the web application. In doing so, " SHORT_PROGRAM_NAME " had to first start an internal" " helper tool called the \"preloader\", which" " in turn had to be started through another internal" " tool called the \"SpawnEnvSetupper\". But the" " SpawnEnvSetupper encountered " + categoryStringWithIndefiniteArticle, category, advancedProblemDetails, stdoutAndErrData); case SUBPROCESS_EXEC_WRAPPER: if (!config.genericApp && config.startsUsingWrapper && config.wrapperSuppliedByThirdParty) { return wrapInParaAndMaybeAddErrorMessages( "The " PROGRAM_NAME " application server tried to" " start the web application through a " SHORT_PROGRAM_NAME " helper tool called" " the \"wrapper\". This helper tool is not part of " SHORT_PROGRAM_NAME ". But " SHORT_PROGRAM_NAME " was unable to execute that helper tool" " because it encountered " + categoryStringWithIndefiniteArticle, category, advancedProblemDetails, stdoutAndErrData); } else { return wrapInParaAndMaybeAddErrorMessages( "The " PROGRAM_NAME " application server tried to" " start the web application through a " SHORT_PROGRAM_NAME "-internal helper tool called" " the \"wrapper\". But " SHORT_PROGRAM_NAME " was unable to execute that helper tool" " because it encountered " + categoryStringWithIndefiniteArticle, category, advancedProblemDetails, stdoutAndErrData); } case SUBPROCESS_WRAPPER_PREPARATION: if (!config.genericApp && config.startsUsingWrapper && config.wrapperSuppliedByThirdParty) { return wrapInParaAndMaybeAddErrorMessages( "The " PROGRAM_NAME " application server tried to" " start the web application through a " SHORT_PROGRAM_NAME " helper tool called" " the \"wrapper\"). This tool is not part of " SHORT_PROGRAM_NAME ". But that helper tool encountered " + categoryStringWithIndefiniteArticle, category, advancedProblemDetails, stdoutAndErrData); } else { return wrapInParaAndMaybeAddErrorMessages( "The " PROGRAM_NAME " application server tried to" " start the web application through a " SHORT_PROGRAM_NAME "-internal helper tool called" " the \"wrapper\"). But that helper tool encountered " + categoryStringWithIndefiniteArticle, category, advancedProblemDetails, stdoutAndErrData); } case SUBPROCESS_APP_LOAD_OR_EXEC: return wrapInParaAndMaybeAddErrorMessages( "The " PROGRAM_NAME " application server tried to" " start the web application. But the application" " itself (and not " SHORT_PROGRAM_NAME ") encountered " + categoryStringWithIndefiniteArticle, category, advancedProblemDetails, stdoutAndErrData); case SUBPROCESS_LISTEN: return wrapInParaAndMaybeAddErrorMessages( "The " PROGRAM_NAME " application server tried to" " start the web application. The application tried " " to setup a socket for accepting connections," " but in doing so it encountered " + categoryStringWithIndefiniteArticle, category, advancedProblemDetails, stdoutAndErrData); case SUBPROCESS_FINISH: return wrapInParaAndMaybeAddErrorMessages( "The " PROGRAM_NAME " application server tried to" " start the web application, but the application" " encountered " + categoryStringWithIndefiniteArticle + " while finalizing its startup procedure", category, advancedProblemDetails, stdoutAndErrData); default: P_BUG("Unsupported preloader journey step " << toString((int) journey.getFirstFailedStep())); } default: switch (journey.getFirstFailedStep()) { case SPAWNING_KIT_PREPARATION: return wrapInParaAndMaybeAddErrorMessages( "The " PROGRAM_NAME " application server tried to" " start the web application, but " SHORT_PROGRAM_NAME " encountered " + categoryStringWithIndefiniteArticle + " while performing preparation work", category, advancedProblemDetails, stdoutAndErrData); case SPAWNING_KIT_FORK_SUBPROCESS: return wrapInParaAndMaybeAddErrorMessages( "The " PROGRAM_NAME " application server tried to" " start the web application. But " SHORT_PROGRAM_NAME " encountered " + categoryStringWithIndefiniteArticle + " while creating a subprocess", category, advancedProblemDetails, stdoutAndErrData); case SPAWNING_KIT_CONNECT_TO_PRELOADER: return wrapInParaAndMaybeAddErrorMessages( "The " PROGRAM_NAME " application server tried to" " start the web application by communicating with a" " helper process that we call a \"preloader\". However, " SHORT_PROGRAM_NAME " encountered " + categoryStringWithIndefiniteArticle + " while connecting to this helper process", category, advancedProblemDetails, stdoutAndErrData); case SPAWNING_KIT_SEND_COMMAND_TO_PRELOADER: return wrapInParaAndMaybeAddErrorMessages( "The " PROGRAM_NAME " application server tried to" " start the web application by communicating with a" " helper process that we call a \"preloader\". However, " SHORT_PROGRAM_NAME " encountered " + categoryStringWithIndefiniteArticle + " while sending a command to this helper process", category, advancedProblemDetails, stdoutAndErrData); case SPAWNING_KIT_READ_RESPONSE_FROM_PRELOADER: return wrapInParaAndMaybeAddErrorMessages( "The " PROGRAM_NAME " application server tried to" " start the web application by communicating with a" " helper process that we call a \"preloader\". However, " SHORT_PROGRAM_NAME " encountered " + categoryStringWithIndefiniteArticle + " while receiving a response to this helper process", category, advancedProblemDetails, stdoutAndErrData); case SPAWNING_KIT_PARSE_RESPONSE_FROM_PRELOADER: return wrapInParaAndMaybeAddErrorMessages( "The " PROGRAM_NAME " application server tried to" " start the web application by communicating with a" " helper process that we call a \"preloader\". However, " SHORT_PROGRAM_NAME " encountered " + categoryStringWithIndefiniteArticle + " while parsing a response from this helper process", category, advancedProblemDetails, stdoutAndErrData); case SPAWNING_KIT_PROCESS_RESPONSE_FROM_PRELOADER: return wrapInParaAndMaybeAddErrorMessages( "The " PROGRAM_NAME " application server tried to" " start the web application by communicating with a" " helper process that we call a \"preloader\". However, " SHORT_PROGRAM_NAME " encountered " + categoryStringWithIndefiniteArticle + " while processing a response from this helper process", category, advancedProblemDetails, stdoutAndErrData); case SPAWNING_KIT_HANDSHAKE_PERFORM: return wrapInParaAndMaybeAddErrorMessages( "The " PROGRAM_NAME " application server tried to" " start the web application. Everything was looking OK," " but then suddenly " SHORT_PROGRAM_NAME " encountered " + categoryStringWithIndefiniteArticle, category, advancedProblemDetails, stdoutAndErrData); case SUBPROCESS_BEFORE_FIRST_EXEC: return wrapInParaAndMaybeAddErrorMessages( "The " PROGRAM_NAME " application server tried to" " start the web application. " SHORT_PROGRAM_NAME " launched a subprocess which was supposed to" " execute the application, but instead that" " subprocess encountered " + categoryStringWithIndefiniteArticle, category, advancedProblemDetails, stdoutAndErrData); case SUBPROCESS_OS_SHELL: return wrapInParaAndMaybeAddErrorMessages( "The " PROGRAM_NAME " application server tried to" " start the web application through the operating" " system (OS) shell. But the OS shell encountered " + categoryStringWithIndefiniteArticle, category, advancedProblemDetails, stdoutAndErrData); case SUBPROCESS_SPAWN_ENV_SETUPPER_BEFORE_SHELL: case SUBPROCESS_SPAWN_ENV_SETUPPER_AFTER_SHELL: return wrapInParaAndMaybeAddErrorMessages( "The " PROGRAM_NAME " application server tried to" " start the web application through a " SHORT_PROGRAM_NAME "-internal helper tool called the" " SpawnEnvSetupper. But that helper tool encountered " + categoryStringWithIndefiniteArticle, category, advancedProblemDetails, stdoutAndErrData); case SUBPROCESS_EXEC_WRAPPER: if (!config.genericApp && config.startsUsingWrapper && config.wrapperSuppliedByThirdParty) { return wrapInParaAndMaybeAddErrorMessages( "The " PROGRAM_NAME " application server tried to" " start the web application through a " SHORT_PROGRAM_NAME " helper tool called" " the \"wrapper\". This helper tool is not part of " SHORT_PROGRAM_NAME ". But " SHORT_PROGRAM_NAME " was unable to execute that helper tool because" " it encountered " + categoryStringWithIndefiniteArticle, category, advancedProblemDetails, stdoutAndErrData); } else { return wrapInParaAndMaybeAddErrorMessages( "The " PROGRAM_NAME " application server tried to" " start the web application through a " SHORT_PROGRAM_NAME "-internal helper tool called" " the \"wrapper\". But " SHORT_PROGRAM_NAME " was unable to execute that helper tool because" " it encountered " + categoryStringWithIndefiniteArticle, category, advancedProblemDetails, stdoutAndErrData); } case SUBPROCESS_WRAPPER_PREPARATION: if (!config.genericApp && config.startsUsingWrapper && config.wrapperSuppliedByThirdParty) { return wrapInParaAndMaybeAddErrorMessages( "The " PROGRAM_NAME " application server tried to" " start the web application through a " SHORT_PROGRAM_NAME " helper tool called" " the \"wrapper\". This helper tool is not part of " SHORT_PROGRAM_NAME ". But that helper tool encountered " + categoryStringWithIndefiniteArticle, category, advancedProblemDetails, stdoutAndErrData); } else { return wrapInParaAndMaybeAddErrorMessages( "The " PROGRAM_NAME " application server tried to" " start the web application through a " SHORT_PROGRAM_NAME "-internal helper tool called" " the \"wrapper\". But that helper tool encountered " + categoryStringWithIndefiniteArticle, category, advancedProblemDetails, stdoutAndErrData); } case SUBPROCESS_APP_LOAD_OR_EXEC: return wrapInParaAndMaybeAddErrorMessages( "The " PROGRAM_NAME " application server tried to" " start the web application. But the application" " itself (and not " SHORT_PROGRAM_NAME ") encountered " + categoryStringWithIndefiniteArticle, category, advancedProblemDetails, stdoutAndErrData); case SUBPROCESS_PREPARE_AFTER_FORKING_FROM_PRELOADER: return wrapInParaAndMaybeAddErrorMessages( "The " PROGRAM_NAME " application server tried to" " start the web application through a " SHORT_PROGRAM_NAME "-internal helper tool called" " the \"preloader\". But the preloader encountered " + categoryStringWithIndefiniteArticle, category, advancedProblemDetails, stdoutAndErrData); case SUBPROCESS_LISTEN: return wrapInParaAndMaybeAddErrorMessages( "The " PROGRAM_NAME " application server tried to" " start the web application. The application tried " " to setup a socket for accepting connections," " but in doing so it encountered " + categoryStringWithIndefiniteArticle, category, advancedProblemDetails, stdoutAndErrData); case SUBPROCESS_FINISH: return wrapInParaAndMaybeAddErrorMessages( "The " PROGRAM_NAME " application server tried to" " start the web application, but the application" " encountered " + categoryStringWithIndefiniteArticle + " while finalizing its startup procedure", category, advancedProblemDetails, stdoutAndErrData); default: P_BUG("Unrecognized journey step " << toString((int) journey.getFirstFailedStep())); } } case TIMEOUT_ERROR: // We only return a single error message instead of a customized // one based on the failed step, because the timeout // applies to the entire journey, not just to a specific step. // A timeout at a specific step could be the result of a previous // step taking too much time. // The way to debug a timeout error is by looking at the timings // of each step. return wrapInParaAndMaybeAddErrorMessages( "The " PROGRAM_NAME " application server tried" " to start the web application, but this took too much time," " so " SHORT_PROGRAM_NAME " put a stop to that", TIMEOUT_ERROR, string(), stdoutAndErrData); default: P_BUG("Unrecognized error category " + toString((int) category)); return string(); // Never reached, shut up compiler warning. } } static string createDefaultSolutionDescription(ErrorCategory category, const Journey &journey, const Config &config) { string message; switch (category) { case INTERNAL_ERROR: return "<p class=\"sole-solution\">" "Unfortunately, " SHORT_PROGRAM_NAME " does not know" " how to solve this problem. Please try troubleshooting" " the problem by studying the <strong>error message</strong>" " and the <strong>diagnostics</strong> reports. You can also" " consult <a href=\"" SUPPORT_URL "\">the " SHORT_PROGRAM_NAME " support resources</a> for help.</p>"; case FILE_SYSTEM_ERROR: return "<p class=\"sole-solution\">" "Unfortunately, " SHORT_PROGRAM_NAME " does not know how to" " solve this problem. But it looks like some kind of filesystem error." " This generally means that you need to fix nonexistant" " files/directories or fix filesystem permissions. Please" " try troubleshooting the problem by studying the" " <strong>error message</strong> and the" " <strong>diagnostics</strong> reports.</p>"; case OPERATING_SYSTEM_ERROR: case IO_ERROR: return "<div class=\"multiple-solutions\">" "<h3>Check whether the server is low on resources</h3>" "<p>Maybe the server is currently low on resources. This would" " cause errors to occur. Please study the <em>error" " message</em> and the <em>diagnostics reports</em> to" " verify whether this is the case. Key things to check for:</p>" "<ul>" "<li>Excessive CPU usage</li>" "<li>Memory and swap</li>" "<li>Ulimits</li>" "</ul>" "<p>If the server is indeed low on resources, find a way to" " free up some resources.</p>" "<h3>Check your (filesystem) security settings</h3>" "<p>Maybe security settings are preventing " SHORT_PROGRAM_NAME " from doing the work it needs to do. Please check whether the" " error may be caused by your system's security settings, or" " whether it may be caused by wrong permissions on a file or" " directory.</p>" "<h3>Still no luck?</h3>" "<p>Please try troubleshooting the problem by studying the" " <em>diagnostics</em> reports.</p>" "</div>"; case TIMEOUT_ERROR: message = "<div class=\"multiple-solutions\">" "<h3>Check whether the server is low on resources</h3>" "<p>Maybe the server is currently so low on resources that" " all the work that needed to be done, could not finish within" " the given time limit." " Please inspect the server resource utilization statistics" " in the <em>diagnostics</em> section to verify" " whether server is indeed low on resources.</p>" "<p>If so, then either increase the spawn timeout (currently" " configured at " + toString(config.startTimeoutMsec / 1000) + " sec), or find a way to lower the server's resource" " utilization.</p>"; switch (journey.getFirstFailedStep()) { case SUBPROCESS_OS_SHELL: message.append( "<h3>Check whether your OS shell's startup scripts can" " take a long time or get stuck</h3>" "<p>One of your OS shell's startup scripts may do too much work," " or it may have invoked a command that then got stuck." " Please investigate and debug your OS shell's startup" " scripts.</p>"); break; case SUBPROCESS_APP_LOAD_OR_EXEC: if (config.appType == "nodejs") { message.append( "<h3>Check whether the application calls <code>http.Server.listen()</code></h3>" "<p>" SHORT_PROGRAM_NAME " requires that the application calls" " <code>listen()</code> on an http.Server object. If" " the application never calls this, then " SHORT_PROGRAM_NAME " will think the application is" " stuck. <a href=\"https://www.phusionpassenger.com/" "library/indepth/nodejs/reverse_port_binding.html\">" "Learn more about this problem.</a></p>"); } message.append( "<h3>Check whether the application is stuck during startup</h3>" "<p>The easiest way find out where the application is stuck" "is by inserting print statements into the application's code.</p>"); break; default: break; } message.append("<h3>Still no luck?</h3>" "<p>Please try troubleshooting the problem by studying the" " <em>diagnostics</em> reports.</p>" "</div>"); return message; default: return "(error generating solution description: unknown error category)"; } } static string createDefaultAdvancedProblemDetails(const std::exception &e) { const oxt::tracable_exception *te = dynamic_cast<const oxt::tracable_exception *>(&e); if (te != NULL) { return string(e.what()) + "\n" + te->backtrace(); } else { return e.what(); } } static StaticString getErrorCategoryPhraseWithIndefiniteArticle( ErrorCategory category, bool beginOfSentence) { switch (category) { case INTERNAL_ERROR: if (beginOfSentence) { return P_STATIC_STRING("An internal error"); } else { return P_STATIC_STRING("an internal error"); } case FILE_SYSTEM_ERROR: if (beginOfSentence) { return P_STATIC_STRING("A file system error"); } else { return P_STATIC_STRING("a file system error"); } case OPERATING_SYSTEM_ERROR: if (beginOfSentence) { return P_STATIC_STRING("An operating system error"); } else { return P_STATIC_STRING("an operating system error"); } case IO_ERROR: if (beginOfSentence) { return P_STATIC_STRING("An I/O error"); } else { return P_STATIC_STRING("an I/O error"); } case TIMEOUT_ERROR: if (beginOfSentence) { return P_STATIC_STRING("A timeout error"); } else { return P_STATIC_STRING("a timeout error"); } default: P_BUG("Unsupported error category " + toString((int) category)); return StaticString(); } } static StaticString getErrorCategoryPhraseWithIndefiniteArticle( const std::exception &e, const Journey &journey, bool beginOfSentence) { ErrorCategory category = inferErrorCategoryFromAnotherException( e, journey.getFirstFailedStep()); return getErrorCategoryPhraseWithIndefiniteArticle(category, beginOfSentence); } static string wrapInParaAndMaybeAddErrorMessages(const StaticString &message, ErrorCategory category, const StaticString &advancedProblemDetails, const StaticString &stdoutAndErrData) { string result = "<p>" + message + ".</p>"; if (!advancedProblemDetails.empty()) { if (category == INTERNAL_ERROR || category == FILE_SYSTEM_ERROR) { result.append("<p>Error details:</p>" "<pre>" + escapeHTML(advancedProblemDetails) + "</pre>"); } else if (category == IO_ERROR) { result.append("<p>The error reported by the I/O layer is:</p>" "<pre>" + escapeHTML(advancedProblemDetails) + "</pre>"); } else { P_ASSERT_EQ(category, OPERATING_SYSTEM_ERROR); result.append("<p>The error reported by the operating system is:</p>" "<pre>" + escapeHTML(advancedProblemDetails) + "</pre>"); } } if (!stdoutAndErrData.empty()) { result.append("<p>The stdout/stderr output of the subprocess so far is:</p>" "<pre>" + escapeHTML(stdoutAndErrData) + "</pre>"); } return result; } static string gatherEnvvars() { string result; unsigned int i = 0; while (environ[i] != NULL) { result.append(environ[i]); result.append(1, '\n'); i++; } return result; } static string gatherUlimits() { // On Linux, ulimit is a shell builtin and not a command. const char *command[] = { "/bin/sh", "-c", "ulimit -a", NULL }; try { SubprocessInfo info; SubprocessOutput output; runCommandAndCaptureOutput(command, info, output, std::numeric_limits<size_t>::max()); if (output.data.empty()) { output.data.assign("Error: command 'ulimit -a' failed"); } return output.data; } catch (const SystemException &e) { return P_STATIC_STRING("Error: command 'ulimit -a' failed: ") + e.what(); } } static string gatherUserInfo() { const char *command[] = { "id", "-a", NULL }; try { SubprocessInfo info; SubprocessOutput output; runCommandAndCaptureOutput(command, info, output, std::numeric_limits<size_t>::max()); if (output.data.empty()) { output.data.assign("Error: command 'id -a' failed"); } return output.data; } catch (const SystemException &e) { return P_STATIC_STRING("Error: command 'id -a' failed: ") + e.what(); } } static string gatherSystemMetrics() { SystemMetrics metrics; try { SystemMetricsCollector().collect(metrics); } catch (const RuntimeException &e) { return "Error: cannot parse system metrics: " + StaticString(e.what()); } FastStringStream<> stream; metrics.toDescription(stream); return string(stream.data(), stream.size()); } public: SpawnException(ErrorCategory _category, const Journey &_journey, const Config *_config) : category(_category), journey(_journey), config(*_config) { assert(_journey.getFirstFailedStep() != UNKNOWN_JOURNEY_STEP); config.internStrings(); } SpawnException(const std::exception &originalException, const Journey &_journey, const Config *_config) : category(inferErrorCategoryFromAnotherException( originalException, _journey.getFirstFailedStep())), journey(_journey), config(*_config), summary(createDefaultSummary( category, _journey, originalException.what())), advancedProblemDetails(createDefaultAdvancedProblemDetails(originalException)) { assert(_journey.getFirstFailedStep() != UNKNOWN_JOURNEY_STEP); config.internStrings(); } virtual ~SpawnException() throw() {} virtual const char *what() const throw() { return summary.c_str(); } ErrorCategory getErrorCategory() const { return category; } const Journey &getJourney() const { return journey; } const Config &getConfig() const { return config; } const string &getSummary() const { return summary; } void setSummary(const string &value) { summary = value; } const string &getAdvancedProblemDetails() const { return advancedProblemDetails; } void setAdvancedProblemDetails(const string &value) { advancedProblemDetails = value; } const string &getProblemDescriptionHTML() const { return problemDescription; } void setProblemDescriptionHTML(const string &value) { problemDescription = value; } const string &getSolutionDescriptionHTML() const { return solutionDescription; } void setSolutionDescriptionHTML(const string &value) { solutionDescription = value; } const string &getStdoutAndErrData() const { return stdoutAndErrData; } void setStdoutAndErrData(const string &value) { stdoutAndErrData = value; } const string &getId() const { return id; } void setId(const string &value) { id = value; } SpawnException &finalize() { TRACE_POINT(); if (summary.empty()) { summary = createDefaultSummary(category, journey, advancedProblemDetails); } if (problemDescription.empty()) { problemDescription = createDefaultProblemDescription( category, journey, config, advancedProblemDetails, stdoutAndErrData); } if (solutionDescription.empty()) { solutionDescription = createDefaultSolutionDescription( category, journey, config); } parentProcessEnvDump.pid = getpid(); parentProcessEnvDump.envvars = gatherEnvvars(); parentProcessEnvDump.userInfo = gatherUserInfo(); parentProcessEnvDump.ulimits = gatherUlimits(); systemMetrics = gatherSystemMetrics(); return *this; } const string &getParentProcessEnvvars() const { return parentProcessEnvDump.envvars; } const string &getParentProcessUserInfo() const { return parentProcessEnvDump.userInfo; } const string &getParentProcessUlimits() const { return parentProcessEnvDump.ulimits; } pid_t getPreloaderPid() const { return preloaderEnvDump.pid; } void setPreloaderPid(pid_t pid) { preloaderEnvDump.pid = pid; } const string &getPreloaderEnvvars() const { return preloaderEnvDump.envvars; } void setPreloaderEnvvars(const string &value) { preloaderEnvDump.envvars = value; } const string &getPreloaderUserInfo() const { return preloaderEnvDump.userInfo; } void setPreloaderUserInfo(const string &value) { preloaderEnvDump.userInfo = value; } const string &getPreloaderUlimits() const { return preloaderEnvDump.ulimits; } void setPreloaderUlimits(const string &value) { preloaderEnvDump.ulimits = value; } pid_t getSubprocessPid() const { return subprocessEnvDump.pid; } void setSubprocessPid(pid_t pid) { subprocessEnvDump.pid = pid; } const string &getSubprocessEnvvars() const { return subprocessEnvDump.envvars; } void setSubprocessEnvvars(const string &value) { subprocessEnvDump.envvars = value; } const string &getSubprocessUserInfo() const { return subprocessEnvDump.userInfo; } void setSubprocessUserInfo(const string &value) { subprocessEnvDump.userInfo = value; } const string &getSubprocessUlimits() const { return subprocessEnvDump.ulimits; } void setSubprocessUlimits(const string &value) { subprocessEnvDump.ulimits = value; } const string &getSystemMetrics() const { return systemMetrics; } const StringKeyTable<string> &getAnnotations() const { return annotations; } void setAnnotation(const HashedStaticString &name, const string &value, bool overwrite = true) { annotations.insert(name, value, overwrite); } Json::Value inspectBasicInfoAsJson() const { Json::Value doc; doc["category"] = errorCategoryToString(category).toString(); doc["summary"] = summary; doc["problem_description_html"] = problemDescription; doc["solution_description_html"] = solutionDescription; if (!advancedProblemDetails.empty()) { doc["aux_details"] = advancedProblemDetails; } if (!id.empty()) { doc["id"] = id; } return doc; } Json::Value inspectSystemWideDetailsAsJson() const { Json::Value doc; doc["system_metrics"] = systemMetrics; return doc; } Json::Value inspectParentProcessDetailsAsJson() const { Json::Value doc; doc["backtrace"] = backtrace(); doc["pid"] = (Json::Int) parentProcessEnvDump.pid; doc["envvars"] = getParentProcessEnvvars(); doc["user_info"] = getParentProcessUserInfo(); doc["ulimits"] = getParentProcessUlimits(); return doc; } Json::Value inspectPreloaderProcessDetailsAsJson() const { Json::Value doc, annotations(Json::objectValue); if (getPreloaderPid() != (pid_t) -1) { doc["pid"] = getPreloaderPid(); } doc["envvars"] = getPreloaderEnvvars(); doc["user_info"] = getPreloaderUserInfo(); doc["ulimits"] = getPreloaderUlimits(); StringKeyTable<string>::ConstIterator it(this->annotations); while (*it != NULL) { annotations[it.getKey().toString()] = it.getValue(); it.next(); } doc["annotations"] = annotations; return doc; } Json::Value inspectSubprocessDetailsAsJson() const { Json::Value doc, annotations(Json::objectValue); if (getSubprocessPid() != (pid_t) -1) { doc["pid"] = getSubprocessPid(); } doc["envvars"] = getSubprocessEnvvars(); doc["user_info"] = getSubprocessUserInfo(); doc["ulimits"] = getSubprocessUlimits(); doc["stdout_and_err"] = getStdoutAndErrData(); StringKeyTable<string>::ConstIterator it(this->annotations); while (*it != NULL) { annotations[it.getKey().toString()] = it.getValue(); it.next(); } doc["annotations"] = annotations; return doc; } }; inline StaticString errorCategoryToString(ErrorCategory category) { switch (category) { case INTERNAL_ERROR: return P_STATIC_STRING("INTERNAL_ERROR"); case FILE_SYSTEM_ERROR: return P_STATIC_STRING("FILE_SYSTEM_ERROR"); case OPERATING_SYSTEM_ERROR: return P_STATIC_STRING("OPERATING_SYSTEM_ERROR"); case IO_ERROR: return P_STATIC_STRING("IO_ERROR"); case TIMEOUT_ERROR: return P_STATIC_STRING("TIMEOUT_ERROR"); case UNKNOWN_ERROR_CATEGORY: return P_STATIC_STRING("UNKNOWN_ERROR_CATEGORY"); default: return P_STATIC_STRING("(invalid value)"); } } inline bool _isFileSystemError(const std::exception &e) { if (dynamic_cast<const FileSystemException *>(&e) != NULL) { return true; } const SystemException *sysEx = dynamic_cast<const SystemException *>(&e); if (sysEx != NULL) { return sysEx->code() == ENOENT || sysEx->code() == ENAMETOOLONG || sysEx->code() == EEXIST || sysEx->code() == EACCES; } return false; } inline bool _systemErrorIsActuallyIoError(JourneyStep failedJourneyStep) { return failedJourneyStep == SPAWNING_KIT_CONNECT_TO_PRELOADER || failedJourneyStep == SPAWNING_KIT_SEND_COMMAND_TO_PRELOADER || failedJourneyStep == SPAWNING_KIT_READ_RESPONSE_FROM_PRELOADER; } inline ErrorCategory inferErrorCategoryFromAnotherException(const std::exception &e, JourneyStep failedJourneyStep) { if (dynamic_cast<const SystemException *>(&e) != NULL) { if (_systemErrorIsActuallyIoError(failedJourneyStep)) { return IO_ERROR; } else { return OPERATING_SYSTEM_ERROR; } } else if (_isFileSystemError(e)) { return FILE_SYSTEM_ERROR; } else if (dynamic_cast<const IOException *>(&e) != NULL) { return IO_ERROR; } else if (dynamic_cast<const TimeoutException *>(&e) != NULL) { return TIMEOUT_ERROR; } else { return INTERNAL_ERROR; } } inline ErrorCategory stringToErrorCategory(const StaticString &value) { if (value == P_STATIC_STRING("INTERNAL_ERROR")) { return INTERNAL_ERROR; } else if (value == P_STATIC_STRING("FILE_SYSTEM_ERROR")) { return FILE_SYSTEM_ERROR; } else if (value == P_STATIC_STRING("OPERATING_SYSTEM_ERROR")) { return OPERATING_SYSTEM_ERROR; } else if (value == P_STATIC_STRING("IO_ERROR")) { return IO_ERROR; } else if (value == P_STATIC_STRING("TIMEOUT_ERROR")) { return TIMEOUT_ERROR; } else { return UNKNOWN_ERROR_CATEGORY; } } } // namespace SpawningKit } // namespace Passenger #endif /* _PASSENGER_SPAWNING_KIT_EXCEPTIONS_H_ */ SpawningKit/Spawner.h 0000644 00000013610 14756456557 0010623 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2011-2018 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef _PASSENGER_SPAWNING_KIT_SPAWNER_H_ #define _PASSENGER_SPAWNING_KIT_SPAWNER_H_ #include <boost/shared_ptr.hpp> #include <oxt/system_calls.hpp> #include <modp_b64.h> #include <AppLocalConfigFileUtils.h> #include <LoggingKit/Logging.h> #include <SystemTools/SystemTime.h> #include <Core/SpawningKit/Context.h> #include <Core/SpawningKit/Result.h> #include <Core/SpawningKit/UserSwitchingRules.h> namespace Passenger { namespace SpawningKit { using namespace std; using namespace boost; using namespace oxt; class Spawner { private: StringKeyTable<StaticString> decodeEnvironmentVariables(const StaticString &envvarsData) { StringKeyTable<StaticString> result; string::size_type keyStart = 0; while (keyStart < envvarsData.size()) { string::size_type keyEnd = envvarsData.find('\0', keyStart); string::size_type valueStart = keyEnd + 1; if (valueStart >= envvarsData.size()) { break; } string::size_type valueEnd = envvarsData.find('\0', valueStart); if (valueEnd >= envvarsData.size()) { break; } StaticString key = envvarsData.substr(keyStart, keyEnd - keyStart); StaticString value = envvarsData.substr(valueStart, valueEnd - valueStart); result.insert(key, value, true); keyStart = valueEnd + 1; } result.compact(); return result; } protected: Context *context; void setConfigFromAppPoolOptions(Config *config, Json::Value &extraArgs, const AppPoolOptions &options) { TRACE_POINT(); string envvarsData; try { envvarsData = modp::b64_decode(options.environmentVariables.data(), options.environmentVariables.size()); } catch (const std::runtime_error &) { P_WARN("Unable to decode base64-encoded environment variables: " << options.environmentVariables); envvarsData.clear(); } AppLocalConfig appLocalConfig = parseAppLocalConfigFile(options.appRoot); string startCommand; if (appLocalConfig.appSupportsKuriaProtocol) { config->genericApp = false; config->startsUsingWrapper = false; config->startCommand = options.appStartCommand; } else if (options.appType.empty()) { config->genericApp = true; config->startCommand = options.appStartCommand; } else { startCommand = options.getStartCommand(*context->resourceLocator, *context->wrapperRegistry); config->genericApp = false; config->startsUsingWrapper = true; config->startCommand = startCommand; } config->appGroupName = options.getAppGroupName(); config->appRoot = options.appRoot; config->logLevel = options.logLevel; config->wrapperSuppliedByThirdParty = false; config->findFreePort = false; config->preloadBundler = options.preloadBundler; config->loadShellEnvvars = options.loadShellEnvvars; config->startupFile = options.getStartupFile(*context->wrapperRegistry); config->appType = options.appType; config->processTitle = options.getProcessTitle(*context->wrapperRegistry); config->appEnv = options.environment; config->baseURI = options.baseURI; config->environmentVariables = decodeEnvironmentVariables( envvarsData); config->logFile = options.appLogFile; config->apiKey = options.apiKey; config->groupUuid = options.groupUuid; config->lveMinUid = options.lveMinUid; config->fileDescriptorUlimit = options.fileDescriptorUlimit; config->startTimeoutMsec = options.startTimeout; UserSwitchingInfo info = prepareUserSwitching(options, *context->wrapperRegistry); config->user = info.username; config->group = info.groupname; extraArgs["spawn_method"] = options.spawnMethod.toString(); config->bindAddress = options.bindAddress; /******************/ /******************/ config->internStrings(); } static void nonInterruptableKillAndWaitpid(pid_t pid) { boost::this_thread::disable_syscall_interruption dsi; syscalls::kill(pid, SIGKILL); syscalls::waitpid(pid, NULL, 0); } static void possiblyRaiseInternalError(const AppPoolOptions &options) { if (options.raiseInternalError) { throw RuntimeException("An internal error!"); } } public: /** * Timestamp at which this Spawner was created. Microseconds resolution. */ const unsigned long long creationTime; Spawner(Context *_context) : context(_context), creationTime(SystemTime::getUsec()) { } virtual ~Spawner() { } virtual Result spawn(const AppPoolOptions &options) = 0; virtual bool cleanable() const { return false; } virtual void cleanup() { // Do nothing. } virtual unsigned long long lastUsed() const { return 0; } Context *getContext() const { return context; } }; typedef boost::shared_ptr<Spawner> SpawnerPtr; } // namespace SpawningKit } // namespace Passenger #endif /* _PASSENGER_SPAWNING_KIT_SPAWNER_H_ */ SpawningKit/Result/AutoGeneratedCode.h 0000644 00000005356 14756456557 0014014 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2017 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /* * SpawningKit/Result/AutoGeneratedCode.h is automatically generated from * SpawningKit/Result/AutoGeneratedCode.h.cxxcodebuilder by the build system. * It uses the comment hints from SpawningKit/Result.h. * * To force regenerating this file: * rm -f src/agent/Core/SpawningKit/Result/AutoGeneratedCode.h * rake src/agent/Core/SpawningKit/Result/AutoGeneratedCode.h */ inline void Passenger::SpawningKit::Result::validate_autoGeneratedCode(vector<StaticString> &internalFieldErrors, vector<StaticString> &appSuppliedFieldErrors) const { const Result &result = *this; if (OXT_UNLIKELY(!(result.pid != -1))) { internalFieldErrors.push_back(P_STATIC_STRING("pid is not valid")); } if (OXT_UNLIKELY(gupid.empty())) { internalFieldErrors.push_back(P_STATIC_STRING("gupid may not be empty")); } if (OXT_UNLIKELY(!(result.spawnStartTime != 0))) { internalFieldErrors.push_back(P_STATIC_STRING("spawn_start_time is not valid")); } if (OXT_UNLIKELY(!(result.spawnEndTime != 0))) { internalFieldErrors.push_back(P_STATIC_STRING("spawn_end_time is not valid")); } if (OXT_UNLIKELY(!(result.spawnStartTimeMonotonic != 0))) { internalFieldErrors.push_back(P_STATIC_STRING("spawn_start_time_monotonic is not valid")); } if (OXT_UNLIKELY(!(result.spawnEndTimeMonotonic != 0))) { internalFieldErrors.push_back(P_STATIC_STRING("spawn_end_time_monotonic is not valid")); } /* * Excluded: * * type * codeRevision * stdinFd * stdoutAndErrFd */ } SpawningKit/Result/AutoGeneratedCode.h.cxxcodebuilder 0000644 00000007143 14756456557 0017013 0 ustar 00 # Phusion Passenger - https://www.phusionpassenger.com/ # Copyright (c) 2017 Phusion Holding B.V. # # "Passenger", "Phusion Passenger" and "Union Station" are registered # trademarks of Phusion Holding B.V. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. require 'build/support/vendor/cxx_hinted_parser/lib/cxx_hinted_parser' def main result_class_fields = parse_result_class_fields comment copyright_header_for(__FILE__), 1 separator comment %q{ SpawningKit/Result/AutoGeneratedCode.h is automatically generated from SpawningKit/Result/AutoGeneratedCode.h.cxxcodebuilder by the build system. It uses the comment hints from SpawningKit/Result.h. To force regenerating this file: rm -f src/agent/Core/SpawningKit/Result/AutoGeneratedCode.h rake src/agent/Core/SpawningKit/Result/AutoGeneratedCode.h } separator function 'inline void Passenger::SpawningKit::Result::validate_autoGeneratedCode(' \ 'vector<StaticString> &internalFieldErrors, ' \ 'vector<StaticString> &appSuppliedFieldErrors) const' \ do add_code %q{ const Result &result = *this; } separator excluded_field_names = [] result_class_fields.each do |field| if field.metadata[:supplied_by_app] error_collection = 'appSuppliedFieldErrors' else error_collection = 'internalFieldErrors' end if field.metadata[:require_non_empty] add_code %Q{ if (OXT_UNLIKELY(#{field.name}.empty())) { #{error_collection}.push_back(P_STATIC_STRING("#{filename_for(field)} may not be empty")); } } elsif field.metadata[:require] add_code %Q{ if (OXT_UNLIKELY(!(#{field.metadata[:require]}))) { #{error_collection}.push_back(P_STATIC_STRING("#{filename_for(field)} is not valid")); } } else excluded_field_names << field.name end end separator comment "Excluded:\n\n#{excluded_field_names.join("\n")}" end end def filename_for(field) key = field.metadata[:supplied_by_app] if key.is_a?(String) key else field.name.gsub(/([A-Z])/, '_\1').downcase end end def read_expression_for(field) case field.type when 'string' %Q{strip(readAll(dir + "/#{filename_for(field)}"))} when 'vector<Socket>' %Q{parseSocketJsonFile(dir + "/#{filename_for(field)}")} else raise "Unsupported field type '#{field.type}' for field #{field.name}" end end def parse_result_class_fields result_h = File.dirname(__FILE__) + '/../Result.h' parser = CxxHintedParser::Parser.load_file(result_h).parse parser.structs['Result'] end main SpawningKit/Config/AutoGeneratedCode.h 0000644 00000025507 14756456557 0013743 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2017 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /* * SpawningKit/Config/AutoGeneratedCode.h is automatically generated from * SpawningKit/Config/AutoGeneratedCode.h.cxxcodebuilder by the build system. * It uses the comment hints from SpawningKit/Config.h. * * To force regenerating this file: * rm -f src/agent/Core/SpawningKit/Config/AutoGeneratedCode.h * rake src/agent/Core/SpawningKit/Config/AutoGeneratedCode.h */ inline void Passenger::SpawningKit::Config::internStrings() { size_t totalSize = 0; size_t tmpSize; char *newStorage, *pos, *end; /* * Calculated required storage size */ totalSize += appGroupName.size() + 1; totalSize += appRoot.size() + 1; totalSize += startCommand.size() + 1; totalSize += startupFile.size() + 1; totalSize += processTitle.size() + 1; totalSize += appType.size() + 1; totalSize += appEnv.size() + 1; totalSize += spawnMethod.size() + 1; totalSize += bindAddress.size() + 1; totalSize += baseURI.size() + 1; totalSize += user.size() + 1; totalSize += group.size() + 1; { StringKeyTable<StaticString>::ConstIterator it(environmentVariables); while (*it != NULL) { totalSize += it.getValue().size() + 1; it.next(); } } totalSize += logFile.size() + 1; totalSize += apiKey.size() + 1; totalSize += groupUuid.size() + 1; /* * Allocate new storage */ newStorage = pos = new char[totalSize]; end = newStorage + totalSize; /* * Fill new storage */ pos = appendData(pos, end, appGroupName); pos = appendData(pos, end, "\0", 1); pos = appendData(pos, end, appRoot); pos = appendData(pos, end, "\0", 1); pos = appendData(pos, end, startCommand); pos = appendData(pos, end, "\0", 1); pos = appendData(pos, end, startupFile); pos = appendData(pos, end, "\0", 1); pos = appendData(pos, end, processTitle); pos = appendData(pos, end, "\0", 1); pos = appendData(pos, end, appType); pos = appendData(pos, end, "\0", 1); pos = appendData(pos, end, appEnv); pos = appendData(pos, end, "\0", 1); pos = appendData(pos, end, spawnMethod); pos = appendData(pos, end, "\0", 1); pos = appendData(pos, end, bindAddress); pos = appendData(pos, end, "\0", 1); pos = appendData(pos, end, baseURI); pos = appendData(pos, end, "\0", 1); pos = appendData(pos, end, user); pos = appendData(pos, end, "\0", 1); pos = appendData(pos, end, group); pos = appendData(pos, end, "\0", 1); { StringKeyTable<StaticString>::Iterator it(environmentVariables); while (*it != NULL) { pos = appendData(pos, end, it.getValue()); pos = appendData(pos, end, "\0", 1); it.next(); } } pos = appendData(pos, end, logFile); pos = appendData(pos, end, "\0", 1); pos = appendData(pos, end, apiKey); pos = appendData(pos, end, "\0", 1); pos = appendData(pos, end, groupUuid); pos = appendData(pos, end, "\0", 1); /* * Move over pointers to new storage */ pos = newStorage; tmpSize = appGroupName.size(); appGroupName = StaticString(pos, tmpSize); pos += tmpSize + 1; tmpSize = appRoot.size(); appRoot = StaticString(pos, tmpSize); pos += tmpSize + 1; tmpSize = startCommand.size(); startCommand = StaticString(pos, tmpSize); pos += tmpSize + 1; tmpSize = startupFile.size(); startupFile = StaticString(pos, tmpSize); pos += tmpSize + 1; tmpSize = processTitle.size(); processTitle = StaticString(pos, tmpSize); pos += tmpSize + 1; tmpSize = appType.size(); appType = StaticString(pos, tmpSize); pos += tmpSize + 1; tmpSize = appEnv.size(); appEnv = StaticString(pos, tmpSize); pos += tmpSize + 1; tmpSize = spawnMethod.size(); spawnMethod = StaticString(pos, tmpSize); pos += tmpSize + 1; tmpSize = bindAddress.size(); bindAddress = StaticString(pos, tmpSize); pos += tmpSize + 1; tmpSize = baseURI.size(); baseURI = StaticString(pos, tmpSize); pos += tmpSize + 1; tmpSize = user.size(); user = StaticString(pos, tmpSize); pos += tmpSize + 1; tmpSize = group.size(); group = StaticString(pos, tmpSize); pos += tmpSize + 1; { StringKeyTable<StaticString>::Iterator it(environmentVariables); while (*it != NULL) { tmpSize = it.getValue().size(); it.getValue() = StaticString(pos, tmpSize); pos += tmpSize + 1; it.next(); } } tmpSize = logFile.size(); logFile = StaticString(pos, tmpSize); pos += tmpSize + 1; tmpSize = apiKey.size(); apiKey = StaticString(pos, tmpSize); pos += tmpSize + 1; tmpSize = groupUuid.size(); groupUuid = StaticString(pos, tmpSize); pos += tmpSize + 1; /* * Commit current storage */ storage.reset(newStorage); } inline bool Passenger::SpawningKit::Config::validate(vector<StaticString> &errors) const { bool ok = true; const Config &config = *this; if (OXT_UNLIKELY(appGroupName.empty())) { ok = false; errors.push_back(P_STATIC_STRING("app_group_name may not be empty")); } if (OXT_UNLIKELY(appRoot.empty())) { ok = false; errors.push_back(P_STATIC_STRING("app_root may not be empty")); } if (OXT_UNLIKELY(startCommand.empty())) { ok = false; errors.push_back(P_STATIC_STRING("start_command may not be empty")); } if (!config.genericApp && config.startsUsingWrapper && OXT_UNLIKELY(startupFile.empty())) { ok = false; errors.push_back(P_STATIC_STRING("startup_file may not be empty")); } if (OXT_UNLIKELY(appType.empty())) { ok = false; errors.push_back(P_STATIC_STRING("app_type may not be empty")); } if (OXT_UNLIKELY(appEnv.empty())) { ok = false; errors.push_back(P_STATIC_STRING("app_env may not be empty")); } if (OXT_UNLIKELY(spawnMethod.empty())) { ok = false; errors.push_back(P_STATIC_STRING("spawn_method may not be empty")); } if (OXT_UNLIKELY(bindAddress.empty())) { ok = false; errors.push_back(P_STATIC_STRING("bind_address may not be empty")); } if (OXT_UNLIKELY(baseURI.empty())) { ok = false; errors.push_back(P_STATIC_STRING("base_uri may not be empty")); } if (OXT_UNLIKELY(user.empty())) { ok = false; errors.push_back(P_STATIC_STRING("user may not be empty")); } if (OXT_UNLIKELY(group.empty())) { ok = false; errors.push_back(P_STATIC_STRING("group may not be empty")); } if (OXT_UNLIKELY(!(config.startTimeoutMsec > 0))) { ok = false; errors.push_back(P_STATIC_STRING("start_timeout_msec does not satisfy requirement: " "config.startTimeoutMsec > 0")); } /* * Excluded: * * logLevel * genericApp * startsUsingWrapper * wrapperSuppliedByThirdParty * findFreePort * preloadBundler * loadShellEnvvars * debugWorkDir * processTitle * environmentVariables * logFile * apiKey * groupUuid * lveMinUid * fileDescriptorUlimit */ return ok; } inline Json::Value Passenger::SpawningKit::Config::getConfidentialFieldsToPassToApp() const { Json::Value doc; const Config &config = *this; doc["app_group_name"] = appGroupName.toString(); doc["app_root"] = appRoot.toString(); doc["log_level"] = logLevel; doc["generic_app"] = genericApp; if (!config.genericApp) { doc["starts_using_wrapper"] = startsUsingWrapper; } if (!config.genericApp && config.startsUsingWrapper) { doc["wrapper_supplied_by_third_party"] = wrapperSuppliedByThirdParty; } if (config.appType == "ruby") { doc["preload_bundler"] = preloadBundler; } doc["load_shell_envvars"] = loadShellEnvvars; doc["start_command"] = startCommand.toString(); if (!config.genericApp && config.startsUsingWrapper) { doc["startup_file"] = startupFile.toString(); } if (!config.processTitle.empty()) { doc["process_title"] = processTitle.toString(); } doc["app_type"] = appType.toString(); doc["app_env"] = appEnv.toString(); doc["spawn_method"] = spawnMethod.toString(); doc["bind_address"] = bindAddress.toString(); doc["base_uri"] = baseURI.toString(); doc["user"] = user.toString(); doc["group"] = group.toString(); doc["environment_variables"] = tableToJson(environmentVariables); doc["log_file"] = logFile.toString(); if (!config.apiKey.empty()) { doc["api_key"] = apiKey.toString(); } if (!config.groupUuid.empty()) { doc["group_uuid"] = groupUuid.toString(); } if (config.fileDescriptorUlimit > 0) { doc["file_descriptor_ulimit"] = fileDescriptorUlimit; } /* * Excluded: * * findFreePort * debugWorkDir * lveMinUid * startTimeoutMsec */ return doc; } inline Json::Value Passenger::SpawningKit::Config::getNonConfidentialFieldsToPassToApp() const { Json::Value doc; const Config &config = *this; doc["app_group_name"] = appGroupName.toString(); doc["app_root"] = appRoot.toString(); doc["log_level"] = logLevel; doc["generic_app"] = genericApp; if (!config.genericApp) { doc["starts_using_wrapper"] = startsUsingWrapper; } if (!config.genericApp && config.startsUsingWrapper) { doc["wrapper_supplied_by_third_party"] = wrapperSuppliedByThirdParty; } if (config.appType == "ruby") { doc["preload_bundler"] = preloadBundler; } doc["load_shell_envvars"] = loadShellEnvvars; doc["start_command"] = startCommand.toString(); if (!config.genericApp && config.startsUsingWrapper) { doc["startup_file"] = startupFile.toString(); } if (!config.processTitle.empty()) { doc["process_title"] = processTitle.toString(); } doc["app_type"] = appType.toString(); doc["app_env"] = appEnv.toString(); doc["spawn_method"] = spawnMethod.toString(); doc["bind_address"] = bindAddress.toString(); doc["base_uri"] = baseURI.toString(); doc["user"] = user.toString(); doc["group"] = group.toString(); doc["environment_variables"] = "<SECRET>"; doc["log_file"] = logFile.toString(); if (!config.apiKey.empty()) { doc["api_key"] = "<SECRET>"; } if (!config.groupUuid.empty()) { doc["group_uuid"] = "<SECRET>"; } if (config.fileDescriptorUlimit > 0) { doc["file_descriptor_ulimit"] = fileDescriptorUlimit; } /* * Excluded: * * findFreePort * debugWorkDir * lveMinUid * startTimeoutMsec */ return doc; } SpawningKit/Config/AutoGeneratedCode.h.cxxcodebuilder 0000644 00000020567 14756456557 0016747 0 ustar 00 # Phusion Passenger - https://www.phusionpassenger.com/ # Copyright (c) 2017 Phusion Holding B.V. # # "Passenger", "Phusion Passenger" and "Union Station" are registered # trademarks of Phusion Holding B.V. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. require 'build/support/vendor/cxx_hinted_parser/lib/cxx_hinted_parser' def main fields = parse_config_class_fields comment copyright_header_for(__FILE__), 1 separator comment %q{ SpawningKit/Config/AutoGeneratedCode.h is automatically generated from SpawningKit/Config/AutoGeneratedCode.h.cxxcodebuilder by the build system. It uses the comment hints from SpawningKit/Config.h. To force regenerating this file: rm -f src/agent/Core/SpawningKit/Config/AutoGeneratedCode.h rake src/agent/Core/SpawningKit/Config/AutoGeneratedCode.h } separator function 'inline void Passenger::SpawningKit::Config::internStrings()' do add_code %Q{ size_t totalSize = 0; size_t tmpSize; char *newStorage, *pos, *end; } separator comment 'Calculated required storage size' fields.each do |field| case field.type when 'StaticString' add_code %Q{ totalSize += #{field.name}.size() + 1; } when 'StringKeyTable<StaticString>' add_code %Q{ { StringKeyTable<StaticString>::ConstIterator it(#{field.name}); while (*it != NULL) { totalSize += it.getValue().size() + 1; it.next(); } } } end end separator comment 'Allocate new storage' add_code %Q{ newStorage = pos = new char[totalSize]; end = newStorage + totalSize; } separator comment 'Fill new storage' fields.each do |field| case field.type when 'StaticString' add_code %Q{ pos = appendData(pos, end, #{field.name}); pos = appendData(pos, end, "\\0", 1); } when 'StringKeyTable<StaticString>' add_code %Q{ { StringKeyTable<StaticString>::Iterator it(#{field.name}); while (*it != NULL) { pos = appendData(pos, end, it.getValue()); pos = appendData(pos, end, "\\0", 1); it.next(); } } } end end separator comment 'Move over pointers to new storage' add_code %Q{ pos = newStorage; } fields.each do |field| case field.type when 'StaticString' add_code %Q{ tmpSize = #{field.name}.size(); #{field.name} = StaticString(pos, tmpSize); pos += tmpSize + 1; } separator when 'StringKeyTable<StaticString>' add_code %Q{ { StringKeyTable<StaticString>::Iterator it(#{field.name}); while (*it != NULL) { tmpSize = it.getValue().size(); it.getValue() = StaticString(pos, tmpSize); pos += tmpSize + 1; it.next(); } } } separator end end separator comment 'Commit current storage' add_code %Q{ storage.reset(newStorage); } end function 'inline bool Passenger::SpawningKit::Config::validate(vector<StaticString> &errors) const' do add_code %q{ bool ok = true; const Config &config = *this; } separator excluded_field_names = [] fields.each do |field| if field.metadata[:only_meaningful_if] meaningfulness_check = "#{field.metadata[:only_meaningful_if]} && " end if field.metadata[:require_non_empty] add_code %Q{ if (#{meaningfulness_check}OXT_UNLIKELY(#{field.name}.empty())) { ok = false; errors.push_back(P_STATIC_STRING("#{key_for(field)} may not be empty")); } } elsif field.metadata[:require] add_code %Q{ if (#{meaningfulness_check}OXT_UNLIKELY(!(#{field.metadata[:require]}))) { ok = false; errors.push_back(P_STATIC_STRING("#{key_for(field)} does not satisfy requirement: " #{field.metadata[:require].inspect})); } } else excluded_field_names << field.name end end separator comment "Excluded:\n\n#{excluded_field_names.join("\n")}" separator add_code %Q{ return ok; } end function 'inline Json::Value Passenger::SpawningKit::Config::getConfidentialFieldsToPassToApp() const' do add_code %q{ Json::Value doc; const Config &config = *this; } separator excluded_field_names = [] fields.each do |field| if field.metadata[:pass_during_handshake] setter_code = %Q{ doc["#{key_for(field)}"] = #{value_expression_for(field)}; } if field.metadata[:only_meaningful_if] || field.metadata[:only_pass_during_handshake_if] conditions = [ field.metadata[:only_meaningful_if], field.metadata[:only_pass_during_handshake_if] ].compact add_code %Q[ if (#{conditions.join(" && ")}) { ] indent do add_code(setter_code) end add_code %Q[ } ] else add_code(setter_code) end else excluded_field_names << field.name end end separator comment "Excluded:\n\n#{excluded_field_names.join("\n")}" separator add_code %q{ return doc; } end function 'inline Json::Value Passenger::SpawningKit::Config::getNonConfidentialFieldsToPassToApp() const' do add_code %q{ Json::Value doc; const Config &config = *this; } separator excluded_field_names = [] fields.each do |field| if field.metadata[:pass_during_handshake] if field.metadata[:non_confidential] setter_code = %Q{ doc["#{key_for(field)}"] = #{value_expression_for(field)}; } else setter_code = %Q{ doc["#{key_for(field)}"] = "<SECRET>"; } end if field.metadata[:only_meaningful_if] || field.metadata[:only_pass_during_handshake_if] conditions = [ field.metadata[:only_meaningful_if], field.metadata[:only_pass_during_handshake_if] ].compact add_code %Q[ if (#{conditions.join(" && ")}) { ] indent do add_code(setter_code) end add_code %Q[ } ] else add_code(setter_code) end else excluded_field_names << field.name end end separator comment "Excluded:\n\n#{excluded_field_names.join("\n")}" separator add_code %q{ return doc; } end end def key_for(field) key = field.metadata[:pass_during_handshake] if key.is_a?(String) key else field.name.gsub(/([A-Z])/, '_\1').downcase end end def value_expression_for(field) case field.type when 'StaticString' "#{field.name}.toString()" when 'int', 'unsigned int', 'bool' field.name when 'StringKeyTable<StaticString>' "tableToJson(#{field.name})" else raise "Unsupported field type '#{field.type}' for field #{field.name}" end end def parse_config_class_fields config_h = File.dirname(__FILE__) + '/../Config.h' parser = CxxHintedParser::Parser.load_file(config_h).parse parser.structs['Config'] end main SpawningKit/Config.h 0000644 00000025701 14756456557 0010415 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2011-2018 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef _PASSENGER_SPAWNING_KIT_CONFIG_H_ #define _PASSENGER_SPAWNING_KIT_CONFIG_H_ #include <oxt/macros.hpp> #include <boost/shared_array.hpp> #include <vector> #include <cstddef> #include <jsoncpp/json.h> #include <Constants.h> #include <StaticString.h> #include <DataStructures/StringKeyTable.h> namespace Passenger { namespace SpawningKit { using namespace std; // The following hints are available: // // @require_non_empty // @pass_during_handshake // @non_confidential // @only_meaningful_if // @only_pass_during_handshake_if // // - begin hinted parseable class - class Config { private: boost::shared_array<char> storage; Json::Value tableToJson(const StringKeyTable<StaticString> &table) const { Json::Value doc(Json::objectValue); StringKeyTable<StaticString>::ConstIterator it(table); while (*it != NULL) { doc[it.getKey().toString()] = it.getValue().toString(); it.next(); } return doc; } public: /** * The app group name that the spawned process shall belong to. SpawningKit does * not use this information directly: it is passed to LoggingKit when logging * app output. * * @hinted_parseable * @require_non_empty * @pass_during_handshake * @non_confidential */ StaticString appGroupName; /** * The root directory of the application to spawn. For example, for Ruby apps, this * is the directory containing config.ru. The startCommand will be invoked from * this directory. * * @hinted_parseable * @require_non_empty * @pass_during_handshake * @non_confidential */ StaticString appRoot; /** * The log level to use. * * @hinted_parseable * @pass_during_handshake * @non_confidential */ int logLevel; /** * Whether the app to be spawned is generic or not. Generic * apps do not have special support for Passenger built in, * nor do we have a wrapper for loading the app. * * For example, Rack and Node.js apps are not considered * generic because we have wrappers for them. Go apps without * special Passenger support built in are considered generic. * * @hinted_parseable * @pass_during_handshake * @non_confidential */ bool genericApp: 1; /** * If the app is not generic (`!genericApp`), then this specifies * whether the app is loaded through a wrapper (true), or whether * the app has special support for Passenger built in and is * started directly (false). The only use for this in SpawningKit * is to better format error messages. * * @hinted_parseable * @only_meaningful_if !config.genericApp * @pass_during_handshake * @non_confidential */ bool startsUsingWrapper: 1; /** * When a wrapper is used to load the application, this field * specifies whether the wrapper is supplied by Phusion or by * a third party. The only use for this in SpawningKit is to better * format error messages. * * @hinted_parseable * @only_meaningful_if !config.genericApp && config.startsUsingWrapper * @pass_during_handshake * @non_confidential */ bool wrapperSuppliedByThirdParty: 1; /** * If the app is not generic (`!genericApp`), then this specifies * whether SpawningKit should find a free port to pass to the app * so that it can listen on that port. * This is always done if the app is generic, but *can* be done * for non-generic apps as well. * * @hinted_parseable * @only_meaningful_if !config.genericApp */ bool findFreePort: 1; /** * Whether Passenger should tell Ruby to preload bundler, * this is to help deal with multiple versions of gems * being installed, which is due to updates of default gems. * * @hinted_parseable * @pass_during_handshake * @only_meaningful_if config.appType == "ruby" * @non_confidential */ bool preloadBundler: 1; /** * Whether to load environment variables set in shell startup * files (e.g. ~/.bashrc) during spawning. * * @hinted_parseable * @pass_during_handshake * @non_confidential */ bool loadShellEnvvars: 1; /** * Set to true if you do not want SpawningKit to remove the * work directory after a spawning operation, which is useful * for debugging. Defaults to false. * * @hinted_parseable */ bool debugWorkDir: 1; /** * The command to run in order to start the app. * * If `genericApp` is true, then the command string must contain '$PORT'. * The command string is expected to start the app on the given port. * SpawningKit will take care of passing an appropriate $PORT value to * the app. * * If `genericApp` is false, then the command string is expected do * either one of these things: * - If there is a wrapper available for the app, then the command string * is to invoke the wrapper (and `startsUsingWrapper` should be true). * - Otherwise, the command string is to start the app directly, in * Passenger mode (and `startsUsingWrapper` should be false). * * @hinted_parseable * @require_non_empty * @pass_during_handshake * @non_confidential */ StaticString startCommand; /** * The application's entry point file. If a relative path is given, then it * is relative to the app root. Only meaningful if app is to be loaded through * a wrapper. * * @hinted_parseable * @only_meaningful_if !config.genericApp && config.startsUsingWrapper * @require_non_empty * @pass_during_handshake * @non_confidential */ StaticString startupFile; /** * A process title to set when spawning the application. * * @hinted_parseable * @pass_during_handshake * @non_confidential * @only_pass_during_handshake_if !config.processTitle.empty() */ StaticString processTitle; /** * An application type name, e.g. "ruby" or "nodejs". The only use for this * in SpawningKit is to better format error messages. * * @hinted_parseable * @require_non_empty * @pass_during_handshake * @non_confidential */ StaticString appType; /** * The value to set PASSENGER_APP_ENV/RAILS_ENV/etc to. * * @hinted_parseable * @require_non_empty * @pass_during_handshake * @non_confidential */ StaticString appEnv; /** * The spawn method used for spawning the app, i.e. "smart" or "direct". * * @hinted_parseable * @require_non_empty * @pass_during_handshake * @non_confidential */ StaticString spawnMethod; /** * The address that Passenger binds to in order to allow sending HTTP * requests to individual application processes. * * @hinted_parseable * @require_non_empty * @pass_during_handshake * @non_confidential */ StaticString bindAddress; /** * The base URI on which the app runs. If the app is running on the * root URI, then this value must be "/". * * @hinted_parseable * @require_non_empty * @pass_during_handshake base_uri * @non_confidential */ StaticString baseURI; /** * The user to start run the app as. Only has effect if the current process * is running with root privileges. * * @hinted_parseable * @require_non_empty * @pass_during_handshake * @non_confidential */ StaticString user; /** * The group to start run the app as. Only has effect if the current process * is running with root privileges. * * @hinted_parseable * @require_non_empty * @pass_during_handshake * @non_confidential */ StaticString group; /** * Any environment variables to pass to the application. These will be set * after the OS shell has already done its work, but before the application * is started. * * @hinted_parseable * @pass_during_handshake */ StringKeyTable<StaticString> environmentVariables; /** * Specifies that the app's stdout/stderr output should be written * to the given log file. * * @hinted_parseable * @non_confidential * @pass_during_handshake */ StaticString logFile; /** * The API key of the pool group that the spawned process is to belong to. * * @hinted_parseable * @pass_during_handshake * @only_pass_during_handshake_if !config.apiKey.empty() */ StaticString apiKey; /** * A UUID that's generated on Group initialization, and changes every time * the Group receives a restart command. Allows Union Station to track app * restarts. * * @hinted_parseable * @pass_during_handshake * @only_pass_during_handshake_if !config.groupUuid.empty() */ StaticString groupUuid; /** * Minimum user ID starting from which entering LVE and CageFS is allowed. * * @hinted_parseable */ unsigned int lveMinUid; /** * The file descriptor ulimit that the app should have. * A value of 0 means that the ulimit should not be changed. * * @hinted_parseable * @pass_during_handshake * @non_confidential * @only_pass_during_handshake_if config.fileDescriptorUlimit > 0 */ unsigned int fileDescriptorUlimit; /** * The maximum amount of time, in milliseconds, that may be spent * on spawning the process or the preloader. * * @hinted_parseable * @require config.startTimeoutMsec > 0 */ unsigned int startTimeoutMsec; /*********************/ /*********************/ Config() : logLevel(DEFAULT_LOG_LEVEL), genericApp(false), startsUsingWrapper(false), wrapperSuppliedByThirdParty(false), findFreePort(false), preloadBundler(false), loadShellEnvvars(false), debugWorkDir(false), appEnv(P_STATIC_STRING(DEFAULT_APP_ENV)), baseURI(P_STATIC_STRING("/")), lveMinUid(DEFAULT_LVE_MIN_UID), fileDescriptorUlimit(0), startTimeoutMsec(DEFAULT_START_TIMEOUT) /*********************/ { } void internStrings(); bool validate(vector<StaticString> &errors) const; Json::Value getConfidentialFieldsToPassToApp() const; Json::Value getNonConfidentialFieldsToPassToApp() const; }; // - end hinted parseable class - #include <Core/SpawningKit/Config/AutoGeneratedCode.h> } // namespace SpawningKit } // namespace Passenger #endif /* _PASSENGER_SPAWNING_KIT_CONFIG_H_ */ CoreMain.cpp 0000644 00000130427 14756456557 0007004 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2010-2018 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef _GNU_SOURCE #define _GNU_SOURCE #endif #ifdef __linux__ #define SUPPORTS_PER_THREAD_CPU_AFFINITY #include <sched.h> #include <pthread.h> #endif #ifdef USE_SELINUX #include <selinux/selinux.h> #endif #include <boost/config.hpp> #include <boost/scoped_ptr.hpp> #include <boost/foreach.hpp> #include <sys/types.h> #include <sys/socket.h> #include <sys/stat.h> #include <sys/un.h> #include <sys/resource.h> #include <cstring> #include <cassert> #include <cerrno> #include <stdlib.h> #include <unistd.h> #include <limits.h> #include <fcntl.h> #include <pwd.h> #include <grp.h> #include <set> #include <vector> #include <string> #include <algorithm> #include <iostream> #include <sstream> #include <stdexcept> #include <curl/curl.h> #include <boost/thread.hpp> #include <boost/shared_ptr.hpp> #include <boost/make_shared.hpp> #include <boost/atomic.hpp> #include <oxt/thread.hpp> #include <oxt/system_calls.hpp> #include <ev++.h> #include <jsoncpp/json.h> #include <Shared/Fundamentals/Initialization.h> #include <Shared/ApiServerUtils.h> #include <Constants.h> #include <LoggingKit/Context.h> #include <ConfigKit/SubComponentUtils.h> #include <ServerKit/Server.h> #include <ServerKit/AcceptLoadBalancer.h> #include <AppTypeDetector/Detector.h> #include <IOTools/MessageSerialization.h> #include <FileDescriptor.h> #include <ResourceLocator.h> #include <BackgroundEventLoop.cpp> #include <FileTools/FileManip.h> #include <FileTools/PathSecurityCheck.h> #include <Exceptions.h> #include <Utils.h> #include <Utils/Timer.h> #include <IOTools/MessageIO.h> #include <Core/OptionParser.h> #include <Core/Controller.h> #include <Core/ApiServer.h> #include <Core/Config.h> #include <Core/ConfigChange.h> #include <Core/ApplicationPool/Pool.h> #include <Core/SecurityUpdateChecker.h> #include <Core/TelemetryCollector.h> #include <Core/AdminPanelConnector.h> using namespace boost; using namespace oxt; using namespace Passenger; using namespace Passenger::Agent::Fundamentals; using namespace Passenger::ApplicationPool2; /***** Structures, constants, global variables and forward declarations *****/ namespace Passenger { namespace Core { struct ThreadWorkingObjects { BackgroundEventLoop *bgloop; ServerKit::Context *serverKitContext; Controller *controller; ThreadWorkingObjects() : bgloop(NULL), serverKitContext(NULL), controller(NULL) { } }; struct ApiWorkingObjects { BackgroundEventLoop *bgloop; ServerKit::Context *serverKitContext; ApiServer::ApiServer *apiServer; ApiWorkingObjects() : bgloop(NULL), serverKitContext(NULL), apiServer(NULL) { } }; struct WorkingObjects { int serverFds[SERVER_KIT_MAX_SERVER_ENDPOINTS]; int apiServerFds[SERVER_KIT_MAX_SERVER_ENDPOINTS]; string controllerSecureHeadersPassword; boost::mutex configSyncher; ResourceLocator resourceLocator; RandomGeneratorPtr randomGenerator; SpawningKit::Context::Schema spawningKitContextSchema; SpawningKit::ContextPtr spawningKitContext; ApplicationPool2::ContextPtr appPoolContext; PoolPtr appPool; Json::Value singleAppModeConfig; ServerKit::AcceptLoadBalancer<Controller> loadBalancer; vector<ThreadWorkingObjects> threadWorkingObjects; struct ev_signal sigintWatcher; struct ev_signal sigtermWatcher; struct ev_signal sigquitWatcher; ApiWorkingObjects apiWorkingObjects; EventFd exitEvent; EventFd allClientsDisconnectedEvent; unsigned int terminationCount; boost::atomic<unsigned int> shutdownCounter; oxt::thread *prestarterThread; SecurityUpdateChecker *securityUpdateChecker; TelemetryCollector *telemetryCollector; AdminPanelConnector *adminPanelConnector; oxt::thread *adminPanelConnectorThread; WorkingObjects() : exitEvent(__FILE__, __LINE__, "WorkingObjects: exitEvent"), allClientsDisconnectedEvent(__FILE__, __LINE__, "WorkingObjects: allClientsDisconnectedEvent"), terminationCount(0), shutdownCounter(0), prestarterThread(NULL), securityUpdateChecker(NULL), telemetryCollector(NULL), adminPanelConnector(NULL), adminPanelConnectorThread(NULL) /*******************/ { for (unsigned int i = 0; i < SERVER_KIT_MAX_SERVER_ENDPOINTS; i++) { serverFds[i] = -1; apiServerFds[i] = -1; } } ~WorkingObjects() { delete prestarterThread; delete adminPanelConnectorThread; delete adminPanelConnector; delete securityUpdateChecker; delete telemetryCollector; /*******************/ /*******************/ vector<ThreadWorkingObjects>::iterator it, end = threadWorkingObjects.end(); for (it = threadWorkingObjects.begin(); it != end; it++) { delete it->controller; delete it->serverKitContext; delete it->bgloop; } delete apiWorkingObjects.apiServer; delete apiWorkingObjects.serverKitContext; delete apiWorkingObjects.bgloop; } }; } // namespace Core } // namespace Passenger using namespace Passenger::Core; static WrapperRegistry::Registry *coreWrapperRegistry; static Schema *coreSchema; static ConfigKit::Store *coreConfig; static WorkingObjects *workingObjects; #include <Core/ConfigChange.cpp> /***** Core stuff *****/ static void waitForExitEvent(); static void cleanup(); static void deletePidFile(); static void abortLongRunningConnections(const ApplicationPool2::ProcessPtr &process); static void serverShutdownFinished(); static void controllerShutdownFinished(Controller *controller); static void apiServerShutdownFinished(Core::ApiServer::ApiServer *server); static void printInfoInThread(); static void initializePrivilegedWorkingObjects() { TRACE_POINT(); WorkingObjects *wo = workingObjects = new WorkingObjects(); Json::Value password = coreConfig->get("controller_secure_headers_password"); if (password.isString()) { wo->controllerSecureHeadersPassword = password.asString(); } else if (password.isObject()) { wo->controllerSecureHeadersPassword = strip(unsafeReadFile(password["path"].asString())); } } static void initializeSingleAppMode() { TRACE_POINT(); if (coreConfig->get("multi_app").asBool()) { P_NOTICE(SHORT_PROGRAM_NAME " core running in multi-application mode."); return; } WorkingObjects *wo = workingObjects; string appType, startupFile, appStartCommand; string appRoot = coreConfig->get("single_app_mode_app_root").asString(); if (!coreConfig->get("single_app_mode_app_type").isNull() && !coreConfig->get("single_app_mode_app_start_command").isNull()) { fprintf(stderr, "ERROR: it is not allowed for both --app-type and" " --app-start-command to be set.\n"); exit(1); } if (!coreConfig->get("single_app_mode_app_start_command").isNull()) { // The config specified that this is a generic app or a Kuria app. appStartCommand = coreConfig->get("single_app_mode_app_start_command").asString(); } else if (coreConfig->get("single_app_mode_app_type").isNull()) { // Autodetect whether this is generic app, Kuria app or auto-supported app. P_DEBUG("Autodetecting application type..."); AppTypeDetector::Detector detector(*coreWrapperRegistry); AppTypeDetector::Detector::Result detectorResult = detector.checkAppRoot(appRoot); if (!detectorResult.appStartCommand.empty()) { // This is a generic or Kuria app. appStartCommand = detectorResult.appStartCommand; } else { // This is an auto-supported app. if (coreConfig->get("single_app_mode_app_type").isNull()) { if (detectorResult.isNull()) { fprintf(stderr, "ERROR: unable to autodetect what kind of application " "lives in %s. Please specify information about the app using " "--app-type, --startup-file and --app-start-command, or specify a " "correct location to the application you want to serve.\n" "Type '" SHORT_PROGRAM_NAME " core --help' for more information.\n", appRoot.c_str()); exit(1); } appType = detectorResult.wrapperRegistryEntry->language; } else { appType = coreConfig->get("single_app_mode_app_type").asString(); } } } else { // This is an auto-supported app. appType = coreConfig->get("single_app_mode_app_type").asString(); } if (!appType.empty()) { if (coreConfig->get("single_app_mode_startup_file").isNull()) { const WrapperRegistry::Entry &entry = coreWrapperRegistry->lookup(appType); if (entry.defaultStartupFiles.empty()) { startupFile = appRoot + "/"; } else { startupFile = appRoot + "/" + entry.defaultStartupFiles[0]; } } else { startupFile = coreConfig->get("single_app_mode_startup_file").asString(); } if (!fileExists(startupFile)) { fprintf(stderr, "ERROR: unable to find expected startup file %s." " Please specify its correct path with --startup-file.\n", startupFile.c_str()); exit(1); } } wo->singleAppModeConfig["app_root"] = appRoot; P_NOTICE(SHORT_PROGRAM_NAME " core running in single-application mode."); P_NOTICE("Serving app : " << appRoot); if (!appType.empty()) { P_NOTICE("App type : " << appType); P_NOTICE("App startup file : " << startupFile); wo->singleAppModeConfig["app_type"] = appType; wo->singleAppModeConfig["startup_file"] = startupFile; } else { P_NOTICE("App start command: " << appStartCommand); wo->singleAppModeConfig["app_start_command"] = appStartCommand; } } static void setUlimits() { TRACE_POINT(); unsigned int number = coreConfig->get("file_descriptor_ulimit").asUInt(); if (number != 0) { struct rlimit limit; int ret; limit.rlim_cur = number; limit.rlim_max = number; do { ret = setrlimit(RLIMIT_NOFILE, &limit); } while (ret == -1 && errno == EINTR); if (ret == -1) { int e = errno; P_ERROR("Unable to set file descriptor ulimit to " << number << ": " << strerror(e) << " (errno=" << e << ")"); } } } static void makeFileWorldReadableAndWritable(const string &path) { int ret; do { ret = chmod(path.c_str(), parseModeString("u=rw,g=rw,o=rw")); } while (ret == -1 && errno == EINTR); } #ifdef USE_SELINUX // Set next socket context to *:system_r:passenger_instance_httpd_socket_t. // Note that this only sets the context of the socket file descriptor, // not the socket file on the filesystem. This is why we need selinuxRelabelFile(). static void setSelinuxSocketContext() { security_context_t currentCon; string newCon; int e; if (getcon(¤tCon) == -1) { e = errno; P_DEBUG("Unable to obtain SELinux context: " << strerror(e) << " (errno=" << e << ")"); return; } P_DEBUG("Current SELinux process context: " << currentCon); if (strstr(currentCon, ":unconfined_r:unconfined_t:") == NULL) { goto cleanup; } newCon = replaceString(currentCon, ":unconfined_r:unconfined_t:", ":object_r:passenger_instance_httpd_socket_t:"); if (setsockcreatecon((security_context_t) newCon.c_str()) == -1) { e = errno; P_WARN("Cannot set SELinux socket context to " << newCon << ": " << strerror(e) << " (errno=" << e << ")"); goto cleanup; } cleanup: freecon(currentCon); } static void resetSelinuxSocketContext() { setsockcreatecon(NULL); } static void selinuxRelabelFile(const string &path, const char *newLabel) { security_context_t currentCon; string newCon; int e; if (getfilecon(path.c_str(), ¤tCon) == -1) { e = errno; P_DEBUG("Unable to obtain SELinux context for file " << path <<": " << strerror(e) << " (errno=" << e << ")"); return; } P_DEBUG("SELinux context for " << path << ": " << currentCon); if (strstr(currentCon, ":object_r:passenger_instance_content_t:") == NULL) { goto cleanup; } newCon = replaceString(currentCon, ":object_r:passenger_instance_content_t:", StaticString(":object_r:") + newLabel + ":"); P_DEBUG("Relabeling " << path << " to: " << newCon); if (setfilecon(path.c_str(), (security_context_t) newCon.c_str()) == -1) { e = errno; P_WARN("Cannot set SELinux context for " << path << " to " << newCon << ": " << strerror(e) << " (errno=" << e << ")"); } cleanup: freecon(currentCon); } #endif static void startListening() { TRACE_POINT(); WorkingObjects *wo = workingObjects; const Json::Value addresses = coreConfig->get("controller_addresses"); const Json::Value apiAddresses = coreConfig->get("api_server_addresses"); Json::Value::const_iterator it; unsigned int i; #ifdef USE_SELINUX // Set SELinux context on the first socket that we create // so that the web server can access it. setSelinuxSocketContext(); #endif for (it = addresses.begin(), i = 0; it != addresses.end(); it++, i++) { wo->serverFds[i] = createServer(it->asString(), coreConfig->get("controller_socket_backlog").asUInt(), true, __FILE__, __LINE__); #ifdef USE_SELINUX resetSelinuxSocketContext(); if (i == 0 && getSocketAddressType(it->asString()) == SAT_UNIX) { // setSelinuxSocketContext() sets the context of the // socket file descriptor but not the file on the filesystem. // So we relabel the socket file here. selinuxRelabelFile(parseUnixSocketAddress(it->asString()), "passenger_instance_httpd_socket_t"); } #endif P_LOG_FILE_DESCRIPTOR_PURPOSE(wo->serverFds[i], "Server address: " << it->asString()); if (getSocketAddressType(it->asString()) == SAT_UNIX) { makeFileWorldReadableAndWritable(parseUnixSocketAddress(it->asString())); } } for (it = apiAddresses.begin(), i = 0; it != apiAddresses.end(); it++, i++) { wo->apiServerFds[i] = createServer(it->asString(), 0, true, __FILE__, __LINE__); P_LOG_FILE_DESCRIPTOR_PURPOSE(wo->apiServerFds[i], "ApiServer address: " << it->asString()); if (getSocketAddressType(it->asString()) == SAT_UNIX) { makeFileWorldReadableAndWritable(parseUnixSocketAddress(it->asString())); } } } static void createPidFile() { TRACE_POINT(); Json::Value pidFile = coreConfig->get("pid_file"); if (!pidFile.isNull()) { char pidStr[32]; snprintf(pidStr, sizeof(pidStr), "%lld", (long long) getpid()); int fd = syscalls::open(pidFile.asCString(), O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd == -1) { int e = errno; throw FileSystemException("Cannot create PID file " + pidFile.asString(), e, pidFile.asString()); } UPDATE_TRACE_POINT(); FdGuard guard(fd, __FILE__, __LINE__); writeExact(fd, pidStr, strlen(pidStr)); } } static void lowerPrivilege() { TRACE_POINT(); } static void printInfo(EV_P_ struct ev_signal *watcher, int revents) { oxt::thread(printInfoInThread, "Information printer"); } static void inspectControllerStateAsJson(Controller *controller, string *result) { *result = controller->inspectStateAsJson().toStyledString(); } static void inspectControllerConfigAsJson(Controller *controller, string *result) { *result = controller->inspectConfig().toStyledString(); } static void getMbufStats(struct MemoryKit::mbuf_pool *input, struct MemoryKit::mbuf_pool *result) { *result = *input; } static void printInfoInThread() { TRACE_POINT(); WorkingObjects *wo = workingObjects; unsigned int i; cerr << "### Backtraces\n"; cerr << "\n" << oxt::thread::all_backtraces(); cerr << "\n"; cerr.flush(); for (i = 0; i < wo->threadWorkingObjects.size(); i++) { ThreadWorkingObjects *two = &wo->threadWorkingObjects[i]; string json; cerr << "### Request handler state (thread " << (i + 1) << ")\n"; two->bgloop->safe->runSync(boost::bind(inspectControllerStateAsJson, two->controller, &json)); cerr << json; cerr << "\n"; cerr.flush(); } for (i = 0; i < wo->threadWorkingObjects.size(); i++) { ThreadWorkingObjects *two = &wo->threadWorkingObjects[i]; string json; cerr << "### Request handler config (thread " << (i + 1) << ")\n"; two->bgloop->safe->runSync(boost::bind(inspectControllerConfigAsJson, two->controller, &json)); cerr << json; cerr << "\n"; cerr.flush(); } struct MemoryKit::mbuf_pool stats; cerr << "### mbuf stats\n\n"; wo->threadWorkingObjects[0].bgloop->safe->runSync(boost::bind(getMbufStats, &wo->threadWorkingObjects[0].serverKitContext->mbuf_pool, &stats)); cerr << "nfree_mbuf_blockq : " << stats.nfree_mbuf_blockq << "\n"; cerr << "nactive_mbuf_blockq : " << stats.nactive_mbuf_blockq << "\n"; cerr << "mbuf_block_chunk_size: " << stats.mbuf_block_chunk_size << "\n"; cerr << "\n"; cerr.flush(); cerr << "### Pool state\n"; cerr << "\n" << wo->appPool->inspect(); cerr << "\n"; cerr.flush(); } static void dumpOxtBacktracesOnCrash(void *userData) { cerr << oxt::thread::all_backtraces(); cerr.flush(); } static void dumpControllerStatesOnCrash(void *userData) { WorkingObjects *wo = workingObjects; unsigned int i; for (i = 0; i < wo->threadWorkingObjects.size(); i++) { ThreadWorkingObjects *two = &wo->threadWorkingObjects[i]; cerr << "####### Controller state (thread " << (i + 1) << ") #######\n"; cerr << two->controller->inspectStateAsJson(); cerr << "\n\n"; cerr.flush(); } } static void dumpControllerConfigsOnCrash(void *userData) { WorkingObjects *wo = workingObjects; unsigned int i; for (i = 0; i < wo->threadWorkingObjects.size(); i++) { ThreadWorkingObjects *two = &wo->threadWorkingObjects[i]; cerr << "####### Controller config (thread " << (i + 1) << ") #######\n"; cerr << two->controller->inspectConfig(); cerr << "\n\n"; cerr.flush(); } } static void dumpPoolStateOnCrash(void *userData) { WorkingObjects *wo = workingObjects; cerr << "####### Pool state (simple) #######\n"; // Do not lock, the crash may occur within the pool. Pool::InspectOptions options(Pool::InspectOptions::makeAuthorized()); options.verbose = true; cerr << wo->appPool->inspect(options, false); cerr << "\n\n"; cerr.flush(); cerr << "####### Pool state (XML) #######\n"; Pool::ToXmlOptions options2(Pool::ToXmlOptions::makeAuthorized()); options2.secrets = true; cerr << wo->appPool->toXml(options2, false); cerr << "\n\n"; cerr.flush(); } static void dumpMbufStatsOnCrash(void *userData) { WorkingObjects *wo = workingObjects; cerr << "nfree_mbuf_blockq : " << wo->threadWorkingObjects[0].serverKitContext->mbuf_pool.nfree_mbuf_blockq << "\n"; cerr << "nactive_mbuf_blockq: " << wo->threadWorkingObjects[0].serverKitContext->mbuf_pool.nactive_mbuf_blockq << "\n"; cerr << "mbuf_block_chunk_size: " << wo->threadWorkingObjects[0].serverKitContext->mbuf_pool.mbuf_block_chunk_size << "\n"; cerr << "\n"; cerr.flush(); } static void onTerminationSignal(EV_P_ struct ev_signal *watcher, int revents) { WorkingObjects *wo = workingObjects; // Start output after '^C' printf("\n"); wo->terminationCount++; if (wo->terminationCount < 3) { P_NOTICE("Signal received. Gracefully shutting down... (send signal " << (3 - wo->terminationCount) << " more time(s) to force shutdown)"); workingObjects->exitEvent.notify(); } else { P_NOTICE("Signal received. Forcing shutdown."); _exit(2); } } static void initializeCurl() { TRACE_POINT(); CURLcode code = curl_global_init(CURL_GLOBAL_ALL); // Initializes underlying TLS stack if (code != CURLE_OK) { P_CRITICAL("Could not initialize libcurl: " << curl_easy_strerror(code)); exit(1); } } static void initializeNonPrivilegedWorkingObjects() { TRACE_POINT(); WorkingObjects *wo = workingObjects; const Json::Value addresses = coreConfig->get("controller_addresses"); const Json::Value apiAddresses = coreConfig->get("api_server_addresses"); setenv("SERVER_SOFTWARE", coreConfig->get("server_software").asCString(), 1); wo->resourceLocator = ResourceLocator(coreConfig->get("passenger_root").asString()); wo->randomGenerator = boost::make_shared<RandomGenerator>(); // Check whether /dev/urandom is actually random. // https://code.google.com/p/phusion-passenger/issues/detail?id=516 if (wo->randomGenerator->generateByteString(16) == wo->randomGenerator->generateByteString(16)) { throw RuntimeException("Your random number device, /dev/urandom, appears to be broken. " "It doesn't seem to be returning random data. Please fix this."); } UPDATE_TRACE_POINT(); wo->spawningKitContext = boost::make_shared<SpawningKit::Context>( wo->spawningKitContextSchema); wo->spawningKitContext->resourceLocator = &wo->resourceLocator; wo->spawningKitContext->wrapperRegistry = coreWrapperRegistry; wo->spawningKitContext->randomGenerator = wo->randomGenerator; wo->spawningKitContext->integrationMode = coreConfig->get("integration_mode").asString(); wo->spawningKitContext->instanceDir = coreConfig->get("instance_dir").asString(); wo->spawningKitContext->spawnDir = coreConfig->get("spawn_dir").asString(); if (!wo->spawningKitContext->instanceDir.empty()) { wo->spawningKitContext->instanceDir = absolutizePath( wo->spawningKitContext->instanceDir); } wo->spawningKitContext->finalize(); UPDATE_TRACE_POINT(); wo->appPoolContext = boost::make_shared<ApplicationPool2::Context>(); wo->appPoolContext->spawningKitFactory = boost::make_shared<SpawningKit::Factory>( wo->spawningKitContext.get()); wo->appPoolContext->agentConfig = coreConfig->inspectEffectiveValues(); wo->appPoolContext->finalize(); wo->appPool = boost::make_shared<Pool>(wo->appPoolContext.get()); wo->appPool->initialize(); wo->appPool->setMax(coreConfig->get("max_pool_size").asInt()); wo->appPool->setMaxIdleTime(coreConfig->get("pool_idle_time").asInt() * 1000000ULL); wo->appPool->enableSelfChecking(coreConfig->get("pool_selfchecks").asBool()); wo->appPool->abortLongRunningConnectionsCallback = abortLongRunningConnections; UPDATE_TRACE_POINT(); unsigned int nthreads = coreConfig->get("controller_threads").asUInt(); BackgroundEventLoop *firstLoop = NULL; // Avoid compiler warning wo->threadWorkingObjects.reserve(nthreads); for (unsigned int i = 0; i < nthreads; i++) { UPDATE_TRACE_POINT(); ThreadWorkingObjects two; Json::Value contextConfig = coreConfig->inspectEffectiveValues(); contextConfig["secure_mode_password"] = wo->controllerSecureHeadersPassword; Json::Value controllerConfig = coreConfig->inspectEffectiveValues(); controllerConfig["thread_number"] = i + 1; if (i == 0) { two.bgloop = firstLoop = new BackgroundEventLoop(true, true); } else { two.bgloop = new BackgroundEventLoop(true, true); } UPDATE_TRACE_POINT(); two.serverKitContext = new ServerKit::Context( coreSchema->controllerServerKit.schema, contextConfig, coreSchema->controllerServerKit.translator); two.serverKitContext->libev = two.bgloop->safe; two.serverKitContext->libuv = two.bgloop->libuv_loop; two.serverKitContext->initialize(); UPDATE_TRACE_POINT(); two.controller = new Core::Controller(two.serverKitContext, coreSchema->controller.schema, controllerConfig, coreSchema->controller.translator, &coreSchema->controllerSingleAppMode.schema, &wo->singleAppModeConfig, coreSchema->controllerSingleAppMode.translator); two.controller->resourceLocator = &wo->resourceLocator; two.controller->wrapperRegistry = coreWrapperRegistry; two.controller->appPool = wo->appPool; two.controller->shutdownFinishCallback = controllerShutdownFinished; two.controller->initialize(); wo->shutdownCounter.fetch_add(1, boost::memory_order_relaxed); wo->threadWorkingObjects.push_back(two); } UPDATE_TRACE_POINT(); ev_signal_init(&wo->sigquitWatcher, printInfo, SIGQUIT); ev_signal_start(firstLoop->libev_loop, &wo->sigquitWatcher); ev_signal_init(&wo->sigintWatcher, onTerminationSignal, SIGINT); ev_signal_start(firstLoop->libev_loop, &wo->sigintWatcher); ev_signal_init(&wo->sigtermWatcher, onTerminationSignal, SIGTERM); ev_signal_start(firstLoop->libev_loop, &wo->sigtermWatcher); UPDATE_TRACE_POINT(); if (!apiAddresses.empty()) { UPDATE_TRACE_POINT(); ApiWorkingObjects *awo = &wo->apiWorkingObjects; Json::Value contextConfig = coreConfig->inspectEffectiveValues(); awo->bgloop = new BackgroundEventLoop(true, true); awo->serverKitContext = new ServerKit::Context( coreSchema->apiServerKit.schema, contextConfig, coreSchema->apiServerKit.translator); awo->serverKitContext->libev = awo->bgloop->safe; awo->serverKitContext->libuv = awo->bgloop->libuv_loop; awo->serverKitContext->initialize(); UPDATE_TRACE_POINT(); awo->apiServer = new Core::ApiServer::ApiServer(awo->serverKitContext, coreSchema->apiServer.schema, coreConfig->inspectEffectiveValues(), coreSchema->apiServer.translator); awo->apiServer->controllers.reserve(wo->threadWorkingObjects.size()); for (unsigned int i = 0; i < wo->threadWorkingObjects.size(); i++) { awo->apiServer->controllers.push_back( wo->threadWorkingObjects[i].controller); } awo->apiServer->appPool = wo->appPool; awo->apiServer->exitEvent = &wo->exitEvent; awo->apiServer->shutdownFinishCallback = apiServerShutdownFinished; awo->apiServer->initialize(); wo->shutdownCounter.fetch_add(1, boost::memory_order_relaxed); } UPDATE_TRACE_POINT(); /* We do not delete Unix domain socket files at shutdown because * that can cause a race condition if the user tries to start another * server with the same addresses at the same time. The new server * would then delete the socket and replace it with its own, * while the old server would delete the file yet again shortly after. * This is especially noticeable on systems that heavily swap. */ for (unsigned int i = 0; i < addresses.size(); i++) { if (nthreads == 1) { ThreadWorkingObjects *two = &wo->threadWorkingObjects[0]; two->controller->listen(wo->serverFds[i]); } else { wo->loadBalancer.listen(wo->serverFds[i]); } } for (unsigned int i = 0; i < nthreads; i++) { ThreadWorkingObjects *two = &wo->threadWorkingObjects[i]; two->controller->createSpareClients(); } if (nthreads > 1) { wo->loadBalancer.servers.reserve(nthreads); for (unsigned int i = 0; i < nthreads; i++) { ThreadWorkingObjects *two = &wo->threadWorkingObjects[i]; wo->loadBalancer.servers.push_back(two->controller); } } for (unsigned int i = 0; i < apiAddresses.size(); i++) { wo->apiWorkingObjects.apiServer->listen(wo->apiServerFds[i]); } } static void initializeSecurityUpdateChecker() { TRACE_POINT(); Json::Value config = coreConfig->inspectEffectiveValues(); // nginx / apache / standalone string serverIdentifier = coreConfig->get("integration_mode").asString(); // nginx / builtin if (!coreConfig->get("standalone_engine").isNull()) { serverIdentifier.append(" "); serverIdentifier.append(coreConfig->get("standalone_engine").asString()); } if (coreConfig->get("server_software").asString().find(FLYING_PASSENGER_NAME) != string::npos) { serverIdentifier.append(" flying"); } config["server_identifier"] = serverIdentifier; SecurityUpdateChecker *checker = new SecurityUpdateChecker( coreSchema->securityUpdateChecker.schema, config, coreSchema->securityUpdateChecker.translator); workingObjects->securityUpdateChecker = checker; checker->resourceLocator = &workingObjects->resourceLocator; checker->initialize(); checker->start(); } static void initializeTelemetryCollector() { TRACE_POINT(); WorkingObjects &wo = *workingObjects; Json::Value config = coreConfig->inspectEffectiveValues(); TelemetryCollector *collector = new TelemetryCollector( coreSchema->telemetryCollector.schema, coreConfig->inspectEffectiveValues(), coreSchema->telemetryCollector.translator); wo.telemetryCollector = collector; for (unsigned int i = 0; i < wo.threadWorkingObjects.size(); i++) { ThreadWorkingObjects *two = &wo.threadWorkingObjects[i]; collector->controllers.push_back(two->controller); } collector->initialize(); collector->start(); wo.shutdownCounter.fetch_add(1, boost::memory_order_relaxed); } static void runAdminPanelConnector(AdminPanelConnector *connector) { connector->run(); P_DEBUG("Admin panel connector shutdown finished"); serverShutdownFinished(); } static void initializeAdminPanelConnector() { TRACE_POINT(); WorkingObjects &wo = *workingObjects; if (coreConfig->get("admin_panel_url").empty()) { return; } Json::Value config = coreConfig->inspectEffectiveValues(); config["log_prefix"] = "AdminPanelConnector: "; config["ruby"] = config["default_ruby"]; P_NOTICE("Initialize connection with " << PROGRAM_NAME " admin panel at " << config["admin_panel_url"].asString()); AdminPanelConnector *connector = new Core::AdminPanelConnector( coreSchema->adminPanelConnector.schema, config, coreSchema->adminPanelConnector.translator); connector->resourceLocator = &wo.resourceLocator; connector->appPool = wo.appPool; connector->configGetter = inspectConfig; for (unsigned int i = 0; i < wo.threadWorkingObjects.size(); i++) { ThreadWorkingObjects *two = &wo.threadWorkingObjects[i]; connector->controllers.push_back(two->controller); } connector->initialize(); wo.shutdownCounter.fetch_add(1, boost::memory_order_relaxed); wo.adminPanelConnector = connector; wo.adminPanelConnectorThread = new oxt::thread( boost::bind(runAdminPanelConnector, connector), "Admin panel connector main loop", 128 * 1024); } static void prestartWebApps() { TRACE_POINT(); WorkingObjects *wo = workingObjects; vector<string> prestartURLs; const Json::Value jPrestartURLs = coreConfig->get("prestart_urls"); Json::Value::const_iterator it, end = jPrestartURLs.end(); prestartURLs.reserve(jPrestartURLs.size()); for (it = jPrestartURLs.begin(); it != end; it++) { prestartURLs.push_back(it->asString()); } boost::function<void ()> func = boost::bind(prestartWebApps, wo->resourceLocator, coreConfig->get("default_ruby").asString(), prestartURLs ); wo->prestarterThread = new oxt::thread( boost::bind(runAndPrintExceptions, func, true) ); } /* * Emit a warning (log) if the Passenger root dir (and/or its parents) can be modified by non-root users * while Passenger was run as root (because non-root users can then tamper with something running as root). * It's just a convenience warning, so check failures are only logged at the debug level. * * N.B. we limit our checking to use cases that can easily (gotcha) lead to this vulnerable setup, such as * installing Passenger via gem or tarball in a user dir, and then running it as root (for example by installing * it as nginx or apache module). We do not check the entire installation file/dir structure for whether users have * changed owner or access rights. */ static void warnIfPassengerRootVulnerable() { TRACE_POINT(); if (geteuid() != 0) { return; // Passenger is not root, so no escalation. } string root = workingObjects->resourceLocator.getInstallSpec(); vector<string> errors, checkErrors; if (isPathProbablySecureForRootUse(root, errors, checkErrors)) { if (!checkErrors.empty()) { string message = "WARNING: unable to perform privilege escalation vulnerability detection:\n"; foreach (string line, checkErrors) { message.append("\n - " + line); } P_WARN(message); } } else { string message = "WARNING: potential privilege escalation vulnerability detected. " \ PROGRAM_NAME " is running as root, and part(s) of the " SHORT_PROGRAM_NAME " root path (" + root + ") can be changed by non-root user(s):\n"; foreach (string line, errors) { message.append("\n - " + line); } foreach (string line, checkErrors) { message.append("\n - " + line); } message.append("\n\nPlease either fix up the permissions for the insecure paths, or install " SHORT_PROGRAM_NAME " in a different location that can only be modified by root."); P_WARN(message); } } static void reportInitializationInfo() { TRACE_POINT(); if (feedbackFdAvailable()) { P_NOTICE(SHORT_PROGRAM_NAME " core online, PID " << getpid()); writeArrayMessage(FEEDBACK_FD, "initialized", NULL); } else { const Json::Value addresses = coreConfig->get("controller_addresses"); const Json::Value apiAddresses = coreConfig->get("api_server_addresses"); Json::Value::const_iterator it; P_NOTICE(SHORT_PROGRAM_NAME " core online, PID " << getpid() << ", listening on " << addresses.size() << " socket(s):"); for (it = addresses.begin(); it != addresses.end(); it++) { string address = it->asString(); if (startsWith(address, "tcp://")) { address.erase(0, sizeof("tcp://") - 1); address.insert(0, "http://"); address.append("/"); } P_NOTICE(" * " << address); } if (!apiAddresses.empty()) { P_NOTICE("API server listening on " << apiAddresses.size() << " socket(s):"); for (it = apiAddresses.begin(); it != apiAddresses.end(); it++) { string address = it->asString(); if (startsWith(address, "tcp://")) { address.erase(0, sizeof("tcp://") - 1); address.insert(0, "http://"); address.append("/"); } P_NOTICE(" * " << address); } } } } static void initializeAbortHandlerCustomerDiagnostics() { if (!Agent::Fundamentals::abortHandlerInstalled()) { return; } Agent::Fundamentals::AbortHandlerConfig::DiagnosticsDumper *diagnosticsDumpers = &Agent::Fundamentals::context->abortHandlerConfig.diagnosticsDumpers[0]; diagnosticsDumpers[0].name = "OXT backtraces"; diagnosticsDumpers[0].logFileName = "backtrace_oxt.log"; diagnosticsDumpers[0].func = dumpOxtBacktracesOnCrash; diagnosticsDumpers[1].name = "controller states"; diagnosticsDumpers[1].logFileName = "controller_states.log"; diagnosticsDumpers[1].func = dumpControllerStatesOnCrash; diagnosticsDumpers[2].name = "controller configs"; diagnosticsDumpers[2].logFileName = "controller_configs.log"; diagnosticsDumpers[2].func = dumpControllerConfigsOnCrash; diagnosticsDumpers[3].name = "pool state"; diagnosticsDumpers[3].logFileName = "pool.log"; diagnosticsDumpers[3].func = dumpPoolStateOnCrash; diagnosticsDumpers[4].name = "mbuf statistics"; diagnosticsDumpers[4].logFileName = "mbufs.log"; diagnosticsDumpers[4].func = dumpMbufStatsOnCrash; Agent::Fundamentals::abortHandlerConfigChanged(); } static void uninstallAbortHandlerCustomDiagnostics() { if (!Agent::Fundamentals::abortHandlerInstalled()) { return; } for (unsigned int i = 0; i < Agent::Fundamentals::AbortHandlerConfig::MAX_DIAGNOSTICS_DUMPERS; i++) { Agent::Fundamentals::context->abortHandlerConfig.diagnosticsDumpers[i].func = NULL; } Agent::Fundamentals::abortHandlerConfigChanged(); } static void mainLoop() { TRACE_POINT(); WorkingObjects *wo = workingObjects; #ifdef SUPPORTS_PER_THREAD_CPU_AFFINITY unsigned int maxCpus = boost::thread::hardware_concurrency(); bool cpuAffine = coreConfig->get("controller_cpu_affine").asBool() && maxCpus <= CPU_SETSIZE; #endif for (unsigned int i = 0; i < wo->threadWorkingObjects.size(); i++) { ThreadWorkingObjects *two = &wo->threadWorkingObjects[i]; two->bgloop->start("Main event loop: thread " + toString(i + 1), 0); #ifdef SUPPORTS_PER_THREAD_CPU_AFFINITY if (cpuAffine) { cpu_set_t cpus; int result; CPU_ZERO(&cpus); CPU_SET(i % maxCpus, &cpus); P_DEBUG("Setting CPU affinity of core thread " << (i + 1) << " to CPU " << (i % maxCpus + 1)); result = pthread_setaffinity_np(two->bgloop->getNativeHandle(), maxCpus, &cpus); if (result != 0) { P_WARN("Cannot set CPU affinity on core thread " << (i + 1) << ": " << strerror(result) << " (errno=" << result << ")"); } } #endif } if (wo->apiWorkingObjects.apiServer != NULL) { wo->apiWorkingObjects.bgloop->start("API event loop", 0); } if (wo->threadWorkingObjects.size() > 1) { wo->loadBalancer.start(); } waitForExitEvent(); } static void abortLongRunningConnectionsOnController(Core::Controller *controller, string gupid) { controller->disconnectLongRunningConnections(gupid); } static void abortLongRunningConnections(const ApplicationPool2::ProcessPtr &process) { // We are inside the ApplicationPool lock. Be very careful here. WorkingObjects *wo = workingObjects; P_NOTICE("Checking whether to disconnect long-running connections for process " << process->getPid() << ", application " << process->getGroup()->getName()); for (unsigned int i = 0; i < wo->threadWorkingObjects.size(); i++) { wo->threadWorkingObjects[i].bgloop->safe->runLater( boost::bind(abortLongRunningConnectionsOnController, wo->threadWorkingObjects[i].controller, process->getGupid().toString())); } } static void shutdownController(ThreadWorkingObjects *two) { two->controller->shutdown(); } static void shutdownApiServer() { workingObjects->apiWorkingObjects.apiServer->shutdown(); } static void serverShutdownFinished() { unsigned int i = workingObjects->shutdownCounter.fetch_sub(1, boost::memory_order_release); P_DEBUG("Shutdown counter = " << (i - 1)); if (i == 1) { boost::atomic_thread_fence(boost::memory_order_acquire); workingObjects->allClientsDisconnectedEvent.notify(); } } static void controllerShutdownFinished(Core::Controller *controller) { P_DEBUG("Controller " << controller->getThreadNumber() << " shutdown finished"); serverShutdownFinished(); } static void apiServerShutdownFinished(Core::ApiServer::ApiServer *server) { P_DEBUG("API server shutdown finished"); serverShutdownFinished(); } static void telemetryCollectorAsyncShutdownThreadMain() { WorkingObjects *wo = workingObjects; wo->telemetryCollector->stop(); serverShutdownFinished(); } static void asyncShutdownTelemetryCollector() { oxt::thread(telemetryCollectorAsyncShutdownThreadMain, "Telemetry collector shutdown", 512 * 1024); } /* Wait until the watchdog closes the feedback fd (meaning it * was killed) or until we receive an exit message. */ static void waitForExitEvent() { boost::this_thread::disable_syscall_interruption dsi; WorkingObjects *wo = workingObjects; fd_set fds; int largestFd = -1; FD_ZERO(&fds); if (feedbackFdAvailable()) { FD_SET(FEEDBACK_FD, &fds); largestFd = std::max(largestFd, FEEDBACK_FD); } FD_SET(wo->exitEvent.fd(), &fds); largestFd = std::max(largestFd, wo->exitEvent.fd()); TRACE_POINT(); if (syscalls::select(largestFd + 1, &fds, NULL, NULL, NULL) == -1) { int e = errno; uninstallAbortHandlerCustomDiagnostics(); throw SystemException("select() failed", e); } if (FD_ISSET(FEEDBACK_FD, &fds)) { UPDATE_TRACE_POINT(); /* If the watchdog has been killed then we'll kill all descendant * processes and exit. There's no point in keeping the server agent * running because we can't detect when the web server exits, * and because this server agent doesn't own the instance * directory. As soon as passenger-status is run, the instance * directory will be cleaned up, making the server inaccessible. */ P_WARN("Watchdog seems to be killed; forcing shutdown of all subprocesses"); // We send a SIGTERM first to allow processes to gracefully shut down. syscalls::killpg(getpgrp(), SIGTERM); usleep(500000); syscalls::killpg(getpgrp(), SIGKILL); _exit(2); // In case killpg() fails. } else { UPDATE_TRACE_POINT(); /* We received an exit command. */ P_NOTICE("Received command to shutdown gracefully. " "Waiting until all clients have disconnected..."); wo->appPool->prepareForShutdown(); for (unsigned i = 0; i < wo->threadWorkingObjects.size(); i++) { ThreadWorkingObjects *two = &wo->threadWorkingObjects[i]; two->bgloop->safe->runLater(boost::bind(shutdownController, two)); } if (wo->threadWorkingObjects.size() > 1) { wo->loadBalancer.shutdown(); } if (wo->apiWorkingObjects.apiServer != NULL) { wo->apiWorkingObjects.bgloop->safe->runLater(shutdownApiServer); } if (wo->telemetryCollector != NULL) { asyncShutdownTelemetryCollector(); } if (wo->adminPanelConnector != NULL) { wo->adminPanelConnector->asyncShutdown(); } UPDATE_TRACE_POINT(); FD_ZERO(&fds); FD_SET(wo->allClientsDisconnectedEvent.fd(), &fds); if (syscalls::select(wo->allClientsDisconnectedEvent.fd() + 1, &fds, NULL, NULL, NULL) == -1) { int e = errno; uninstallAbortHandlerCustomDiagnostics(); throw SystemException("select() failed", e); } P_INFO("All clients have now disconnected. Proceeding with graceful shutdown"); } } static void cleanup() { TRACE_POINT(); WorkingObjects *wo = workingObjects; P_DEBUG("Shutting down " SHORT_PROGRAM_NAME " core..."); wo->appPool->destroy(); uninstallAbortHandlerCustomDiagnostics(); for (unsigned i = 0; i < wo->threadWorkingObjects.size(); i++) { ThreadWorkingObjects *two = &wo->threadWorkingObjects[i]; two->bgloop->stop(); } if (wo->apiWorkingObjects.apiServer != NULL) { wo->apiWorkingObjects.bgloop->stop(); } if (wo->telemetryCollector != NULL && !coreConfig->get("telemetry_collector_disabled").asBool()) { wo->telemetryCollector->runOneCycle(true); } wo->appPool.reset(); for (unsigned i = 0; i < wo->threadWorkingObjects.size(); i++) { ThreadWorkingObjects *two = &wo->threadWorkingObjects[i]; delete two->controller; two->controller = NULL; } if (wo->prestarterThread != NULL) { wo->prestarterThread->interrupt_and_join(); delete wo->prestarterThread; wo->prestarterThread = NULL; } for (unsigned int i = 0; i < SERVER_KIT_MAX_SERVER_ENDPOINTS; i++) { if (wo->serverFds[i] != -1) { close(wo->serverFds[i]); } if (wo->apiServerFds[i] != -1) { close(wo->apiServerFds[i]); } } deletePidFile(); delete workingObjects; workingObjects = NULL; P_NOTICE(SHORT_PROGRAM_NAME " core shutdown finished"); } static void deletePidFile() { TRACE_POINT(); Json::Value pidFile = coreConfig->get("pid_file"); if (!pidFile.isNull()) { syscalls::unlink(pidFile.asCString()); } } static int runCore() { TRACE_POINT(); P_NOTICE("Starting " SHORT_PROGRAM_NAME " core..."); try { UPDATE_TRACE_POINT(); initializePrivilegedWorkingObjects(); initializeSingleAppMode(); setUlimits(); startListening(); createPidFile(); lowerPrivilege(); initializeCurl(); initializeNonPrivilegedWorkingObjects(); initializeSecurityUpdateChecker(); initializeTelemetryCollector(); initializeAdminPanelConnector(); prestartWebApps(); UPDATE_TRACE_POINT(); warnIfPassengerRootVulnerable(); reportInitializationInfo(); initializeAbortHandlerCustomerDiagnostics(); mainLoop(); UPDATE_TRACE_POINT(); cleanup(); } catch (const tracable_exception &e) { // We intentionally don't call cleanup() in // order to avoid various destructor assertions. P_CRITICAL("ERROR: " << e.what() << "\n" << e.backtrace()); deletePidFile(); return 1; } catch (const std::runtime_error &e) { P_CRITICAL("ERROR: " << e.what()); deletePidFile(); return 1; } return 0; } /***** Entry point and command line argument parsing *****/ static void parseOptions(int argc, const char *argv[], ConfigKit::Store &config) { OptionParser p(coreUsage); Json::Value updates(Json::objectValue); int i = 2; while (i < argc) { if (parseCoreOption(argc, argv, i, updates)) { continue; } else if (p.isFlag(argv[i], 'h', "--help")) { coreUsage(); exit(0); } else { fprintf(stderr, "ERROR: unrecognized argument %s. Please type " "'%s core --help' for usage.\n", argv[i], argv[0]); exit(1); } } if (!updates.empty()) { vector<ConfigKit::Error> errors; if (!config.update(updates, errors)) { P_BUG("Unable to set initial configuration: " << ConfigKit::toString(errors) << "\n" "Raw initial configuration: " << updates.toStyledString()); } } } static void loggingKitPreInitFunc(Json::Value &loggingKitInitialConfig) { loggingKitInitialConfig = manipulateLoggingKitConfig(*coreConfig, loggingKitInitialConfig); } int coreMain(int argc, char *argv[]) { int ret; coreWrapperRegistry = new WrapperRegistry::Registry(); coreWrapperRegistry->finalize(); coreSchema = new Schema(coreWrapperRegistry); coreConfig = new ConfigKit::Store(*coreSchema); initializeAgent(argc, &argv, SHORT_PROGRAM_NAME " core", *coreConfig, coreSchema->loggingKit.translator, parseOptions, loggingKitPreInitFunc, 2); #if !BOOST_OS_MACOS restoreOomScore(coreConfig->get("oom_score").asString()); #endif ret = runCore(); shutdownAgent(coreSchema, coreConfig); delete coreWrapperRegistry; return ret; } Controller.h 0000644 00000041154 14756456557 0007075 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2011-2018 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef _PASSENGER_CORE_CONTROLLER_H_ #define _PASSENGER_CORE_CONTROLLER_H_ //#define DEBUG_CC_EVENT_LOOP_BLOCKING #define CC_BENCHMARK_POINT(client, req, value) \ do { \ if (OXT_UNLIKELY(mainConfig.benchmarkMode == value)) { \ writeBenchmarkResponse(&client, &req); \ return; \ } \ } while (false) #include <boost/shared_ptr.hpp> #include <boost/make_shared.hpp> #include <boost/cstdint.hpp> #include <oxt/macros.hpp> #include <ev++.h> #include <ostream> #if defined(__GLIBCXX__) || defined(__APPLE__) #include <cxxabi.h> #define CXX_ABI_API_AVAILABLE #endif #include <sys/types.h> #include <sys/uio.h> #include <utility> #include <typeinfo> #include <cstdio> #include <cstdlib> #include <cstddef> #include <cassert> #include <cctype> #include <LoggingKit/LoggingKit.h> #include <IOTools/MessageSerialization.h> #include <Constants.h> #include <ConfigKit/ConfigKit.h> #include <ServerKit/Errors.h> #include <ServerKit/HttpServer.h> #include <ServerKit/HttpHeaderParser.h> #include <MemoryKit/palloc.h> #include <DataStructures/LString.h> #include <DataStructures/StringKeyTable.h> #include <WrapperRegistry/Registry.h> #include <StaticString.h> #include <Utils.h> #include <StrIntTools/StrIntUtils.h> #include <IOTools/IOUtils.h> #include <JsonTools/JsonUtils.h> #include <Utils/HttpConstants.h> #include <Utils/Timer.h> #include <Core/Controller/Config.h> #include <Core/Controller/Client.h> #include <Core/Controller/AppResponse.h> #include <Core/Controller/TurboCaching.h> namespace Passenger { using namespace std; using namespace boost; using namespace oxt; using namespace ApplicationPool2; namespace ServerKit { extern const HashedStaticString HTTP_COOKIE; extern const HashedStaticString HTTP_SET_COOKIE; } namespace Core { class Controller: public ServerKit::HttpServer<Controller, Client> { private: typedef ServerKit::HttpServer<Controller, Client> ParentClass; typedef ServerKit::Channel Channel; typedef ServerKit::FdSinkChannel FdSinkChannel; typedef ServerKit::FdSourceChannel FdSourceChannel; typedef ServerKit::FileBufferedChannel FileBufferedChannel; typedef ServerKit::FileBufferedFdSinkChannel FileBufferedFdSinkChannel; // If you change this value, make sure that Request::sessionCheckoutTry // has enough bits. static const unsigned int MAX_SESSION_CHECKOUT_TRY = 10; ControllerMainConfig mainConfig; ControllerRequestConfigPtr requestConfig; StringKeyTable< boost::shared_ptr<Options> > poolOptionsCache; HashedStaticString PASSENGER_APP_GROUP_NAME; HashedStaticString PASSENGER_ENV_VARS; HashedStaticString PASSENGER_MAX_REQUESTS; HashedStaticString PASSENGER_SHOW_VERSION_IN_HEADER; HashedStaticString PASSENGER_STICKY_SESSIONS; HashedStaticString PASSENGER_STICKY_SESSIONS_COOKIE_NAME; HashedStaticString PASSENGER_STICKY_SESSIONS_COOKIE_ATTRIBUTES; HashedStaticString PASSENGER_REQUEST_OOB_WORK; HashedStaticString REMOTE_ADDR; HashedStaticString REMOTE_PORT; HashedStaticString REMOTE_USER; HashedStaticString FLAGS; HashedStaticString HTTP_COOKIE; HashedStaticString HTTP_DATE; HashedStaticString HTTP_HOST; HashedStaticString HTTP_CONTENT_LENGTH; HashedStaticString HTTP_CONTENT_TYPE; HashedStaticString HTTP_EXPECT; HashedStaticString HTTP_CONNECTION; HashedStaticString HTTP_STATUS; HashedStaticString HTTP_TRANSFER_ENCODING; friend class TurboCaching<Request>; friend class ResponseCache<Request>; struct ev_check checkWatcher; TurboCaching<Request> turboCaching; ConfigKit::Store *singleAppModeConfig; #ifdef DEBUG_CC_EVENT_LOOP_BLOCKING struct ev_prepare prepareWatcher; ev_tstamp timeBeforeBlocking; #endif /****** Initialization and shutdown ******/ void preinitialize(); /****** Stage: initialize request ******/ struct RequestAnalysis; void initializeFlags(Client *client, Request *req, RequestAnalysis &analysis); bool respondFromTurboCache(Client *client, Request *req); void initializePoolOptions(Client *client, Request *req, RequestAnalysis &analysis); void fillPoolOptionsFromConfigCaches(Options &options, psg_pool_t *pool, const ControllerRequestConfigPtr &requestConfigCache); static void fillPoolOption(Request *req, StaticString &field, const HashedStaticString &name); static void fillPoolOption(Request *req, int &field, const HashedStaticString &name); static void fillPoolOption(Request *req, bool &field, const HashedStaticString &name); static void fillPoolOption(Request *req, unsigned int &field, const HashedStaticString &name); static void fillPoolOption(Request *req, unsigned long &field, const HashedStaticString &name); static void fillPoolOption(Request *req, long &field, const HashedStaticString &name); static void fillPoolOptionSecToMsec(Request *req, unsigned int &field, const HashedStaticString &name); void createNewPoolOptions(Client *client, Request *req, const HashedStaticString &appGroupName); void setStickySessionId(Client *client, Request *req); const LString *getStickySessionCookieName(Request *req); /****** Stage: buffering body ******/ void beginBufferingBody(Client *client, Request *req); Channel::Result whenBufferingBody_onRequestBody(Client *client, Request *req, const MemoryKit::mbuf &buffer, int errcode); static void _bodyBufferFlushed(FileBufferedChannel *_channel); /****** Stage: checkout session ******/ void checkoutSession(Client *client, Request *req); static void sessionCheckedOut(const AbstractSessionPtr &session, const ExceptionPtr &e, void *userData); void sessionCheckedOutFromAnotherThread(Client *client, Request *req, AbstractSessionPtr session, ExceptionPtr e); void sessionCheckedOutFromEventLoopThread(Client *client, Request *req, const AbstractSessionPtr &session, const ExceptionPtr &e); void maybeSend100Continue(Client *client, Request *req); void initiateSession(Client *client, Request *req); static void checkoutSessionLater(Request *req); void reportSessionCheckoutError(Client *client, Request *req, const ExceptionPtr &e); int lookupCodeFromHeader(Request *req, const char* header, int statusCode); void writeRequestQueueFullExceptionErrorResponse(Client *client, Request *req, const boost::shared_ptr<RequestQueueFullException> &e); void writeSpawnExceptionErrorResponse(Client *client, Request *req, const boost::shared_ptr<SpawningKit::SpawnException> &e); void writeOtherExceptionErrorResponse(Client *client, Request *req, const ExceptionPtr &e); void endRequestWithErrorResponse(Client **c, Request **r, const SpawningKit::SpawnException &e, int statusCode); bool friendlyErrorPagesEnabled(Request *req); /****** Stage: send request to application ******/ struct SessionProtocolWorkingState; struct HttpHeaderConstructionCache; void sendHeaderToApp(Client *client, Request *req); void sendHeaderToAppWithSessionProtocol(Client *client, Request *req); static void sendBodyToAppWhenAppSinkIdle(Channel *_channel, unsigned int size); unsigned int determineMaxHeaderSizeForSessionProtocol(Request *req, SessionProtocolWorkingState &state, string delta_monotonic); bool constructHeaderForSessionProtocol(Request *req, char * restrict buffer, unsigned int &size, const SessionProtocolWorkingState &state, string delta_monotonic); void sendHeaderToAppWithHttpProtocol(Client *client, Request *req); bool constructHeaderBuffersForHttpProtocol(Request *req, struct iovec *buffers, unsigned int maxbuffers, unsigned int & restrict_ref nbuffers, unsigned int & restrict_ref dataSize, HttpHeaderConstructionCache &cache); bool sendHeaderToAppWithHttpProtocolAndWritev(Request *req, ssize_t &bytesWritten, HttpHeaderConstructionCache &cache); void sendHeaderToAppWithHttpProtocolWithBuffering(Request *req, unsigned int offset, HttpHeaderConstructionCache &cache); void sendBodyToApp(Client *client, Request *req); void maybeHalfCloseAppSinkBecauseRequestBodyEndReached(Client *client, Request *req); Channel::Result whenSendingRequest_onRequestBody(Client *client, Request *req, const MemoryKit::mbuf &buffer, int errcode); static void resumeRequestBodyChannelWhenAppSinkIdle(Channel *_channel, unsigned int size); void startBodyChannel(Client *client, Request *req); void stopBodyChannel(Client *client, Request *req); void logAppSocketWriteError(Client *client, int errcode); /****** Stage: forward application response to client ******/ static Channel::Result _onAppSourceData(Channel *_channel, const MemoryKit::mbuf &buffer, int errcode); Channel::Result onAppSourceData(Client *client, Request *req, const MemoryKit::mbuf &buffer, int errcode); void onAppResponseBegin(Client *client, Request *req); void prepareAppResponseCaching(Client *client, Request *req); void onAppResponse100Continue(Client *client, Request *req); bool constructHeaderBuffersForResponse(Request *req, struct iovec *buffers, unsigned int maxbuffers, unsigned int & restrict_ref nbuffers, unsigned int & restrict_ref dataSize, unsigned int & restrict_ref nCacheableBuffers); unsigned int constructDateHeaderBuffersForResponse(char *dateStr, unsigned int bufsize); bool sendResponseHeaderWithWritev(Client *client, Request *req, ssize_t &bytesWritten); void sendResponseHeaderWithBuffering(Client *client, Request *req, unsigned int offset); void logResponseHeaders(Client *client, Request *req, struct iovec *buffers, unsigned int nbuffers, unsigned int dataSize); void markHeaderBuffersForTurboCaching(Client *client, Request *req, struct iovec *buffers, unsigned int nbuffers); static ServerKit::HttpHeaderParser<AppResponse, ServerKit::HttpParseResponse> createAppResponseHeaderParser(ServerKit::Context *ctx, Request *req); static ServerKit::HttpChunkedBodyParser createAppResponseChunkedBodyParser( Request *req); static unsigned int formatAppResponseChunkedBodyParserLoggingPrefix(char *buf, unsigned int bufsize, void *userData); void prepareAppResponseChunkedBodyParsing(Client *client, Request *req); void writeResponseAndMarkForTurboCaching(Client *client, Request *req, const MemoryKit::mbuf &buffer); void markResponsePartForTurboCaching(Client *client, Request *req, const MemoryKit::mbuf &buffer); void maybeThrottleAppSource(Client *client, Request *req); static void _outputBuffersFlushed(FileBufferedChannel *_channel); void outputBuffersFlushed(Client *client, Request *req); static void _outputDataFlushed(FileBufferedChannel *_channel); void outputDataFlushed(Client *client, Request *req); void handleAppResponseBodyEnd(Client *client, Request *req); OXT_FORCE_INLINE void keepAliveAppConnection(Client *client, Request *req); void storeAppResponseInTurboCache(Client *client, Request *req); /***** Hooks ******/ static Channel::Result onBodyBufferData(Channel *_channel, const MemoryKit::mbuf &buffer, int errcode); #ifdef DEBUG_CC_EVENT_LOOP_BLOCKING static void onEventLoopPrepare(EV_P_ struct ev_prepare *w, int revents); #endif static void onEventLoopCheck(EV_P_ struct ev_check *w, int revents); /****** Internal utility functions ******/ void disconnectWithClientSocketWriteError(Client **client, int e); void disconnectWithAppSocketIncompleteResponseError(Client **client); void disconnectWithAppSocketReadError(Client **client, int e); void disconnectWithAppSocketWriteError(Client **client, int e); void endRequestWithAppSocketIncompleteResponse(Client **client, Request **req); void endRequestWithAppSocketReadError(Client **client, Request **req, int e); ServerKit::HeaderTable getHeadersWithContentType(Request *req); const string getFormattedMessage(Request *req, const StaticString &body); void endRequestWithSimpleResponse(Client **c, Request **r, const StaticString &body, int code = 200); void endRequestAsBadGateway(Client **client, Request **req); void writeBenchmarkResponse(Client **client, Request **req, bool end = true); bool getBoolOption(Request *req, const HashedStaticString &name, bool defaultValue = false); template<typename Number> static Number clamp(Number value, Number min, Number max); static void gatherBuffers(char * restrict dest, unsigned int size, const struct iovec *buffers, unsigned int nbuffers); static LString *resolveSymlink(const StaticString &path, psg_pool_t *pool); void parseCookieHeader(psg_pool_t *pool, const LString *headerValue, vector< pair<StaticString, StaticString> > &cookies) const; #ifdef DEBUG_CC_EVENT_LOOP_BLOCKING void reportLargeTimeDiff(Client *client, const char *name, ev_tstamp fromTime, ev_tstamp toTime); #endif protected: /****** Stage: initialize request ******/ virtual void onRequestBegin(Client *client, Request *req); /****** Hooks ******/ virtual void onClientAccepted(Client *client); virtual void onRequestObjectCreated(Client *client, Request *req); virtual void deinitializeClient(Client *client); virtual void reinitializeRequest(Client *client, Request *req); virtual void deinitializeRequest(Client *client, Request *req); void reinitializeAppResponse(Client *client, Request *req); void deinitializeAppResponse(Client *client, Request *req); virtual Channel::Result onRequestBody(Client *client, Request *req, const MemoryKit::mbuf &buffer, int errcode); virtual void onNextRequestEarlyReadError(Client *client, Request *req, int errcode); virtual bool shouldDisconnectClientOnShutdown(Client *client); virtual bool shouldAutoDechunkBody(Client *client, Request *req); virtual bool supportsUpgrade(Client *client, Request *req); /****** Marked virtual so that unit tests can mock these ******/ virtual void asyncGetFromApplicationPool(Request *req, ApplicationPool2::GetCallback callback); public: typedef ControllerConfigChangeRequest ConfigChangeRequest; // Dependencies ResourceLocator *resourceLocator; WrapperRegistry::Registry *wrapperRegistry; PoolPtr appPool; /****** Initialization and shutdown ******/ Controller(ServerKit::Context *context, const ControllerSchema &schema, const Json::Value &initialConfig, const ConfigKit::Translator &translator1 = ConfigKit::DummyTranslator(), const ControllerSingleAppModeSchema *singleAppModeSchema = NULL, const Json::Value *_singleAppModeConfig = NULL, const ConfigKit::Translator &translator2 = ConfigKit::DummyTranslator() ) : ParentClass(context, schema, initialConfig, translator1), mainConfig(config), requestConfig(new ControllerRequestConfig(config)), poolOptionsCache(4), turboCaching(), singleAppModeConfig(NULL), resourceLocator(NULL) /**************************/ { if (mainConfig.singleAppMode) { singleAppModeConfig = new ConfigKit::Store(*singleAppModeSchema, *_singleAppModeConfig, translator2); } preinitialize(); } virtual ~Controller(); virtual void initialize(); /****** Hooks ******/ virtual unsigned int getClientName(const Client *client, char *buf, size_t size) const; virtual StaticString getServerName() const; /****** Configuration handling ******/ bool prepareConfigChange(const Json::Value &updates, vector<ConfigKit::Error> &errors, ControllerConfigChangeRequest &req); void commitConfigChange(ControllerConfigChangeRequest &req) BOOST_NOEXCEPT_OR_NOTHROW; /****** State and configuration ******/ unsigned int getThreadNumber() const; // Thread-safe virtual Json::Value inspectStateAsJson() const; virtual Json::Value inspectClientStateAsJson(const Client *client) const; virtual Json::Value inspectRequestStateAsJson(const Request *req) const; /****** Miscellaneous *******/ void disconnectLongRunningConnections(const StaticString &gupid); }; } // namespace Core } // namespace Passenger #endif /* _PASSENGER_CORE_CONTROLLER_H_ */ ConfigChange.h 0000644 00000004530 14756456557 0007262 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2017 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef _PASSENGER_CORE_CONFIG_CHANGE_H_ #define _PASSENGER_CORE_CONFIG_CHANGE_H_ #include <boost/config.hpp> #include <boost/function.hpp> #include <ConfigKit/ConfigKit.h> namespace Passenger { namespace Core { using namespace std; struct ConfigChangeRequest; typedef boost::function<void (const vector<ConfigKit::Error> &errors, ConfigChangeRequest *req)> PrepareConfigChangeCallback; typedef boost::function<void (ConfigChangeRequest *req)> CommitConfigChangeCallback; ConfigChangeRequest *createConfigChangeRequest(); void freeConfigChangeRequest(ConfigChangeRequest *req); void asyncPrepareConfigChange(const Json::Value &updates, ConfigChangeRequest *req, const PrepareConfigChangeCallback &callback); void asyncCommitConfigChange(ConfigChangeRequest *req, const CommitConfigChangeCallback &callback) BOOST_NOEXCEPT_OR_NOTHROW; Json::Value inspectConfig(); Json::Value manipulateLoggingKitConfig(const ConfigKit::Store &coreConfig, const Json::Value &loggingKitConfig); } // namespace Core } // namespace Passenger #endif /* _PASSENGER_CORE_CONFIG_CHANGE_H_ */ Config.h 0000644 00000070754 14756456557 0006167 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2011-2018 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef _PASSENGER_CORE_CONFIG_H_ #define _PASSENGER_CORE_CONFIG_H_ #include <string> #include <vector> #include <boost/bind/bind.hpp> #include <boost/foreach.hpp> #include <LoggingKit/LoggingKit.h> #include <LoggingKit/Config.h> #include <LoggingKit/Context.h> #include <ConfigKit/Schema.h> #include <ConfigKit/TableTranslator.h> #include <ConfigKit/PrefixTranslator.h> #include <ServerKit/Context.h> #include <ServerKit/HttpServer.h> #include <WrapperRegistry/Registry.h> #include <Core/Controller/Config.h> #include <Core/SecurityUpdateChecker.h> #include <Core/TelemetryCollector.h> #include <Core/ApiServer.h> #include <Core/AdminPanelConnector.h> #include <Shared/ApiAccountUtils.h> #include <Constants.h> #include <Utils.h> #include <IOTools/IOUtils.h> namespace Passenger { namespace Core { using namespace std; /* * BEGIN ConfigKit schema: Passenger::Core::Schema * (do not edit: following text is automatically generated * by 'rake configkit_schemas_inline_comments') * * admin_panel_auth_type string - default("basic") * admin_panel_close_timeout float - default(10.0) * admin_panel_connect_timeout float - default(30.0) * admin_panel_data_debug boolean - default(false) * admin_panel_password string - secret * admin_panel_password_file string - - * admin_panel_ping_interval float - default(30.0) * admin_panel_ping_timeout float - default(30.0) * admin_panel_proxy_password string - secret * admin_panel_proxy_timeout float - default(30.0) * admin_panel_proxy_url string - - * admin_panel_proxy_username string - - * admin_panel_reconnect_timeout float - default(5.0) * admin_panel_url string - read_only * admin_panel_username string - - * admin_panel_websocketpp_debug_access boolean - default(false) * admin_panel_websocketpp_debug_error boolean - default(false) * api_server_accept_burst_count unsigned integer - default(32) * api_server_addresses array of strings - default([]),read_only * api_server_authorizations array - default("[FILTERED]"),secret * api_server_client_freelist_limit unsigned integer - default(0) * api_server_file_buffered_channel_auto_start_mover boolean - default(true) * api_server_file_buffered_channel_auto_truncate_file boolean - default(true) * api_server_file_buffered_channel_buffer_dir string - default * api_server_file_buffered_channel_delay_in_file_mode_switching unsigned integer - default(0) * api_server_file_buffered_channel_max_disk_chunk_read_size unsigned integer - default(0) * api_server_file_buffered_channel_threshold unsigned integer - default(131072) * api_server_mbuf_block_chunk_size unsigned integer - default(4096),read_only * api_server_min_spare_clients unsigned integer - default(0) * api_server_request_freelist_limit unsigned integer - default(1024) * api_server_start_reading_after_accept boolean - default(true) * app_output_log_level string - default("notice") * benchmark_mode string - - * config_manifest object - read_only * controller_accept_burst_count unsigned integer - default(32) * controller_addresses array of strings - default(["tcp://127.0.0.1:3000"]),read_only * controller_client_freelist_limit unsigned integer - default(0) * controller_cpu_affine boolean - default(false),read_only * controller_file_buffered_channel_auto_start_mover boolean - default(true) * controller_file_buffered_channel_auto_truncate_file boolean - default(true) * controller_file_buffered_channel_buffer_dir string - default * controller_file_buffered_channel_delay_in_file_mode_switching unsigned integer - default(0) * controller_file_buffered_channel_max_disk_chunk_read_size unsigned integer - default(0) * controller_file_buffered_channel_threshold unsigned integer - default(131072) * controller_mbuf_block_chunk_size unsigned integer - default(4096),read_only * controller_min_spare_clients unsigned integer - default(0) * controller_request_freelist_limit unsigned integer - default(1024) * controller_secure_headers_password any - secret * controller_socket_backlog unsigned integer - default(2048),read_only * controller_start_reading_after_accept boolean - default(true) * controller_threads unsigned integer - default,read_only * default_abort_websockets_on_process_shutdown boolean - default(true) * default_app_file_descriptor_ulimit unsigned integer - - * default_bind_address string - default("127.0.0.1") * default_environment string - default("production") * default_force_max_concurrent_requests_per_process integer - default(-1) * default_friendly_error_pages string - default("auto") * default_group string - default * default_load_shell_envvars boolean - default(false) * default_max_preloader_idle_time unsigned integer - default(300) * default_max_request_queue_size unsigned integer - default(100) * default_max_requests unsigned integer - default(0) * default_meteor_app_settings string - - * default_min_instances unsigned integer - default(1) * default_nodejs string - default("node") * default_preload_bundler boolean - default(false) * default_python string - default("python") * default_ruby string - default("ruby") * default_server_name string - default * default_server_port unsigned integer - default * default_spawn_method string - default("smart") * default_sticky_sessions boolean - default(false) * default_sticky_sessions_cookie_attributes string - default("SameSite=Lax; Secure;") * default_sticky_sessions_cookie_name string - default("_passenger_route") * default_user string - default("nobody") * disable_log_prefix boolean - default(false) * file_descriptor_log_target any - - * file_descriptor_ulimit unsigned integer - default(0),read_only * graceful_exit boolean - default(true) * hook_attached_process string - read_only * hook_detached_process string - read_only * hook_queue_full_error string - read_only * hook_spawn_failed string - read_only * instance_dir string - read_only * integration_mode string - default("standalone") * log_level string - default("notice") * log_target any - default({"stderr": true}) * max_instances_per_app unsigned integer - read_only * max_pool_size unsigned integer - default(6) * multi_app boolean - default(false),read_only * oom_score string - read_only * passenger_root string required read_only * pid_file string - read_only * pool_idle_time unsigned integer - default(300) * pool_selfchecks boolean - default(false) * prestart_urls array of strings - default([]),read_only * response_buffer_high_watermark unsigned integer - default(134217728) * security_update_checker_certificate_path string - - * security_update_checker_disabled boolean - default(false) * security_update_checker_interval unsigned integer - default(86400) * security_update_checker_proxy_url string - - * security_update_checker_url string - default("https://securitycheck.phusionpassenger.com/v1/check.json") * server_software string - default("Phusion_Passenger/6.0.20") * show_version_in_header boolean - default(true) * single_app_mode_app_root string - default,read_only * single_app_mode_app_start_command string - read_only * single_app_mode_app_type string - read_only * single_app_mode_startup_file string - read_only * spawn_dir string required read_only * standalone_engine string - default * stat_throttle_rate unsigned integer - default(10) * telemetry_collector_ca_certificate_path string - - * telemetry_collector_debug_curl boolean - default(false) * telemetry_collector_disabled boolean - default(false) * telemetry_collector_final_run_timeout unsigned integer - default(5) * telemetry_collector_first_interval unsigned integer - default(7200) * telemetry_collector_interval unsigned integer - default(21600) * telemetry_collector_interval_jitter unsigned integer - default(7200) * telemetry_collector_proxy_url string - - * telemetry_collector_timeout unsigned integer - default(180) * telemetry_collector_url string - default("https://anontelemetry.phusionpassenger.com/v1/collect.json") * telemetry_collector_verify_server boolean - default(true) * turbocaching boolean - default(true),read_only * user_switching boolean - default(true) * vary_turbocache_by_cookie string - - * watchdog_fd_passing_password string - secret * web_server_module_version string - read_only * web_server_version string - read_only * * END */ class Schema: public ConfigKit::Schema { private: // Prefix config options that come from the given schema template<typename SchemaType> static void addSubSchemaPrefixTranslations(ConfigKit::TableTranslator &translator, const StaticString &prefix) { vector<string> keys = SchemaType().inspect().getMemberNames(); vector<string>::const_iterator it, end = keys.end(); for (it = keys.begin(); it != end; it++) { translator.add(prefix + *it, *it); } } static Json::Value getDefaultServerName(const ConfigKit::Store &store) { Json::Value addresses = store["controller_addresses"]; if (addresses.size() > 0) { string firstAddress = addresses[0].asString(); if (getSocketAddressType(firstAddress) == SAT_TCP) { string host; unsigned short port; parseTcpSocketAddress(firstAddress, host, port); return host; } } return "localhost"; } static Json::Value getDefaultServerPort(const ConfigKit::Store &store) { Json::Value addresses = store["controller_addresses"]; if (addresses.size() > 0) { string firstAddress = addresses[0].asString(); if (getSocketAddressType(firstAddress) == SAT_TCP) { string host; unsigned short port; parseTcpSocketAddress(firstAddress, host, port); return port; } } return 80; } static Json::Value getDefaultThreads(const ConfigKit::Store &store) { return Json::UInt(boost::thread::hardware_concurrency()); } static Json::Value getDefaultControllerAddresses() { Json::Value doc; doc.append(DEFAULT_HTTP_SERVER_LISTEN_ADDRESS); return doc; } static void validateMultiAppMode(const ConfigKit::Store &config, vector<ConfigKit::Error> &errors) { typedef ConfigKit::Error Error; if (!config["multi_app"].asBool()) { return; } if (!config["single_app_mode_app_type"].isNull()) { errors.push_back(Error("If '{{multi_app_mode}}' is set," " then '{{single_app_mode_app_type}}' may not be set")); } if (!config["single_app_mode_startup_file"].isNull()) { errors.push_back(Error("If '{{multi_app_mode}}' is set," " then '{{single_app_mode_startup_file}}' may not be set")); } if (!config["single_app_mode_app_start_command"].isNull()) { errors.push_back(Error("If '{{multi_app_mode}}' is set," " then '{{single_app_mode_app_start_command}}' may not be set")); } } static void validateSingleAppMode(const ConfigKit::Store &config, const WrapperRegistry::Registry *wrapperRegistry, vector<ConfigKit::Error> &errors) { typedef ConfigKit::Error Error; if (config["multi_app"].asBool()) { return; } // single_app_mode_app_type, single_app_mode_startup_file and // single_app_mode_app_start_command are autodetected in // initializeSingleAppMode() so no need to validate them. ControllerSingleAppModeSchema::validateAppType("single_app_mode_app_type", wrapperRegistry, config, errors); } static void validateControllerSecureHeadersPassword(const ConfigKit::Store &config, vector<ConfigKit::Error> &errors) { typedef ConfigKit::Error Error; Json::Value password = config["controller_secure_headers_password"]; if (password.isNull()) { return; } if (!password.isString() && !password.isObject()) { errors.push_back(Error("'{{controller_secure_headers_password}}' must be a string or an object")); return; } if (password.isObject()) { if (!password.isMember("path")) { errors.push_back(Error("If '{{controller_secure_headers_password}}' is an object, then it must contain a 'path' option")); } else if (!password["path"].isString()) { errors.push_back(Error("If '{{controller_secure_headers_password}}' is an object, then its 'path' option must be a string")); } } } static void validateApplicationPool(const ConfigKit::Store &config, vector<ConfigKit::Error> &errors) { typedef ConfigKit::Error Error; if (config["max_pool_size"].asUInt() < 1) { errors.push_back(Error("'{{max_pool_size}}' must be at least 1")); } } static void validateController(const ConfigKit::Store &config, vector<ConfigKit::Error> &errors) { typedef ConfigKit::Error Error; if (config["controller_threads"].asUInt() < 1) { errors.push_back(Error("'{{controller_threads}}' must be at least 1")); } } static void validateAddresses(const ConfigKit::Store &config, vector<ConfigKit::Error> &errors) { typedef ConfigKit::Error Error; if (config["controller_addresses"].empty()) { errors.push_back(Error("'{{controller_addresses}}' must contain at least 1 item")); } else if (config["controller_addresses"].size() > SERVER_KIT_MAX_SERVER_ENDPOINTS) { errors.push_back(Error("'{{controller_addresses}}' may contain at most " + toString(SERVER_KIT_MAX_SERVER_ENDPOINTS) + " items")); } if (config["api_server_addresses"].size() > SERVER_KIT_MAX_SERVER_ENDPOINTS) { errors.push_back(Error("'{{api_server_addresses}}' may contain at most " + toString(SERVER_KIT_MAX_SERVER_ENDPOINTS) + " items")); } } /*****************/ /*****************/ static Json::Value normalizeSingleAppMode(const Json::Value &effectiveValues) { if (effectiveValues["multi_app"].asBool()) { return Json::Value(); } Json::Value updates; updates["single_app_mode_app_root"] = absolutizePath( effectiveValues["single_app_mode_app_root"].asString()); if (!effectiveValues["single_app_mode_startup_file"].isNull()) { updates["single_app_mode_startup_file"] = absolutizePath( effectiveValues["single_app_mode_startup_file"].asString()); } return updates; } static Json::Value normalizeServerSoftware(const Json::Value &effectiveValues) { string serverSoftware = effectiveValues["server_software"].asString(); if (serverSoftware.find(SERVER_TOKEN_NAME) == string::npos && serverSoftware.find(FLYING_PASSENGER_NAME) == string::npos) { serverSoftware.append(" " SERVER_TOKEN_NAME "/" PASSENGER_VERSION); } Json::Value updates; updates["server_software"] = serverSoftware; return updates; } public: struct { LoggingKit::Schema schema; ConfigKit::TableTranslator translator; } loggingKit; struct { ControllerSchema schema; ConfigKit::TableTranslator translator; } controller; struct ControllerSingleAppModeSubschemaContainer { ControllerSingleAppModeSchema schema; ConfigKit::PrefixTranslator translator; ControllerSingleAppModeSubschemaContainer(const WrapperRegistry::Registry *registry) : schema(registry) { } } controllerSingleAppMode; struct { ServerKit::Schema schema; ConfigKit::PrefixTranslator translator; } controllerServerKit; struct { SecurityUpdateChecker::Schema schema; ConfigKit::PrefixTranslator translator; } securityUpdateChecker; struct { TelemetryCollector::Schema schema; ConfigKit::PrefixTranslator translator; } telemetryCollector; struct { ApiServer::Schema schema; ConfigKit::TableTranslator translator; } apiServer; struct { ServerKit::Schema schema; ConfigKit::PrefixTranslator translator; } apiServerKit; struct { AdminPanelConnector::Schema schema; ConfigKit::TableTranslator translator; } adminPanelConnector; Schema(const WrapperRegistry::Registry *wrapperRegistry = NULL) : controllerSingleAppMode(wrapperRegistry) { using namespace ConfigKit; // Add subschema: loggingKit loggingKit.translator.add("log_level", "level"); loggingKit.translator.add("log_target", "target"); loggingKit.translator.finalize(); addSubSchema(loggingKit.schema, loggingKit.translator); erase("redirect_stderr"); erase("buffer_logs"); // Add subschema: controller addSubSchemaPrefixTranslations<ServerKit::HttpServerSchema>( controller.translator, "controller_"); controller.translator.finalize(); addSubSchema(controller.schema, controller.translator); erase("thread_number"); // Add subschema: controller (single app mode) controllerSingleAppMode.translator.setPrefixAndFinalize("single_app_mode_"); addWithDynamicDefault("single_app_mode_app_root", STRING_TYPE, OPTIONAL | READ_ONLY | CACHE_DEFAULT_VALUE, ControllerSingleAppModeSchema::getDefaultAppRoot); add("single_app_mode_app_type", STRING_TYPE, OPTIONAL | READ_ONLY); add("single_app_mode_startup_file", STRING_TYPE, OPTIONAL | READ_ONLY); add("single_app_mode_app_start_command", STRING_TYPE, OPTIONAL | READ_ONLY); // Add subschema: controllerServerKit controllerServerKit.translator.setPrefixAndFinalize("controller_"); addSubSchema(controllerServerKit.schema, controllerServerKit.translator); erase("controller_secure_mode_password"); // Add subschema: securityUpdateChecker securityUpdateChecker.translator.setPrefixAndFinalize("security_update_checker_"); addSubSchema(securityUpdateChecker.schema, securityUpdateChecker.translator); erase("security_update_checker_server_identifier"); erase("security_update_checker_web_server_version"); // Add subschema: telemetryCollector telemetryCollector.translator.setPrefixAndFinalize("telemetry_collector_"); addSubSchema(telemetryCollector.schema, telemetryCollector.translator); // Add subschema: apiServer apiServer.translator.add("api_server_authorizations", "authorizations"); addSubSchemaPrefixTranslations<ServerKit::HttpServerSchema>( apiServer.translator, "api_server_"); apiServer.translator.finalize(); addSubSchema(apiServer.schema, apiServer.translator); // Add subschema: apiServerKit apiServerKit.translator.setPrefixAndFinalize("api_server_"); addSubSchema(apiServerKit.schema, apiServerKit.translator); erase("api_server_secure_mode_password"); // Add subschema: adminPanelConnector addSubSchemaPrefixTranslations<WebSocketCommandReverseServer::Schema>( adminPanelConnector.translator, "admin_panel_"); adminPanelConnector.translator.finalize(); addSubSchema(adminPanelConnector.schema, adminPanelConnector.translator); erase("admin_panel_log_prefix"); erase("ruby"); override("admin_panel_url", STRING_TYPE, OPTIONAL | READ_ONLY); override("instance_dir", STRING_TYPE, OPTIONAL | READ_ONLY); override("multi_app", BOOL_TYPE, OPTIONAL | READ_ONLY, false); overrideWithDynamicDefault("default_server_name", STRING_TYPE, OPTIONAL, getDefaultServerName); overrideWithDynamicDefault("default_server_port", UINT_TYPE, OPTIONAL, getDefaultServerPort); add("passenger_root", STRING_TYPE, REQUIRED | READ_ONLY); add("spawn_dir", STRING_TYPE, REQUIRED | READ_ONLY); add("config_manifest", OBJECT_TYPE, OPTIONAL | READ_ONLY); add("pid_file", STRING_TYPE, OPTIONAL | READ_ONLY); add("web_server_version", STRING_TYPE, OPTIONAL | READ_ONLY); add("oom_score", STRING_TYPE, OPTIONAL | READ_ONLY); addWithDynamicDefault("controller_threads", UINT_TYPE, OPTIONAL | READ_ONLY, getDefaultThreads); add("max_pool_size", UINT_TYPE, OPTIONAL, DEFAULT_MAX_POOL_SIZE); add("pool_idle_time", UINT_TYPE, OPTIONAL, Json::UInt(DEFAULT_POOL_IDLE_TIME)); add("pool_selfchecks", BOOL_TYPE, OPTIONAL, false); add("prestart_urls", STRING_ARRAY_TYPE, OPTIONAL | READ_ONLY, Json::arrayValue); add("controller_secure_headers_password", ANY_TYPE, OPTIONAL | SECRET); add("controller_socket_backlog", UINT_TYPE, OPTIONAL | READ_ONLY, DEFAULT_SOCKET_BACKLOG); add("controller_addresses", STRING_ARRAY_TYPE, OPTIONAL | READ_ONLY, getDefaultControllerAddresses()); add("api_server_addresses", STRING_ARRAY_TYPE, OPTIONAL | READ_ONLY, Json::arrayValue); add("controller_cpu_affine", BOOL_TYPE, OPTIONAL | READ_ONLY, false); add("file_descriptor_ulimit", UINT_TYPE, OPTIONAL | READ_ONLY, 0); add("hook_attached_process", STRING_TYPE, OPTIONAL | READ_ONLY); add("hook_detached_process", STRING_TYPE, OPTIONAL | READ_ONLY); add("hook_spawn_failed", STRING_TYPE, OPTIONAL | READ_ONLY); add("hook_queue_full_error", STRING_TYPE, OPTIONAL | READ_ONLY); addValidator(validateMultiAppMode); addValidator(boost::bind(validateSingleAppMode, boost::placeholders::_1, wrapperRegistry, boost::placeholders::_2)); addValidator(validateControllerSecureHeadersPassword); addValidator(validateApplicationPool); addValidator(validateController); addValidator(validateAddresses); addNormalizer(normalizeSingleAppMode); addNormalizer(normalizeServerSoftware); /*******************/ /*******************/ finalize(); } }; } // namespace Core } // namespace Passenger #endif /* _PASSENGER_CORE_CONFIG_H_ */ TelemetryCollectorTest.cpp 0000644 00000021220 14756504010 0011730 0 ustar 00 #include <TestSupport.h> #include <Core/TelemetryCollector.h> using namespace std; using namespace boost; using namespace Passenger; using namespace Passenger::Core; namespace tut { struct Core_TelemetryCollectorTest: public TestBase { class MyTelemetryCollector: public TelemetryCollector { private: virtual TelemetryData collectTelemetryData(bool isFinalRun) const { return mockTelemetryData; } virtual CURLcode performCurlAction(CURL *curl, const char *lastErrorMessage, const string &requestBody, string &responseData, long &responseCode) { Json::Reader reader; if (!reader.parse(requestBody, lastRequestBody)) { throw RuntimeException("Request body parse error: " + reader.getFormattedErrorMessages()); } responseData = mockResponse.toStyledString(); responseCode = mockResponseCode; return mockCurlResult; } public: TelemetryData mockTelemetryData; long mockResponseCode; Json::Value mockResponse; CURLcode mockCurlResult; Json::Value lastRequestBody; MyTelemetryCollector(const Schema &schema, const Json::Value &initialConfig = Json::Value(), const ConfigKit::Translator &translator = ConfigKit::DummyTranslator()) : TelemetryCollector(schema, initialConfig, translator), mockResponseCode(200), mockCurlResult(CURLE_OK) { mockResponse["data_processed"] = true; } }; TelemetryCollector::Schema schema; Json::Value config; MyTelemetryCollector *col; Core_TelemetryCollectorTest() : col(NULL) { } ~Core_TelemetryCollectorTest() { delete col; SystemTime::releaseAll(); } void init() { col = new MyTelemetryCollector(schema, config); col->controllers.resize(2, NULL); col->mockTelemetryData.requestsHandled.resize(2, 0); col->initialize(); } }; DEFINE_TEST_GROUP(Core_TelemetryCollectorTest); /***** Passing request information to the app *****/ TEST_METHOD(1) { set_test_name("On first run, it sends the number of requests handled so far"); init(); col->mockTelemetryData.requestsHandled[0] = 90; col->mockTelemetryData.requestsHandled[1] = 150; col->mockTelemetryData.timestamp = SystemTime::getMonotonicUsec(); col->runOneCycle(); ensure_equals(col->lastRequestBody["requests_handled"].asUInt(), 90u + 150u); } TEST_METHOD(2) { set_test_name("On first run, it sends begin_time = object creation time, end_time = now"); SystemTime::forceAll(1000000); init(); SystemTime::forceAll(2000000); col->mockTelemetryData.requestsHandled[0] = 90; col->mockTelemetryData.requestsHandled[1] = 150; col->mockTelemetryData.timestamp = SystemTime::getMonotonicUsec(); col->runOneCycle(); ensure_equals(col->lastRequestBody["begin_time"].asUInt(), 1u); ensure_equals(col->lastRequestBody["end_time"].asUInt(), 2u); } TEST_METHOD(5) { set_test_name("On subsequent runs, it sends the number of requests handled since last run"); init(); col->mockTelemetryData.requestsHandled[0] = 90; col->mockTelemetryData.requestsHandled[1] = 150; col->mockTelemetryData.timestamp = SystemTime::getMonotonicUsec(); col->runOneCycle(); col->mockTelemetryData.requestsHandled[0] = 120; col->mockTelemetryData.requestsHandled[1] = 180; col->mockTelemetryData.timestamp = SystemTime::getMonotonicUsec(); col->runOneCycle(); ensure_equals(col->lastRequestBody["requests_handled"].asUInt(), (120u - 90u) + (180u - 150u)); } TEST_METHOD(6) { set_test_name("On subsequent runs, it sends begin_time = last send time, end_time = now"); SystemTime::forceAll(1000000); init(); SystemTime::forceAll(2000000); col->mockTelemetryData.requestsHandled[0] = 90; col->mockTelemetryData.requestsHandled[1] = 150; col->mockTelemetryData.timestamp = SystemTime::getMonotonicUsec(); col->runOneCycle(); SystemTime::forceAll(3000000); col->mockTelemetryData.requestsHandled[0] = 120; col->mockTelemetryData.requestsHandled[1] = 180; col->mockTelemetryData.timestamp = SystemTime::getMonotonicUsec(); col->runOneCycle(); ensure_equals(col->lastRequestBody["begin_time"].asUInt(), 2u); ensure_equals(col->lastRequestBody["end_time"].asUInt(), 3u); } TEST_METHOD(7) { set_test_name("On subsequent runs, it handles request counter overflows"); init(); col->mockTelemetryData.requestsHandled[0] = std::numeric_limits<boost::uint64_t>::max(); col->mockTelemetryData.requestsHandled[1] = std::numeric_limits<boost::uint64_t>::max() - 1; col->mockTelemetryData.timestamp = SystemTime::getMonotonicUsec(); col->runOneCycle(); col->mockTelemetryData.requestsHandled[0] = 0; col->mockTelemetryData.requestsHandled[1] = 2; col->mockTelemetryData.timestamp = SystemTime::getMonotonicUsec(); col->runOneCycle(); ensure_equals(col->lastRequestBody["requests_handled"].asUInt(), 1u + 4u); } TEST_METHOD(10) { set_test_name("If the server responds with data_processed = false," " then the next run sends telemetry relative to the last time" " the server responded with data_processed = true"); SystemTime::forceAll(1000000); init(); SystemTime::forceAll(2000000); col->mockTelemetryData.requestsHandled[0] = 90; col->mockTelemetryData.requestsHandled[1] = 150; col->mockTelemetryData.timestamp = SystemTime::getMonotonicUsec(); col->runOneCycle(); SystemTime::forceAll(3000000); col->mockTelemetryData.requestsHandled[0] = 120; col->mockTelemetryData.requestsHandled[1] = 180; col->mockTelemetryData.timestamp = SystemTime::getMonotonicUsec(); col->mockResponse["data_processed"] = false; col->runOneCycle(); SystemTime::forceAll(4000000); col->mockTelemetryData.requestsHandled[0] = 160; col->mockTelemetryData.requestsHandled[1] = 200; col->mockTelemetryData.timestamp = SystemTime::getMonotonicUsec(); col->mockResponse["data_processed"] = true; col->runOneCycle(); ensure_equals(col->lastRequestBody["requests_handled"].asUInt(), (160u - 90u) + (200u - 150u)); ensure_equals(col->lastRequestBody["begin_time"].asUInt(), 2u); ensure_equals(col->lastRequestBody["end_time"].asUInt(), 4u); } TEST_METHOD(11) { set_test_name("If the server responds with an error," " then the next run sends telemetry relative to the last time" " the server responded with data_processed = true"); SystemTime::forceAll(1000000); init(); SystemTime::forceAll(2000000); col->mockTelemetryData.requestsHandled[0] = 90; col->mockTelemetryData.requestsHandled[1] = 150; col->mockTelemetryData.timestamp = SystemTime::getMonotonicUsec(); col->runOneCycle(); SystemTime::forceAll(3000000); col->mockTelemetryData.requestsHandled[0] = 120; col->mockTelemetryData.requestsHandled[1] = 180; col->mockTelemetryData.timestamp = SystemTime::getMonotonicUsec(); col->mockResponseCode = 502; if (defaultLogLevel == (LoggingKit::Level) DEFAULT_LOG_LEVEL) { // If the user did not customize the test's log level, // then we'll want to tone down the noise. LoggingKit::setLevel(LoggingKit::CRIT); } col->runOneCycle(); if (defaultLogLevel == (LoggingKit::Level) DEFAULT_LOG_LEVEL) { LoggingKit::setLevel((LoggingKit::Level) DEFAULT_LOG_LEVEL); } SystemTime::forceAll(4000000); col->mockTelemetryData.requestsHandled[0] = 160; col->mockTelemetryData.requestsHandled[1] = 200; col->mockTelemetryData.timestamp = SystemTime::getMonotonicUsec(); col->mockResponseCode = 200; col->runOneCycle(); ensure_equals(col->lastRequestBody["requests_handled"].asUInt(), (160u - 90u) + (200u - 150u)); ensure_equals(col->lastRequestBody["begin_time"].asUInt(), 2u); ensure_equals(col->lastRequestBody["end_time"].asUInt(), 4u); } TEST_METHOD(12) { set_test_name("If the server responds with 'backoff'," "then the next run is scheduled according to the server-provided backoff"); init(); col->mockTelemetryData.requestsHandled[0] = 90; col->mockTelemetryData.requestsHandled[1] = 150; col->mockTelemetryData.timestamp = SystemTime::getMonotonicUsec(); col->mockResponse["backoff"] = 555; ensure_equals(col->runOneCycle(), 555u); } TEST_METHOD(13) { set_test_name("If the server responds with no 'backoff'," "then the next run is scheduled according to the interval config"); init(); col->mockTelemetryData.requestsHandled[0] = 90; col->mockTelemetryData.requestsHandled[1] = 150; col->mockTelemetryData.timestamp = SystemTime::getMonotonicUsec(); ensure_equals(col->runOneCycle(), 0u); } TEST_METHOD(15) { set_test_name("It sends no data when disabled"); config["disabled"] = true; init(); col->mockTelemetryData.requestsHandled[0] = 90; col->mockTelemetryData.requestsHandled[1] = 150; col->mockTelemetryData.timestamp = SystemTime::getMonotonicUsec(); ensure_equals(col->runOneCycle(), 0u); ensure(col->lastRequestBody.isNull()); } } ApplicationPool/OptionsTest.cpp 0000644 00000001260 14756504011 0012642 0 ustar 00 #include <TestSupport.h> #include <Core/ApplicationPool/Process.h> using namespace Passenger; using namespace Passenger::ApplicationPool2; using namespace std; namespace tut { struct Core_ApplicationPool_OptionsTest: public TestBase { }; DEFINE_TEST_GROUP(Core_ApplicationPool_OptionsTest); TEST_METHOD(1) { // Test persist(). char appRoot[] = "appRoot"; char processTitle[] = "processTitle"; Options options; options.appRoot = appRoot; options.processTitle = processTitle; Options options2 = options.copyAndPersist(); appRoot[0] = processTitle[0] = 'x'; ensure_equals(options2.appRoot, "appRoot"); ensure_equals(options2.processTitle, "processTitle"); } } ApplicationPool/ProcessTest.cpp 0000644 00000020054 14756504011 0012627 0 ustar 00 #include <TestSupport.h> #include <Core/ApplicationPool/Process.h> #include <LoggingKit/Context.h> #include <FileTools/FileManip.h> using namespace Passenger; using namespace Passenger::ApplicationPool2; using namespace std; namespace tut { struct Core_ApplicationPool_ProcessTest: public TestBase { WrapperRegistry::Registry wrapperRegistry; SpawningKit::Context::Schema skContextSchema; SpawningKit::Context skContext; Context context; BasicGroupInfo groupInfo; vector<SpawningKit::Result::Socket> sockets; Pipe stdinFd, stdoutAndErrFd; FileDescriptor server1, server2, server3; Core_ApplicationPool_ProcessTest() : skContext(skContextSchema) { wrapperRegistry.finalize(); skContext.resourceLocator = resourceLocator; skContext.wrapperRegistry = &wrapperRegistry; skContext.integrationMode = "standalone"; skContext.finalize(); context.spawningKitFactory = boost::make_shared<SpawningKit::Factory>(&skContext); context.finalize(); groupInfo.context = &context; groupInfo.group = NULL; groupInfo.name = "test"; struct sockaddr_in addr; socklen_t len = sizeof(addr); SpawningKit::Result::Socket socket; server1.assign(createTcpServer("127.0.0.1", 0, 0, __FILE__, __LINE__), NULL, 0); getsockname(server1, (struct sockaddr *) &addr, &len); socket.address = "tcp://127.0.0.1:" + toString(addr.sin_port); socket.protocol = "session"; socket.concurrency = 3; socket.acceptHttpRequests = true; sockets.push_back(socket); server2.assign(createTcpServer("127.0.0.1", 0, 0, __FILE__, __LINE__), NULL, 0); getsockname(server2, (struct sockaddr *) &addr, &len); socket = SpawningKit::Result::Socket(); socket.address = "tcp://127.0.0.1:" + toString(addr.sin_port); socket.protocol = "session"; socket.concurrency = 3; socket.acceptHttpRequests = true; sockets.push_back(socket); server3.assign(createTcpServer("127.0.0.1", 0, 0, __FILE__, __LINE__), NULL, 0); getsockname(server3, (struct sockaddr *) &addr, &len); socket = SpawningKit::Result::Socket(); socket.address = "tcp://127.0.0.1:" + toString(addr.sin_port); socket.protocol = "session"; socket.concurrency = 3; socket.acceptHttpRequests = true; sockets.push_back(socket); stdinFd = createPipe(__FILE__, __LINE__); stdoutAndErrFd = createPipe(__FILE__, __LINE__); Json::Value config; vector<ConfigKit::Error> errors; LoggingKit::ConfigChangeRequest req; config["app_output_log_level"] = "debug"; if (LoggingKit::context->prepareConfigChange(config, errors, req)) { LoggingKit::context->commitConfigChange(req); } else { P_BUG("Error configuring LoggingKit: " << ConfigKit::toString(errors)); } } ~Core_ApplicationPool_ProcessTest() { Json::Value config; vector<ConfigKit::Error> errors; LoggingKit::ConfigChangeRequest req; config["level"] = DEFAULT_LOG_LEVEL_NAME; config["app_output_log_level"] = DEFAULT_APP_OUTPUT_LOG_LEVEL_NAME; if (LoggingKit::context->prepareConfigChange(config, errors, req)) { LoggingKit::context->commitConfigChange(req); } else { P_BUG("Error configuring LoggingKit: " << ConfigKit::toString(errors)); } } ProcessPtr createProcess(const Json::Value &extraArgs = Json::Value()) { SpawningKit::Result result; Json::Value args = extraArgs; vector<StaticString> internalFieldErrors; vector<StaticString> appSuppliedFieldErrors; result.pid = 123; result.gupid = "123"; result.type = SpawningKit::Result::DUMMY; result.spawnStartTime = 1; result.spawnEndTime = 1; result.spawnStartTimeMonotonic = 1; result.spawnEndTimeMonotonic = 1; result.sockets = sockets; result.stdinFd = stdinFd[1]; result.stdoutAndErrFd = stdoutAndErrFd[0]; if (!result.validate(internalFieldErrors, appSuppliedFieldErrors)) { P_BUG("Cannot create dummy process:\n" << toString(internalFieldErrors) << "\n" << toString(appSuppliedFieldErrors)); } args["spawner_creation_time"] = 0; ProcessPtr process(context.processObjectPool.construct( &groupInfo, result, args), false); process->shutdownNotRequired(); return process; } }; DEFINE_TEST_GROUP(Core_ApplicationPool_ProcessTest); TEST_METHOD(1) { set_test_name("Test initial state"); ProcessPtr process = createProcess(); ensure_equals(process->busyness(), 0); ensure(!process->isTotallyBusy()); } TEST_METHOD(2) { set_test_name("Test opening and closing sessions"); ProcessPtr process = createProcess(); SessionPtr session = process->newSession(); SessionPtr session2 = process->newSession(); ensure_equals(process->sessions, 2); process->sessionClosed(session.get()); ensure_equals(process->sessions, 1); process->sessionClosed(session2.get()); ensure_equals(process->sessions, 0); } TEST_METHOD(3) { set_test_name("newSession() checks out the socket with the smallest busyness number " "and sessionClosed() restores the session busyness statistics"); ProcessPtr process = createProcess(); // The first 3 newSession() commands check out an idle socket. SessionPtr session1 = process->newSession(); SessionPtr session2 = process->newSession(); SessionPtr session3 = process->newSession(); ensure(session1->getSocket()->address != session2->getSocket()->address); ensure(session1->getSocket()->address != session3->getSocket()->address); ensure(session2->getSocket()->address != session3->getSocket()->address); // The next 2 newSession() commands check out sockets with sessions == 1. SessionPtr session4 = process->newSession(); SessionPtr session5 = process->newSession(); ensure(session4->getSocket()->address != session5->getSocket()->address); // There should now be 1 process with 1 session // and 2 processes with 2 sessions. map<int, int> sessionCount; SocketList::const_iterator it; for (it = process->getSockets().begin(); it != process->getSockets().end(); it++) { sessionCount[it->sessions]++; } ensure_equals(sessionCount.size(), 2u); ensure_equals(sessionCount[1], 1); ensure_equals(sessionCount[2], 2); // Closing the first 3 sessions will result in no processes having 1 session // and 1 process having 2 sessions. process->sessionClosed(session1.get()); process->sessionClosed(session2.get()); process->sessionClosed(session3.get()); sessionCount.clear(); for (it = process->getSockets().begin(); it != process->getSockets().end(); it++) { sessionCount[it->sessions]++; } ensure_equals(sessionCount[0], 1); ensure_equals(sessionCount[1], 2); } TEST_METHOD(4) { set_test_name("If all sockets are at their full capacity then newSession() will fail"); ProcessPtr process = createProcess(); vector<SessionPtr> sessions; for (int i = 0; i < 9; i++) { ensure(!process->isTotallyBusy()); SessionPtr session = process->newSession(); ensure(session != NULL); sessions.push_back(session); } ensure(process->isTotallyBusy()); ensure(process->newSession() == NULL); } TEST_METHOD(5) { set_test_name("It forwards all stdout and stderr output, even after the " "Process object has been destroyed"); TempDir temp("tmp.log"); Json::Value extraArgs; extraArgs["log_file"] = "tmp.log/file"; fclose(fopen("tmp.log/file", "w")); ProcessPtr process = createProcess(extraArgs); if (defaultLogLevel == (LoggingKit::Level) DEFAULT_LOG_LEVEL) { // If the user did not customize the test's log level, // then we'll want to tone down the noise. LoggingKit::setLevel(LoggingKit::WARN); } writeExact(stdoutAndErrFd[1], "stdout and err 1\n"); writeExact(stdoutAndErrFd[1], "stdout and err 2\n"); EVENTUALLY(2, string contents = unsafeReadFile("tmp.log/file"); result = contents.find("stdout and err 1\n") != string::npos && contents.find("stdout and err 2\n") != string::npos; ); fclose(fopen("tmp.log/file", "w")); process.reset(); writeExact(stdoutAndErrFd[1], "stdout and err 3\n"); writeExact(stdoutAndErrFd[1], "stdout and err 4\n"); EVENTUALLY(2, string contents = unsafeReadFile("tmp.log/file"); result = contents.find("stdout and err 3\n") != string::npos && contents.find("stdout and err 4\n") != string::npos; ); } } ApplicationPool/PoolTest.cpp 0000644 00000176411 14756504011 0012133 0 ustar 00 #include <TestSupport.h> #include <jsoncpp/json.h> #include <Core/ApplicationPool/Pool.h> #include <LoggingKit/Context.h> #include <FileTools/FileManip.h> #include <StrIntTools/StrIntUtils.h> #include <IOTools/MessageSerialization.h> #include <map> #include <vector> #include <cerrno> #include <signal.h> using namespace std; using namespace Passenger; using namespace Passenger::ApplicationPool2; namespace tut { struct Core_ApplicationPool_PoolTest: public TestBase { WrapperRegistry::Registry wrapperRegistry; SpawningKit::Context::Schema skContextSchema; SpawningKit::Context::DebugSupport skDebugSupport; SpawningKit::Context skContext; SpawningKit::FactoryPtr skFactory; Context context; PoolPtr pool; Pool::DebugSupportPtr debug; Ticket ticket; GetCallback callback; SessionPtr currentSession; ExceptionPtr currentException; AtomicInt number; boost::mutex syncher; list<SessionPtr> sessions; bool retainSessions; Core_ApplicationPool_PoolTest() : skContext(skContextSchema) { retainSessions = false; wrapperRegistry.finalize(); skContext.resourceLocator = resourceLocator; skContext.wrapperRegistry = &wrapperRegistry; skContext.integrationMode = "standalone"; skContext.debugSupport = &skDebugSupport; skContext.spawnDir = getSystemTempDir(); skContext.finalize(); context.spawningKitFactory = boost::make_shared<SpawningKit::Factory>(&skContext); context.finalize(); pool = boost::make_shared<Pool>(&context); pool->initialize(); callback.func = _callback; callback.userData = this; Json::Value config; vector<ConfigKit::Error> errors; LoggingKit::ConfigChangeRequest req; config["level"] = "warn"; config["app_output_log_level"] = "debug"; if (LoggingKit::context->prepareConfigChange(config, errors, req)) { LoggingKit::context->commitConfigChange(req); } else { P_BUG("Error configuring LoggingKit: " << ConfigKit::toString(errors)); } } ~Core_ApplicationPool_PoolTest() { // Explicitly destroy these here because they can run // additional code that depend on other fields in this // class. TRACE_POINT(); clearAllSessions(); UPDATE_TRACE_POINT(); pool->destroy(); UPDATE_TRACE_POINT(); pool.reset(); Json::Value config; vector<ConfigKit::Error> errors; LoggingKit::ConfigChangeRequest req; config["level"] = DEFAULT_LOG_LEVEL_NAME; config["app_output_log_level"] = DEFAULT_APP_OUTPUT_LOG_LEVEL_NAME; if (LoggingKit::context->prepareConfigChange(config, errors, req)) { LoggingKit::context->commitConfigChange(req); } else { P_BUG("Error configuring LoggingKit: " << ConfigKit::toString(errors)); } SystemTime::releaseAll(); } void initPoolDebugging() { pool->initDebugging(); debug = pool->debugSupport; } void clearAllSessions() { SessionPtr myCurrentSession; list<SessionPtr> mySessions; { LockGuard l(syncher); myCurrentSession = currentSession; mySessions = sessions; currentSession.reset(); sessions.clear(); } myCurrentSession.reset(); mySessions.clear(); } Options createOptions() { Options options; options.spawnMethod = "dummy"; options.appRoot = "stub/rack"; options.appType = "ruby"; options.appStartCommand = "ruby start.rb"; options.startupFile = "start.rb"; options.loadShellEnvvars = false; options.user = testConfig["normal_user_1"].asCString(); options.defaultUser = testConfig["default_user"].asCString(); options.defaultGroup = testConfig["default_group"].asCString(); return options; } static void _callback(const AbstractSessionPtr &_session, const ExceptionPtr &e, void *userData) { Core_ApplicationPool_PoolTest *self = (Core_ApplicationPool_PoolTest *) userData; SessionPtr session = static_pointer_cast<Session>(_session); SessionPtr oldSession; { LockGuard l(self->syncher); oldSession = self->currentSession; self->currentSession = session; self->currentException = e; self->number++; if (self->retainSessions && session != NULL) { self->sessions.push_back(session); } } // destroy old session object outside the lock. } void sendHeaders(int connection, ...) { va_list ap; const char *arg; vector<StaticString> args; va_start(ap, connection); while ((arg = va_arg(ap, const char *)) != NULL) { args.push_back(StaticString(arg, strlen(arg) + 1)); } va_end(ap); shared_array<StaticString> args_array(new StaticString[args.size() + 1]); unsigned int totalSize = 0; for (unsigned int i = 0; i < args.size(); i++) { args_array[i + 1] = args[i]; totalSize += args[i].size(); } char sizeHeader[sizeof(uint32_t)]; Uint32Message::generate(sizeHeader, totalSize); args_array[0] = StaticString(sizeHeader, sizeof(uint32_t)); gatheredWrite(connection, args_array.get(), args.size() + 1, NULL); } string stripHeaders(const string &str) { string::size_type pos = str.find("\r\n\r\n"); if (pos == string::npos) { return str; } else { string result = str; result.erase(0, pos + 4); return result; } } string sendRequest(const Options &options, const char *path) { int oldNumber = number; pool->asyncGet(options, callback); EVENTUALLY(5, result = number == oldNumber + 1; ); if (currentException != NULL) { P_ERROR("get() exception: " << currentException->what()); abort(); } currentSession->initiate(); sendHeaders(currentSession->fd(), "PATH_INFO", path, "REQUEST_METHOD", "GET", NULL); shutdown(currentSession->fd(), SHUT_WR); string body = stripHeaders(readAll(currentSession->fd(), 1024 * 1024).first); ProcessPtr process = currentSession->getProcess()->shared_from_this(); currentSession.reset(); EVENTUALLY(5, result = process->busyness() == 0; ); return body; } // Ensure that n processes exist. Options ensureMinProcesses(unsigned int n) { Options options = createOptions(); options.minProcesses = n; pool->asyncGet(options, callback); EVENTUALLY(5, result = number == 1; ); EVENTUALLY(5, result = pool->getProcessCount() == n; ); currentSession.reset(); return options; } void disableProcess(ProcessPtr process, AtomicInt *result) { *result = (int) pool->disableProcess(process->getGupid()); } }; DEFINE_TEST_GROUP_WITH_LIMIT(Core_ApplicationPool_PoolTest, 100); TEST_METHOD(1) { // Test initial state. ensure(!pool->atFullCapacity()); } /*********** Test asyncGet() behavior on a single Group ***********/ TEST_METHOD(2) { // asyncGet() actions on empty pools cannot be immediately satisfied. // Instead a new process will be spawned. In the mean time get() // actions are put on a wait list which will be processed as soon // as the new process is done spawning. Options options = createOptions(); ScopedLock l(pool->syncher); pool->asyncGet(options, callback, false); ensure_equals("(1)", number, 0); ensure("(2)", pool->getWaitlist.empty()); ensure("(3)", !pool->groups.empty()); l.unlock(); EVENTUALLY(5, result = pool->getProcessCount() == 1; ); EVENTUALLY(5, result = number == 1; ); ensure("(4)", currentSession != NULL); ensure("(5)", currentException == NULL); } TEST_METHOD(3) { // If one matching process already exists and it's not at full // capacity then asyncGet() will immediately use it. Options options = createOptions(); // Spawn a process and opens a session with it. pool->asyncGet(options, callback); EVENTUALLY(5, result = number == 1; ); // Close the session so that the process is now idle. ProcessPtr process = currentSession->getProcess()->shared_from_this(); currentSession.reset(); ensure_equals(process->busyness(), 0); ensure(!process->isTotallyBusy()); // Verify test assertion. ScopedLock l(pool->syncher); pool->asyncGet(options, callback, false); ensure_equals("callback is immediately called", number, 2); } TEST_METHOD(4) { // If one matching process already exists but it's at full capacity, // and the limits prevent spawning of a new process, // then asyncGet() will put the get action on the group's wait // queue. When the process is no longer at full capacity it will // process the request. // Spawn a process and verify that it's at full capacity. // Keep its session open. Options options = createOptions(); options.appGroupName = "test"; pool->setMax(1); pool->asyncGet(options, callback); EVENTUALLY(5, result = number == 1; ); SessionPtr session1 = currentSession; ProcessPtr process = session1->getProcess()->shared_from_this(); currentSession.reset(); ensure_equals(process->sessions, 1); ensure(process->isTotallyBusy()); // Now call asyncGet() again. pool->asyncGet(options, callback); ensure_equals("callback is not yet called", number, 1); ensure_equals("the get action has been put on the wait list", pool->groups.lookupCopy("test")->getWaitlist.size(), 1u); session1.reset(); ensure_equals("callback is called after the process becomes idle", number, 2); ensure_equals("the get wait list has been processed", pool->groups.lookupCopy("test")->getWaitlist.size(), 0u); ensure_equals(process->sessions, 1); } TEST_METHOD(5) { // If one matching process already exists but it's at full utilization, // and the limits and pool capacity allow spawning of a new process, // then get() will put the get action on the group's wait // queue while spawning a process in the background. // Either the existing process or the newly spawned process // will process the action, whichever becomes first available. // Here we test the case in which the existing process becomes // available first. initPoolDebugging(); // Spawn a regular process and keep its session open. Options options = createOptions(); debug->messages->send("Proceed with spawn loop iteration 1"); SessionPtr session1 = pool->get(options, &ticket); ProcessPtr process1 = session1->getProcess()->shared_from_this(); // Now spawn a process that never finishes. pool->asyncGet(options, callback); // Release the session on the first process. session1.reset(); EVENTUALLY(1, result = number == 1; ); ensure_equals("The first process handled the second asyncGet() request", currentSession->getProcess(), process1.get()); debug->messages->send("Proceed with spawn loop iteration 2"); EVENTUALLY(5, result = number == 1; ); } TEST_METHOD(6) { // Here we test the case in which the new process becomes // available first. // Spawn a regular process. Options options = createOptions(); pool->asyncGet(options, callback); EVENTUALLY(5, result = number == 1; ); SessionPtr session1 = currentSession; ProcessPtr process1 = currentSession->getProcess()->shared_from_this(); currentSession.reset(); // As long as we don't release process1 the following get // action will be processed by the newly spawned process. pool->asyncGet(options, callback); EVENTUALLY(5, result = pool->getProcessCount() == 2; ); ensure_equals(number, 2); ensure(currentSession->getProcess() != process1.get()); } TEST_METHOD(7) { // If multiple matching processes exist, and one of them is idle, // then asyncGet() will use that. // Spawn 3 processes and keep a session open with 1 of them. Options options = createOptions(); options.minProcesses = 3; pool->asyncGet(options, callback); EVENTUALLY(5, result = number == 1; ); EVENTUALLY(5, result = pool->getProcessCount() == 3; ); SessionPtr session1 = currentSession; ProcessPtr process1 = currentSession->getProcess()->shared_from_this(); currentSession.reset(); // Now open another session. It should complete immediately // and should not use the first process. ScopedLock l(pool->syncher); pool->asyncGet(options, callback, false); ensure_equals("asyncGet() completed immediately", number, 2); SessionPtr session2 = currentSession; ProcessPtr process2 = currentSession->getProcess()->shared_from_this(); l.unlock(); currentSession.reset(); ensure(process2 != process1); // Now open yet another session. It should also complete immediately // and should not use the first or the second process. l.lock(); pool->asyncGet(options, callback, false); ensure_equals("asyncGet() completed immediately", number, 3); SessionPtr session3 = currentSession; ProcessPtr process3 = currentSession->getProcess()->shared_from_this(); l.unlock(); currentSession.reset(); ensure(process3 != process1); ensure(process3 != process2); } TEST_METHOD(8) { // If multiple matching processes exist, then asyncGet() will use // the one with the smallest utilization number. // Spawn 2 processes, each with a concurrency of 2. skDebugSupport.dummyConcurrency = 2; Options options = createOptions(); options.minProcesses = 2; pool->setMax(2); GroupPtr group = pool->findOrCreateGroup(options); { LockGuard l(pool->syncher); group->spawn(); } EVENTUALLY(5, result = pool->getProcessCount() == 2; ); // asyncGet() selects some process. pool->asyncGet(options, callback); ensure_equals("(1)", number, 1); SessionPtr session1 = currentSession; ProcessPtr process1 = currentSession->getProcess()->shared_from_this(); currentSession.reset(); // The first process now has 1 session, so next asyncGet() should // select the other process. pool->asyncGet(options, callback); ensure_equals("(2)", number, 2); SessionPtr session2 = currentSession; ProcessPtr process2 = currentSession->getProcess()->shared_from_this(); currentSession.reset(); ensure("(3)", process1 != process2); // Both processes now have an equal number of sessions. Next asyncGet() // can select either. pool->asyncGet(options, callback); ensure_equals("(4)", number, 3); SessionPtr session3 = currentSession; ProcessPtr process3 = currentSession->getProcess()->shared_from_this(); currentSession.reset(); // One process now has the lowest number of sessions. Next // asyncGet() should select that one. pool->asyncGet(options, callback); ensure_equals("(5)", number, 4); SessionPtr session4 = currentSession; ProcessPtr process4 = currentSession->getProcess()->shared_from_this(); currentSession.reset(); ensure("(6)", process3 != process4); } TEST_METHOD(9) { // If multiple matching processes exist, and all of them are at full capacity, // and no more processes may be spawned, // then asyncGet() will put the action on the group's wait queue. // The process that first becomes not at full capacity will process the action. // Spawn 2 processes and open 4 sessions. Options options = createOptions(); options.appGroupName = "test"; options.minProcesses = 2; pool->setMax(2); skDebugSupport.dummyConcurrency = 2; vector<SessionPtr> sessions; int expectedNumber = 1; for (int i = 0; i < 4; i++) { pool->asyncGet(options, callback); EVENTUALLY(5, result = number == expectedNumber; ); expectedNumber++; sessions.push_back(currentSession); currentSession.reset(); } EVENTUALLY(5, result = pool->getProcessCount() == 2; ); GroupPtr group = pool->groups.lookupCopy("test"); ensure_equals(group->getWaitlist.size(), 0u); ensure(pool->atFullCapacity()); // Now try to open another session. pool->asyncGet(options, callback); ensure_equals("The get request has been put on the wait list", pool->groups.lookupCopy("test")->getWaitlist.size(), 1u); // Close an existing session so that one process is no // longer at full utilization. sessions[0].reset(); ensure_equals("The get request has been removed from the wait list", pool->groups.lookupCopy("test")->getWaitlist.size(), 0u); ensure(pool->atFullCapacity()); } TEST_METHOD(10) { // If multiple matching processes exist, and all of them are at full utilization, // and a new process may be spawned, // then asyncGet() will put the action on the group's wait queue and spawn the // new process. // The process that first becomes not at full utilization // or the newly spawned process // will process the action, whichever is earlier. // Here we test the case where an existing process is earlier. // Spawn 2 processes and open 4 sessions. skDebugSupport.dummyConcurrency = 2; Options options = createOptions(); options.minProcesses = 2; pool->setMax(3); GroupPtr group = pool->findOrCreateGroup(options); vector<SessionPtr> sessions; int expectedNumber = 1; for (int i = 0; i < 4; i++) { pool->asyncGet(options, callback); EVENTUALLY(5, result = number == expectedNumber; ); expectedNumber++; sessions.push_back(currentSession); currentSession.reset(); } EVENTUALLY(5, result = pool->getProcessCount() == 2; ); // The next asyncGet() should spawn a new process and the action should be queued. ScopedLock l(pool->syncher); skDebugSupport.dummySpawnDelay = 5000000; pool->asyncGet(options, callback, false); ensure(group->spawning()); ensure_equals(group->getWaitlist.size(), 1u); l.unlock(); // Close one of the sessions. Now it will process the action. ProcessPtr process = sessions[0]->getProcess()->shared_from_this(); sessions[0].reset(); ensure_equals(number, 5); ensure_equals(currentSession->getProcess(), process.get()); ensure_equals(group->getWaitlist.size(), 0u); ensure_equals(pool->getProcessCount(), 2u); } TEST_METHOD(11) { // Here we test the case where the newly spawned process is earlier. // Spawn 2 processes and open 4 sessions. Options options = createOptions(); options.minProcesses = 2; pool->setMax(3); GroupPtr group = pool->findOrCreateGroup(options); skDebugSupport.dummyConcurrency = 2; vector<SessionPtr> sessions; int expectedNumber = 1; for (int i = 0; i < 4; i++) { pool->asyncGet(options, callback); EVENTUALLY(5, result = number == expectedNumber; ); expectedNumber++; sessions.push_back(currentSession); currentSession.reset(); } EVENTUALLY(5, result = pool->getProcessCount() == 2; ); // The next asyncGet() should spawn a new process. After it's done // spawning it will process the action. pool->asyncGet(options, callback); EVENTUALLY(5, result = pool->getProcessCount() == 3; ); EVENTUALLY(5, result = number == 5; ); ensure_equals(currentSession->getProcess()->getPid(), 3); ensure_equals(group->getWaitlist.size(), 0u); } TEST_METHOD(12) { // Test shutting down. ensureMinProcesses(2); ensure(pool->detachGroupByName("stub/rack")); ensure_equals(pool->getGroupCount(), 0u); } TEST_METHOD(13) { // Test shutting down while Group is restarting. initPoolDebugging(); debug->messages->send("Proceed with spawn loop iteration 1"); ensureMinProcesses(1); ensure(pool->restartGroupByName("stub/rack")); debug->debugger->recv("About to end restarting"); ensure(pool->detachGroupByName("stub/rack")); ensure_equals(pool->getGroupCount(), 0u); } TEST_METHOD(14) { // Test shutting down while Group is spawning. initPoolDebugging(); Options options = createOptions(); pool->asyncGet(options, callback); debug->debugger->recv("Begin spawn loop iteration 1"); ensure(pool->detachGroupByName("stub/rack")); ensure_equals(pool->getGroupCount(), 0u); } TEST_METHOD(17) { // Test that restartGroupByName() spawns more processes to ensure // that minProcesses and other constraints are met. ensureMinProcesses(1); ensure(pool->restartGroupByName("stub/rack")); EVENTUALLY(5, result = pool->getProcessCount() == 1; ); } TEST_METHOD(18) { // Test getting from an app for which minProcesses is set to 0, // and restart.txt already existed. TempDirCopy dir("stub/wsgi", "tmp.wsgi"); Options options = createOptions(); options.appRoot = "tmp.wsgi"; options.appType = "wsgi"; options.startupFile = "passenger_wsgi.py"; options.spawnMethod = "direct"; options.minProcesses = 0; initPoolDebugging(); debug->spawning = false; SystemTime::forceAll(1000000); pool->get(options, &ticket); SystemTime::forceAll(20000000); touchFile("tmp.wsgi/tmp/restart.txt", 1); pool->asyncGet(options, callback); debug->debugger->recv("About to end restarting"); debug->messages->send("Finish restarting"); EVENTUALLY(5, result = number == 1; ); ensure_equals(pool->getProcessCount(), 1u); } /*********** Test asyncGet() behavior on multiple Groups ***********/ TEST_METHOD(20) { // If the pool is full, and one tries to asyncGet() from a nonexistant group, // then it will kill the oldest idle process and spawn a new process. Options options = createOptions(); pool->setMax(2); // Get from /foo and close its session immediately. options.appRoot = "/foo"; pool->asyncGet(options, callback); EVENTUALLY(5, result = number == 1; ); ProcessPtr process1 = currentSession->getProcess()->shared_from_this(); GroupPtr group1 = process1->getGroup()->shared_from_this(); currentSession.reset(); // Get from /bar and keep its session open. options.appRoot = "/bar"; pool->asyncGet(options, callback); EVENTUALLY(5, result = number == 2; ); SessionPtr session2 = currentSession; currentSession.reset(); // Get from /baz. The process for /foo should be killed now. options.appRoot = "/baz"; pool->asyncGet(options, callback); EVENTUALLY(5, result = number == 3; ); ensure_equals(pool->getProcessCount(), 2u); ensure_equals(group1->getProcessCount(), 0u); } TEST_METHOD(21) { // If the pool is full, and one tries to asyncGet() from a nonexistant group, // and all existing processes are non-idle, then it will // kill the oldest process and spawn a new process. Options options = createOptions(); pool->setMax(2); // Get from /foo and close its session immediately. options.appRoot = "/foo"; pool->asyncGet(options, callback); EVENTUALLY(5, result = number == 1; ); ProcessPtr process1 = currentSession->getProcess()->shared_from_this(); GroupPtr group1 = process1->getGroup()->shared_from_this(); // Get from /bar and keep its session open. options.appRoot = "/bar"; pool->asyncGet(options, callback); EVENTUALLY(5, result = number == 2; ); SessionPtr session2 = currentSession; currentSession.reset(); // Get from /baz. The process for /foo should be killed now. options.appRoot = "/baz"; pool->asyncGet(options, callback); EVENTUALLY(5, result = number == 3; ); ensure_equals(pool->getProcessCount(), 2u); ensure_equals(group1->getProcessCount(), 0u); } TEST_METHOD(22) { // Suppose the pool is at full capacity, and one tries to asyncGet() from an // existant group that does not have any processes. It should kill a process // from another group, and the request should succeed. Options options = createOptions(); SessionPtr session; pid_t pid1, pid2; pool->setMax(1); // Create a group /foo. options.appRoot = "/foo"; SystemTime::force(1); session = pool->get(options, &ticket); pid1 = session->getPid(); session.reset(); // Create a group /bar. options.appRoot = "/bar"; SystemTime::force(2); session = pool->get(options, &ticket); pid2 = session->getPid(); session.reset(); // Sleep for a short while to give Pool a chance to shutdown // the first process. usleep(300000); ensure_equals("(1)", pool->getProcessCount(), 1u); // Get from /foo. options.appRoot = "/foo"; SystemTime::force(3); session = pool->get(options, &ticket); ensure("(2)", session->getPid() != pid1); ensure("(3)", session->getPid() != pid2); ensure_equals("(4)", pool->getProcessCount(), 1u); } TEST_METHOD(23) { // Suppose the pool is at full capacity, and one tries to asyncGet() from an // existant group that does not have any processes, and that happens to need // restarting. It should kill a process from another group and the request // should succeed. Options options1 = createOptions(); Options options2 = createOptions(); TempDirCopy dir("stub/wsgi", "tmp.wsgi"); SessionPtr session; pid_t pid1, pid2; pool->setMax(1); // Create a group tmp.wsgi. options1.appRoot = "tmp.wsgi"; options1.appType = "wsgi"; options1.startupFile = "passenger_wsgi.py"; options1.spawnMethod = "direct"; SystemTime::force(1); session = pool->get(options1, &ticket); pid1 = session->getPid(); session.reset(); // Create a group bar. options2.appRoot = "bar"; SystemTime::force(2); session = pool->get(options2, &ticket); pid2 = session->getPid(); session.reset(); // Sleep for a short while to give Pool a chance to shutdown // the first process. usleep(300000); ensure_equals("(1)", pool->getProcessCount(), 1u); // Get from tmp.wsgi. SystemTime::force(3); touchFile("tmp.wsgi/tmp/restart.txt", 4); session = pool->get(options1, &ticket); ensure("(2)", session->getPid() != pid1); ensure("(3)", session->getPid() != pid2); ensure_equals("(4)", pool->getProcessCount(), 1u); } TEST_METHOD(24) { // Suppose the pool is at full capacity, with two groups: // - one that is spawning a process. // - one with no processes. // When one tries to asyncGet() from the second group, there should // be no process to kill, but when the first group is done spawning // it should throw away that process immediately to allow the second // group to spawn. Options options1 = createOptions(); Options options2 = createOptions(); initPoolDebugging(); debug->restarting = false; pool->setMax(1); // Create a group foo. options1.appRoot = "foo"; options1.noop = true; SystemTime::force(1); pool->get(options1, &ticket); // Create a group bar, but don't let it finish spawning. options2.appRoot = "bar"; options2.noop = true; SystemTime::force(2); GroupPtr barGroup = pool->get(options2, &ticket)->getGroup()->shared_from_this(); { LockGuard l(pool->syncher); ensure_equals("(1)", barGroup->spawn(), SR_OK); } debug->debugger->recv("Begin spawn loop iteration 1"); // Now get from foo again and let the request be queued. options1.noop = false; SystemTime::force(3); pool->asyncGet(options1, callback); // Nothing should happen while bar is spawning. SHOULD_NEVER_HAPPEN(100, result = number > 0; ); ensure_equals("(2)", pool->getProcessCount(), 0u); // Now let bar finish spawning. Eventually there should // only be one process: the one for foo. debug->messages->send("Proceed with spawn loop iteration 1"); debug->debugger->recv("Spawn loop done"); debug->messages->send("Proceed with spawn loop iteration 2"); debug->debugger->recv("Spawn loop done"); EVENTUALLY(5, LockGuard l(pool->syncher); vector<ProcessPtr> processes = pool->getProcesses(false); if (processes.size() == 1) { GroupPtr group = processes[0]->getGroup()->shared_from_this(); result = group->getName() == "foo"; } else { result = false; } ); } TEST_METHOD(25) { // Suppose the pool is at full capacity, with two groups: // - one that is spawning a process, and has a queued request. // - one with no processes. // When one tries to asyncGet() from the second group, there should // be no process to kill, but when the first group is done spawning // it should throw away that process immediately to allow the second // group to spawn. Options options1 = createOptions(); Options options2 = createOptions(); initPoolDebugging(); debug->restarting = false; pool->setMax(1); // Create a group foo. options1.appRoot = "foo"; options1.noop = true; SystemTime::force(1); pool->get(options1, &ticket); // Create a group bar with a queued request, but don't let it finish spawning. options2.appRoot = "bar"; SystemTime::force(2); pool->asyncGet(options2, callback); debug->debugger->recv("Begin spawn loop iteration 1"); // Now get from foo again and let the request be queued. options1.noop = false; SystemTime::force(3); pool->asyncGet(options1, callback); // Nothing should happen while bar is spawning. SHOULD_NEVER_HAPPEN(100, result = number > 0; ); ensure_equals("(1)", pool->getProcessCount(), 0u); // Now let bar finish spawning. The request for bar should be served. debug->messages->send("Proceed with spawn loop iteration 1"); debug->debugger->recv("Spawn loop done"); EVENTUALLY(5, result = number == 1; ); ensure_equals(currentSession->getGroup()->getName(), "bar"); // When that request is done, the process for bar should be killed, // and a process for foo should be spawned. currentSession.reset(); debug->messages->send("Proceed with spawn loop iteration 2"); debug->debugger->recv("Spawn loop done"); EVENTUALLY(5, LockGuard l(pool->syncher); vector<ProcessPtr> processes = pool->getProcesses(false); if (processes.size() == 1) { GroupPtr group = processes[0]->getGroup()->shared_from_this(); result = group->getName() == "foo"; } else { result = false; } ); EVENTUALLY(5, result = number == 2; ); } /*********** Test detachProcess() ***********/ TEST_METHOD(30) { // detachProcess() detaches the process from the group. The pool // will restore the minimum number of processes afterwards. Options options = createOptions(); options.appGroupName = "test"; options.minProcesses = 2; pool->asyncGet(options, callback); EVENTUALLY(5, result = pool->getProcessCount() == 2; ); EVENTUALLY(5, result = number == 1; ); ProcessPtr process = currentSession->getProcess()->shared_from_this(); pool->detachProcess(process); { LockGuard l(pool->syncher); ensure(process->enabled == Process::DETACHED); } EVENTUALLY(5, result = pool->getProcessCount() == 2; ); currentSession.reset(); EVENTUALLY(5, result = process->isDead(); ); } TEST_METHOD(31) { // If the containing group had waiters on it, and detachProcess() // detaches the only process in the group, then a new process // is automatically spawned to handle the waiters. Options options = createOptions(); options.appGroupName = "test"; pool->setMax(1); skDebugSupport.dummySpawnDelay = 1000000; pool->asyncGet(options, callback); EVENTUALLY(5, result = number == 1; ); SessionPtr session1 = currentSession; currentSession.reset(); pool->asyncGet(options, callback); { LockGuard l(pool->syncher); ensure_equals(pool->groups.lookupCopy("test")->getWaitlist.size(), 1u); } pool->detachProcess(session1->getProcess()->shared_from_this()); { LockGuard l(pool->syncher); ensure(pool->groups.lookupCopy("test")->spawning()); ensure_equals(pool->groups.lookupCopy("test")->enabledCount, 0); ensure_equals(pool->groups.lookupCopy("test")->getWaitlist.size(), 1u); } EVENTUALLY(5, result = number == 2; ); } TEST_METHOD(32) { // If the pool had waiters on it then detachProcess() will // automatically create the Groups that were requested // by the waiters. Options options = createOptions(); options.appGroupName = "test"; options.minProcesses = 0; pool->setMax(1); skDebugSupport.dummySpawnDelay = 30000; // Begin spawning a process. pool->asyncGet(options, callback); ensure(pool->atFullCapacity()); // asyncGet() on another group should now put it on the waiting list. Options options2 = createOptions(); options2.appGroupName = "test2"; options2.minProcesses = 0; skDebugSupport.dummySpawnDelay = 90000; pool->asyncGet(options2, callback); { LockGuard l(pool->syncher); ensure_equals(pool->getWaitlist.size(), 1u); } // Eventually the dummy process for "test" is now done spawning. // We then detach it. EVENTUALLY(5, result = number == 1; ); SessionPtr session1 = currentSession; currentSession.reset(); pool->detachProcess(session1->getProcess()->shared_from_this()); { LockGuard l(pool->syncher); ensure(pool->groups.lookupCopy("test2") != NULL); ensure_equals(pool->getWaitlist.size(), 0u); } EVENTUALLY(5, result = number == 2; ); } TEST_METHOD(33) { // A Group does not become garbage collectable // after detaching all its processes. Options options = createOptions(); pool->asyncGet(options, callback); EVENTUALLY(5, result = number == 1; ); ProcessPtr process = currentSession->getProcess()->shared_from_this(); currentSession.reset(); GroupPtr group = process->getGroup()->shared_from_this(); pool->detachProcess(process); LockGuard l(pool->syncher); ensure_equals(pool->groups.size(), 1u); ensure(group->isAlive()); ensure(!group->garbageCollectable()); } TEST_METHOD(34) { // When detaching a process, it waits until all sessions have // finished before telling the process to shut down. Options options = createOptions(); options.spawnMethod = "direct"; options.minProcesses = 0; SessionPtr session = pool->get(options, &ticket); ProcessPtr process = session->getProcess()->shared_from_this(); ensure(pool->detachProcess(process)); { LockGuard l(pool->syncher); ensure_equals(process->enabled, Process::DETACHED); } SHOULD_NEVER_HAPPEN(100, LockGuard l(pool->syncher); result = !process->isAlive() || !process->osProcessExists(); ); session.reset(); EVENTUALLY(1, LockGuard l(pool->syncher); result = process->enabled == Process::DETACHED && !process->osProcessExists() && process->isDead(); ); } TEST_METHOD(35) { // When detaching a process, it waits until the OS processes // have exited before cleaning up the in-memory data structures. Options options = createOptions(); options.spawnMethod = "direct"; options.minProcesses = 0; ProcessPtr process = pool->get(options, &ticket)->getProcess()->shared_from_this(); ScopeGuard g(boost::bind(::kill, process->getPid(), SIGCONT)); kill(process->getPid(), SIGSTOP); ensure(pool->detachProcess(process)); { LockGuard l(pool->syncher); ensure_equals(process->enabled, Process::DETACHED); } EVENTUALLY(1, result = process->getLifeStatus() == Process::SHUTDOWN_TRIGGERED; ); SHOULD_NEVER_HAPPEN(100, LockGuard l(pool->syncher); result = process->isDead() || !process->osProcessExists(); ); kill(process->getPid(), SIGCONT); g.clear(); EVENTUALLY(1, LockGuard l(pool->syncher); result = process->enabled == Process::DETACHED && !process->osProcessExists() && process->isDead(); ); } TEST_METHOD(36) { // Detaching a process that is already being detached, works. Options options = createOptions(); options.appGroupName = "test"; options.minProcesses = 0; initPoolDebugging(); debug->restarting = false; debug->spawning = false; debug->detachedProcessesChecker = true; pool->asyncGet(options, callback); EVENTUALLY(5, result = pool->getProcessCount() == 1; ); EVENTUALLY(5, result = number == 1; ); ProcessPtr process = currentSession->getProcess()->shared_from_this(); pool->detachProcess(process); debug->debugger->recv("About to start detached processes checker"); { LockGuard l(pool->syncher); ensure(process->enabled == Process::DETACHED); } // detachProcess() will spawn a new process. Prevent it from being // spawned too soon. debug->spawning = true; pool->detachProcess(process); debug->messages->send("Proceed with starting detached processes checker"); debug->messages->send("Proceed with starting detached processes checker"); debug->messages->send("Proceed with spawn loop iteration 2"); EVENTUALLY(5, result = pool->getProcessCount() == 0; ); currentSession.reset(); EVENTUALLY(5, result = process->isDead(); ); } /*********** Test disabling and enabling processes ***********/ TEST_METHOD(40) { // Disabling a process under idle conditions should succeed immediately. ensureMinProcesses(2); vector<ProcessPtr> processes = pool->getProcesses(); ensure_equals("Disabling succeeds", pool->disableProcess(processes[0]->getGupid()), DR_SUCCESS); LockGuard l(pool->syncher); ensure(processes[0]->isAlive()); ensure_equals("Process is disabled", processes[0]->enabled, Process::DISABLED); ensure("Other processes are not affected", processes[1]->isAlive()); ensure_equals("Other processes are not affected", processes[1]->enabled, Process::ENABLED); } TEST_METHOD(41) { // Disabling the sole process in a group, in case the pool settings allow // spawning another process, should trigger a new process spawn. ensureMinProcesses(1); Options options = createOptions(); SessionPtr session = pool->get(options, &ticket); ensure_equals(pool->getProcessCount(), 1u); ensure(!pool->isSpawning()); skDebugSupport.dummySpawnDelay = 60000; AtomicInt code = -1; TempThread thr(boost::bind(&Core_ApplicationPool_PoolTest::disableProcess, this, session->getProcess()->shared_from_this(), &code)); EVENTUALLY2(100, 1, result = pool->isSpawning(); ); EVENTUALLY(1, result = pool->getProcessCount() == 2u; ); ensure_equals((int) code, -1); session.reset(); EVENTUALLY(1, result = code == (int) DR_SUCCESS; ); } TEST_METHOD(42) { // Disabling the sole process in a group, in case pool settings don't allow // spawning another process, should fail. pool->setMax(1); ensureMinProcesses(1); vector<ProcessPtr> processes = pool->getProcesses(); ensure_equals("(1)", processes.size(), 1u); if (defaultLogLevel == (LoggingKit::Level) DEFAULT_LOG_LEVEL) { // If the user did not customize the test's log level, // then we'll want to tone down the noise. LoggingKit::setLevel(LoggingKit::ERROR); } DisableResult result = pool->disableProcess(processes[0]->getGupid()); ensure_equals("(2)", result, DR_ERROR); ensure_equals("(3)", pool->getProcessCount(), 1u); } TEST_METHOD(43) { // If there are no enabled processes in the group, then disabling should // succeed after the new process has been spawned. initPoolDebugging(); debug->messages->send("Proceed with spawn loop iteration 1"); debug->messages->send("Proceed with spawn loop iteration 2"); Options options = createOptions(); SessionPtr session1 = pool->get(options, &ticket); SessionPtr session2 = pool->get(options, &ticket); ensure_equals(pool->getProcessCount(), 2u); GroupPtr group = session1->getGroup()->shared_from_this(); ProcessPtr process1 = session1->getProcess()->shared_from_this(); ProcessPtr process2 = session2->getProcess()->shared_from_this(); AtomicInt code1 = -1, code2 = -2; TempThread thr(boost::bind(&Core_ApplicationPool_PoolTest::disableProcess, this, process1, &code1)); TempThread thr2(boost::bind(&Core_ApplicationPool_PoolTest::disableProcess, this, process2, &code2)); EVENTUALLY(5, LockGuard l(pool->syncher); result = group->enabledCount == 0 && group->disablingCount == 2 && group->disabledCount == 0; ); session1.reset(); session2.reset(); SHOULD_NEVER_HAPPEN(20, result = code1 != -1 || code2 != -2; ); debug->messages->send("Proceed with spawn loop iteration 3"); EVENTUALLY(5, result = code1 == DR_SUCCESS; ); EVENTUALLY(5, result = code2 == DR_SUCCESS; ); { LockGuard l(pool->syncher); ensure_equals(group->enabledCount, 1); ensure_equals(group->disablingCount, 0); ensure_equals(group->disabledCount, 2); } } TEST_METHOD(44) { // Suppose that a previous disable command triggered a new process spawn, // and the spawn fails. Then any disabling processes should become enabled // again, and the callbacks for the previous disable commands should be called. initPoolDebugging(); debug->messages->send("Proceed with spawn loop iteration 1"); debug->messages->send("Proceed with spawn loop iteration 2"); Options options = createOptions(); options.minProcesses = 2; SessionPtr session1 = pool->get(options, &ticket); SessionPtr session2 = pool->get(options, &ticket); ensure_equals(pool->getProcessCount(), 2u); AtomicInt code1 = -1, code2 = -1; TempThread thr(boost::bind(&Core_ApplicationPool_PoolTest::disableProcess, this, session1->getProcess()->shared_from_this(), &code1)); TempThread thr2(boost::bind(&Core_ApplicationPool_PoolTest::disableProcess, this, session2->getProcess()->shared_from_this(), &code2)); EVENTUALLY(2, GroupPtr group = session1->getGroup()->shared_from_this(); LockGuard l(pool->syncher); result = group->enabledCount == 0 && group->disablingCount == 2 && group->disabledCount == 0; ); SHOULD_NEVER_HAPPEN(20, result = code1 != -1 || code2 != -1; ); if (defaultLogLevel == (LoggingKit::Level) DEFAULT_LOG_LEVEL) { // If the user did not customize the test's log level, // then we'll want to tone down the noise. LoggingKit::setLevel(LoggingKit::CRIT); } debug->messages->send("Fail spawn loop iteration 3"); EVENTUALLY(5, result = code1 == DR_ERROR; ); EVENTUALLY(5, result = code2 == DR_ERROR; ); { GroupPtr group = session1->getGroup()->shared_from_this(); LockGuard l(pool->syncher); ensure_equals(group->enabledCount, 2); ensure_equals(group->disablingCount, 0); ensure_equals(group->disabledCount, 0); } } // TODO: asyncGet() should not select a disabling process if there are enabled processes. // TODO: asyncGet() should not select a disabling process when non-rolling restarting. // TODO: asyncGet() should select a disabling process if there are no enabled processes // in the group. If this happens then asyncGet() will also spawn a new process. // TODO: asyncGet() should not select a disabled process. // TODO: If there are no enabled processes and all disabling processes are at full // utilization, and the process that was being spawned becomes available // earlier than any of the disabling processes, then the newly spawned process // should handle the request. // TODO: A disabling process becomes disabled as soon as it's done with // all its request. TEST_METHOD(50) { // Disabling a process that's already being disabled should result in the // callback being called after disabling is done. ensureMinProcesses(2); Options options = createOptions(); SessionPtr session = pool->get(options, &ticket); AtomicInt code = -1; TempThread thr(boost::bind(&Core_ApplicationPool_PoolTest::disableProcess, this, session->getProcess()->shared_from_this(), &code)); SHOULD_NEVER_HAPPEN(100, result = code != -1; ); session.reset(); EVENTUALLY(5, result = code != -1; ); ensure_equals(code, (int) DR_SUCCESS); } // TODO: Enabling a process that's disabled succeeds immediately. // TODO: Enabling a process that's disabling succeeds immediately. The disable // callbacks will be called with DR_CANCELED. TEST_METHOD(51) { // If the number of processes is already at maximum, then disabling // a process will cause that process to be disabled, without spawning // a new process. pool->setMax(2); ensureMinProcesses(2); vector<ProcessPtr> processes = pool->getProcesses(); ensure_equals(processes.size(), 2u); DisableResult result = pool->disableProcess( processes[0]->getGupid()); ensure_equals(result, DR_SUCCESS); { ScopedLock l(pool->syncher); GroupPtr group = processes[0]->getGroup()->shared_from_this(); ensure_equals(group->enabledCount, 1); ensure_equals(group->disablingCount, 0); ensure_equals(group->disabledCount, 1); } } /*********** Other tests ***********/ TEST_METHOD(60) { // The pool is considered to be at full capacity if and only // if all Groups are at full capacity. Options options = createOptions(); Options options2 = createOptions(); options2.appGroupName = "test"; pool->setMax(2); pool->asyncGet(options, callback); EVENTUALLY(5, result = number == 1; ); pool->asyncGet(options2, callback); EVENTUALLY(5, result = number == 2; ); ensure_equals(pool->getProcessCount(), 2u); ensure(pool->atFullCapacity()); clearAllSessions(); pool->detachGroupByName("test"); ensure(!pool->atFullCapacity()); } TEST_METHOD(61) { // If the pool is at full capacity, then increasing 'max' will cause // new processes to be spawned. Any queued get requests are processed // as those new processes become available or as existing processes // become available. Options options = createOptions(); retainSessions = true; pool->setMax(1); pool->asyncGet(options, callback); pool->asyncGet(options, callback); pool->asyncGet(options, callback); EVENTUALLY(5, result = number == 1; ); pool->setMax(4); EVENTUALLY(5, result = number == 3; ); ensure_equals(pool->getProcessCount(), 3u); } TEST_METHOD(62) { // Each spawned process has a GUPID, which can be looked up // through findProcessByGupid(). Options options = createOptions(); pool->asyncGet(options, callback); EVENTUALLY(5, result = number == 1; ); string gupid = currentSession->getProcess()->getGupid().toString(); ensure(!gupid.empty()); ensure_equals(currentSession->getProcess(), pool->findProcessByGupid(gupid).get()); } TEST_METHOD(63) { // findProcessByGupid() returns a NULL pointer if there is // no matching process. ensure(pool->findProcessByGupid("none") == NULL); } TEST_METHOD(64) { // Test process idle cleaning. Options options = createOptions(); pool->setMaxIdleTime(50000); SessionPtr session1 = pool->get(options, &ticket); SessionPtr session2 = pool->get(options, &ticket); ensure_equals(pool->getProcessCount(), 2u); session2.reset(); // One of the processes still has a session open and should // not be idle cleaned. EVENTUALLY(2, result = pool->getProcessCount() == 1; ); SHOULD_NEVER_HAPPEN(150, result = pool->getProcessCount() == 0; ); // It shouldn't clean more processes than minInstances allows. sessions.clear(); SHOULD_NEVER_HAPPEN(150, result = pool->getProcessCount() == 0; ); } TEST_METHOD(65) { // Test spawner idle cleaning. Options options = createOptions(); options.appGroupName = "test1"; Options options2 = createOptions(); options2.appGroupName = "test2"; retainSessions = true; pool->setMaxIdleTime(50000); pool->asyncGet(options, callback); pool->asyncGet(options2, callback); EVENTUALLY(2, result = number == 2; ); ensure_equals(pool->getProcessCount(), 2u); EVENTUALLY(2, SpawningKit::SpawnerPtr spawner = pool->getGroup("test1")->spawner; result = static_pointer_cast<SpawningKit::DummySpawner>(spawner)->cleanCount >= 1; ); EVENTUALLY(2, SpawningKit::SpawnerPtr spawner = pool->getGroup("test2")->spawner; result = static_pointer_cast<SpawningKit::DummySpawner>(spawner)->cleanCount >= 1; ); } TEST_METHOD(66) { // It should restart the app if restart.txt is created or updated. TempDirCopy dir("stub/wsgi", "tmp.wsgi"); Options options = createOptions(); options.appRoot = "tmp.wsgi"; options.appType = "wsgi"; options.startupFile = "passenger_wsgi.py"; options.spawnMethod = "direct"; options.statThrottleRate = 0; pool->setMax(1); // Send normal request. ensure_equals(sendRequest(options, "/"), "front page"); // Modify application; it shouldn't have effect yet. writeFile("tmp.wsgi/passenger_wsgi.py", "def application(env, start_response):\n" " start_response('200 OK', [('Content-Type', 'text/html')])\n" " return ['restarted']\n"); ensure_equals(sendRequest(options, "/"), "front page"); // Create restart.txt and send request again. The change should now be activated. touchFile("tmp.wsgi/tmp/restart.txt", 1); ensure_equals(sendRequest(options, "/"), "restarted"); // Modify application again; it shouldn't have effect yet. writeFile("tmp.wsgi/passenger_wsgi.py", "def application(env, start_response):\n" " start_response('200 OK', [('Content-Type', 'text/html')])\n" " return ['restarted 2']\n"); ensure_equals(sendRequest(options, "/"), "restarted"); // Touch restart.txt and send request again. The change should now be activated. touchFile("tmp.wsgi/tmp/restart.txt", 2); ensure_equals(sendRequest(options, "/"), "restarted 2"); } TEST_METHOD(67) { // Test spawn exceptions. TempDirCopy dir("stub/wsgi", "tmp.wsgi"); Options options = createOptions(); options.appRoot = "tmp.wsgi"; options.appType = "wsgi"; options.startupFile = "passenger_wsgi.py"; options.spawnMethod = "direct"; writeFile("tmp.wsgi/passenger_wsgi.py", "import sys\n" "sys.stderr.write('Something went wrong!')\n" "exit(1)\n"); if (defaultLogLevel == (LoggingKit::Level) DEFAULT_LOG_LEVEL) { // If the user did not customize the test's log level, // then we'll want to tone down the noise. LoggingKit::setLevel(LoggingKit::CRIT); } pool->asyncGet(options, callback); EVENTUALLY(5, result = number == 1; ); ensure(currentException != NULL); boost::shared_ptr<SpawningKit::SpawnException> e = dynamic_pointer_cast<SpawningKit::SpawnException>(currentException); ensure(e->getProblemDescriptionHTML().find("Something went wrong!") != string::npos); } TEST_METHOD(68) { // If a process fails to spawn, then it stops trying to spawn minProcesses processes. TempDirCopy dir("stub/wsgi", "tmp.wsgi"); Options options = createOptions(); options.appRoot = "tmp.wsgi"; options.appType = "wsgi"; options.startupFile = "passenger_wsgi.py"; options.spawnMethod = "direct"; options.minProcesses = 4; writeFile("tmp.wsgi/counter", "0"); chmod("tmp.wsgi/counter", 0666); // Our application starts successfully the first two times, // and fails all the other times. writeFile("tmp.wsgi/passenger_wsgi.py", "import sys\n" "def application(env, start_response):\n" " pass\n" "counter = int(open('counter', 'r').read())\n" "f = open('counter', 'w')\n" "f.write(str(counter + 1))\n" "f.close()\n" "if counter >= 2:\n" " sys.stderr.write('Something went wrong!')\n" " exit(1)\n"); if (defaultLogLevel == (LoggingKit::Level) DEFAULT_LOG_LEVEL) { // If the user did not customize the test's log level, // then we'll want to tone down the noise. LoggingKit::setLevel(LoggingKit::CRIT); } pool->asyncGet(options, callback); EVENTUALLY(10, result = number == 1; ); EVENTUALLY(10, result = pool->getProcessCount() == 2; ); EVENTUALLY(10, result = !pool->isSpawning(); ); SHOULD_NEVER_HAPPEN(500, result = pool->getProcessCount() > 2; ); } TEST_METHOD(69) { // It removes the process from the pool if session->initiate() fails. Options options = createOptions(); options.appRoot = "stub/wsgi"; options.appType = "wsgi"; options.startupFile = "passenger_wsgi.py"; options.spawnMethod = "direct"; options.minProcesses = 0; pool->asyncGet(options, callback); EVENTUALLY(5, result = number == 1; ); pid_t pid = currentSession->getPid(); kill(pid, SIGTERM); // Wait until process is gone. EVENTUALLY(5, result = kill(pid, 0) == -1 && (errno == ESRCH || errno == EPERM || errno == ECHILD); ); try { currentSession->initiate(); fail("Initiate is supposed to fail"); } catch (const SystemException &e) { ensure_equals(e.code(), ECONNREFUSED); } ensure_equals(pool->getProcessCount(), 0u); } TEST_METHOD(70) { // When a process has become idle, and there are waiters on the pool, // consider detaching it in order to satisfy a waiter. Options options1 = createOptions(); Options options2 = createOptions(); options2.appRoot = "stub/wsgi"; retainSessions = true; pool->setMax(2); pool->asyncGet(options1, callback); pool->asyncGet(options1, callback); EVENTUALLY(3, result = pool->getProcessCount() == 2; ); pool->asyncGet(options2, callback); ensure_equals(pool->getWaitlist.size(), 1u); ensure_equals(number, 2); currentSession.reset(); sessions.pop_front(); EVENTUALLY(3, result = number == 3; ); ensure_equals(pool->getProcessCount(), 2u); GroupPtr group1 = pool->groups.lookupCopy("stub/rack"); GroupPtr group2 = pool->groups.lookupCopy("stub/rack"); ensure_equals(group1->enabledCount, 1); ensure_equals(group2->enabledCount, 1); } TEST_METHOD(71) { // A process is detached after processing maxRequests sessions. Options options = createOptions(); options.minProcesses = 0; options.maxRequests = 5; pool->setMax(1); SessionPtr session = pool->get(options, &ticket); ensure_equals(pool->getProcessCount(), 1u); pid_t origPid = session->getPid(); session.reset(); for (int i = 0; i < 3; i++) { pool->get(options, &ticket).reset(); ensure_equals(pool->getProcessCount(), 1u); ensure_equals(pool->getProcesses()[0]->getPid(), origPid); } pool->get(options, &ticket).reset(); EVENTUALLY(2, result = pool->getProcessCount() == 0; ); } TEST_METHOD(72) { // If we restart while spawning is in progress, and the restart // finishes before the process is done spawning, then that // process will not be attached and the original spawn loop will // abort. A new spawn loop will start to ensure that resource // constraints are met. TempDirCopy dir("stub/wsgi", "tmp.wsgi"); initPoolDebugging(); Options options = createOptions(); options.appRoot = "tmp.wsgi"; options.minProcesses = 3; options.statThrottleRate = 0; // Trigger spawn loop and freeze it at the point where it's spawning // the second process. pool->asyncGet(options, callback); debug->debugger->recv("Begin spawn loop iteration 1"); debug->messages->send("Proceed with spawn loop iteration 1"); debug->debugger->recv("Begin spawn loop iteration 2"); ensure_equals("(1)", pool->getProcessCount(), 1u); // Trigger restart, wait until it's finished. touchFile("tmp.wsgi/tmp/restart.txt", 1); pool->asyncGet(options, callback); debug->messages->send("Finish restarting"); debug->debugger->recv("Restarting done"); ensure_equals("(2)", pool->getProcessCount(), 0u); // The restarter should have created a new spawn loop and // instructed the old one to stop. debug->debugger->recv("Begin spawn loop iteration 3"); // We let the old spawn loop continue, which should drop // the second process and abort. debug->messages->send("Proceed with spawn loop iteration 2"); debug->debugger->recv("Spawn loop done"); ensure_equals("(3)", pool->getProcessCount(), 0u); // We let the new spawn loop continue. debug->messages->send("Proceed with spawn loop iteration 3"); debug->messages->send("Proceed with spawn loop iteration 4"); debug->messages->send("Proceed with spawn loop iteration 5"); debug->debugger->recv("Spawn loop done"); ensure_equals("(4)", pool->getProcessCount(), 3u); } TEST_METHOD(73) { // If a get() request comes in while the restart is in progress, then // that get() request will be put into the get waiters list, which will // be processed after spawning is done. // Spawn 2 processes. TempDirCopy dir("stub/wsgi", "tmp.wsgi"); Options options = createOptions(); options.appRoot = "tmp.wsgi"; options.minProcesses = 2; options.statThrottleRate = 0; pool->asyncGet(options, callback); EVENTUALLY(2, result = pool->getProcessCount() == 2; ); // Trigger a restart. The creation of the new spawner should take a while. skDebugSupport.spawnerCreationSleepTime = 20000; touchFile("tmp.wsgi/tmp/restart.txt"); pool->asyncGet(options, callback); GroupPtr group = pool->findOrCreateGroup(options); ensure_equals("(1)", pool->getProcessCount(), 0u); ensure_equals("(2)", group->getWaitlist.size(), 1u); // Now that the restart is in progress, perform a get(). pool->asyncGet(options, callback); ensure_equals("(3)", group->getWaitlist.size(), 2u); EVENTUALLY(2, result = number == 3; ); ensure_equals("(4) The restart function respects minProcesses", pool->getProcessCount(), 2u); } TEST_METHOD(74) { // If a process fails to spawn, it sends a SpawnException result to all get waiters. TempDirCopy dir("stub/wsgi", "tmp.wsgi"); chmod("tmp.wsgi", 0777); Options options = createOptions(); options.appRoot = "tmp.wsgi"; options.appType = "wsgi"; options.startupFile = "passenger_wsgi.py"; options.spawnMethod = "direct"; pool->setMax(1); writeFile("tmp.wsgi/passenger_wsgi.py", "import os, time, sys\n" "\n" "def file_exists(filename):\n" " try:\n" " os.stat(filename)\n" " return True\n" " except OSError:\n" " return False\n" "\n" "f = open('spawned.txt', 'w')\n" "f.write(str(os.getpid()))\n" "f.close()\n" "while not file_exists('continue.txt'):\n" " time.sleep(0.05)\n" "sys.stderr.write('Something went wrong!')\n" "exit(1)\n"); retainSessions = true; if (defaultLogLevel == (LoggingKit::Level) DEFAULT_LOG_LEVEL) { // If the user did not customize the test's log level, // then we'll want to tone down the noise. LoggingKit::setLevel(LoggingKit::CRIT); } pool->asyncGet(options, callback); pool->asyncGet(options, callback); pool->asyncGet(options, callback); pool->asyncGet(options, callback); EVENTUALLY(5, result = fileExists("tmp.wsgi/spawned.txt"); ); usleep(20000); writeFile("tmp.wsgi/passenger_wsgi.py", unsafeReadFile("stub/wsgi/passenger_wsgi.py")); pid_t pid = (pid_t) stringToLL(unsafeReadFile("tmp.wsgi/spawned.txt")); kill(pid, SIGTERM); EVENTUALLY(5, result = number == 4; ); ensure_equals(pool->getProcessCount(), 0u); ensure(sessions.empty()); } TEST_METHOD(75) { // If a process fails to spawn, the existing processes // are kept alive. TempDirCopy dir("stub/wsgi", "tmp.wsgi"); Options options = createOptions(); options.appRoot = "tmp.wsgi"; options.appType = "wsgi"; options.startupFile = "passenger_wsgi.py"; options.spawnMethod = "direct"; options.minProcesses = 2; // Spawn 2 processes. retainSessions = true; pool->asyncGet(options, callback); pool->asyncGet(options, callback); EVENTUALLY(10, result = number == 2; ); ensure_equals(pool->getProcessCount(), 2u); // Mess up the application and spawn a new one. writeFile("tmp.wsgi/passenger_wsgi.py", "import sys\n" "sys.stderr.write('Something went wrong!')\n" "exit(1)\n"); if (defaultLogLevel == (LoggingKit::Level) DEFAULT_LOG_LEVEL) { // If the user did not customize the test's log level, // then we'll want to tone down the noise. LoggingKit::setLevel(LoggingKit::CRIT); } try { currentSession = pool->get(options, &ticket); fail("SpawnException expected"); } catch (const SpawningKit::SpawnException &) { ensure_equals(pool->getProcessCount(), 2u); } } TEST_METHOD(76) { // No more than maxOutOfBandWorkInstances process will be performing // out-of-band work at the same time. TempDirCopy dir("stub/wsgi", "tmp.wsgi"); Options options = createOptions(); options.appRoot = "tmp.wsgi"; options.appType = "wsgi"; options.startupFile = "passenger_wsgi.py"; options.spawnMethod = "direct"; options.maxOutOfBandWorkInstances = 2; initPoolDebugging(); debug->restarting = false; debug->spawning = false; debug->oobw = true; // Spawn 3 processes and initiate 2 OOBW requests. SessionPtr session1 = pool->get(options, &ticket); SessionPtr session2 = pool->get(options, &ticket); SessionPtr session3 = pool->get(options, &ticket); session1->requestOOBW(); session1.reset(); session2->requestOOBW(); session2.reset(); // 2 OOBW requests eventually start. debug->debugger->recv("OOBW request about to start"); debug->debugger->recv("OOBW request about to start"); // Request another OOBW, but this one is not initiated. session3->requestOOBW(); session3.reset(); SHOULD_NEVER_HAPPEN(100, result = debug->debugger->peek("OOBW request about to start") != NULL; ); // Let one OOBW request finish. The third one should eventually // start. debug->messages->send("Proceed with OOBW request"); debug->debugger->recv("OOBW request about to start"); debug->messages->send("Proceed with OOBW request"); debug->messages->send("Proceed with OOBW request"); debug->debugger->recv("OOBW request finished"); debug->debugger->recv("OOBW request finished"); debug->debugger->recv("OOBW request finished"); } TEST_METHOD(77) { // If the getWaitlist already has maxRequestQueueSize items, // then an exception is returned. Options options = createOptions(); options.appGroupName = "test1"; options.maxRequestQueueSize = 3; GroupPtr group = pool->findOrCreateGroup(options); skDebugSupport.dummyConcurrency = 3; initPoolDebugging(); pool->setMax(1); for (int i = 0; i < 3; i++) { pool->asyncGet(options, callback); } ensure_equals(number, 0); { LockGuard l(pool->syncher); ensure_equals(group->getWaitlist.size(), 3u); } try { pool->get(options, &ticket); fail("Expected RequestQueueFullException"); } catch (const RequestQueueFullException &e) { // OK } debug->messages->send("Proceed with spawn loop iteration 1"); debug->messages->send("Spawn loop done"); EVENTUALLY(5, result = number == 3; ); } TEST_METHOD(78) { // Test restarting while a previous restart was already being finalized. // The previous finalization should abort. Options options = createOptions(); initPoolDebugging(); debug->spawning = false; pool->get(options, &ticket); ensure_equals(pool->restartGroupsByAppRoot(options.appRoot), 1u); debug->debugger->recv("About to end restarting"); ensure_equals(pool->restartGroupsByAppRoot(options.appRoot), 1u); debug->debugger->recv("About to end restarting"); debug->messages->send("Finish restarting"); debug->messages->send("Finish restarting"); debug->debugger->recv("Restarting done"); debug->debugger->recv("Restarting aborted"); } TEST_METHOD(79) { // Test sticky sessions. // Spawn 2 processes and get their sticky session IDs and PIDs. ensureMinProcesses(2); Options options = createOptions(); SessionPtr session1 = pool->get(options, &ticket); SessionPtr session2 = pool->get(options, &ticket); int id1 = session1->getStickySessionId(); int id2 = session2->getStickySessionId(); pid_t pid1 = session1->getPid(); pid_t pid2 = session2->getPid(); session1.reset(); session2.reset(); // Make two requests with id1 as sticky session ID. They should // both go to process pid1. options.stickySessionId = id1; session1 = pool->get(options, &ticket); ensure_equals("Request 1.1 goes to process 1", session1->getPid(), pid1); // The second request should be queued, and should not finish until // the first request is finished. ensure_equals(number, 1); pool->asyncGet(options, callback); SHOULD_NEVER_HAPPEN(100, result = number > 1; ); session1.reset(); EVENTUALLY(1, result = number == 2; ); ensure_equals("Request 1.2 goes to process 1", currentSession->getPid(), pid1); currentSession.reset(); // Make two requests with id2 as sticky session ID. They should // both go to process pid2. options.stickySessionId = id2; session1 = pool->get(options, &ticket); ensure_equals("Request 2.1 goes to process 2", session1->getPid(), pid2); // The second request should be queued, and should not finish until // the first request is finished. pool->asyncGet(options, callback); SHOULD_NEVER_HAPPEN(100, result = number > 2; ); session1.reset(); EVENTUALLY(1, result = number == 3; ); ensure_equals("Request 2.2 goes to process 2", currentSession->getPid(), pid2); currentSession.reset(); } // TODO: Persistent connections. // TODO: If one closes the session before it has reached EOF, and process's maximum concurrency // has already been reached, then the pool should ping the process so that it can detect // when the session's connection has been released by the app. /*********** Test previously discovered bugs ***********/ TEST_METHOD(85) { // Test detaching, then restarting. This should not violate any invariants. TempDirCopy dir("stub/wsgi", "tmp.wsgi"); Options options = createOptions(); options.appRoot = "tmp.wsgi"; options.appType = "wsgi"; options.startupFile = "passenger_wsgi.py"; options.spawnMethod = "direct"; options.statThrottleRate = 0; SessionPtr session = pool->get(options, &ticket); string gupid = session->getProcess()->getGupid().toString(); session.reset(); pool->detachProcess(gupid); touchFile("tmp.wsgi/tmp/restart.txt", 1); pool->get(options, &ticket).reset(); } /*****************************/ } SecurityUpdateCheckerTest.cpp 0000644 00000030204 14756504011 0012351 0 ustar 00 /* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2011-2017 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * See LICENSE file for license information. */ #include <TestSupport.h> #include <Core/SecurityUpdateChecker.h> #include <modp_b64.h> using namespace Passenger; using namespace std; namespace tut { void failNiceWhenSubstringMismatch(string substring, string s) { string failMessage = "expected [" + substring + "] in [" + s + "]"; ensure_equals(failMessage.c_str(), containsSubstring(s, substring), true); } class TestChecker: public SecurityUpdateChecker { private: CURLcode mockResponseCurlCode; int mockResponseHttpCode; string mockResponseData; string mockNonce; public: string lastError; string lastSuccess; string lastSuccessAdditional; TestChecker(const SecurityUpdateChecker::Schema &schema, const Json::Value &initialConfig) : SecurityUpdateChecker(schema, initialConfig), mockResponseCurlCode(CURLE_FAILED_INIT), mockResponseHttpCode(0) { } virtual void logUpdateFail(string error) { lastError = error; // store for checking } virtual void logUpdateFailAdditional(string additional) { // no op } virtual void logUpdateSuccess(int update, string success) { lastSuccess = success; // store for checking } virtual void logUpdateSuccessAdditional(string additional) { lastSuccessAdditional = additional; } virtual CURLcode sendAndReceive(CURL *curl, string *responseData, long *responseCode) { responseData->append(mockResponseData); *responseCode = (int) mockResponseHttpCode; return mockResponseCurlCode; } virtual bool fillNonce(string &nonce) { nonce.append(mockNonce); return true; } int testRaw(CURLcode responseCurlCode, int responseHttpCode, string responseData, string nonce) { lastError.clear(); lastSuccess.clear(); lastSuccessAdditional.clear(); mockResponseCurlCode = responseCurlCode; mockResponseHttpCode = responseHttpCode; mockResponseData = responseData; mockNonce = nonce; return checkAndLogSecurityUpdate(); } void testContentFail(string expectedError, CURLcode responseCurlCode, int responseHttpCode, string data, string signature, string nonce) { testRaw(responseCurlCode, responseHttpCode, "{\"data\":\"" + data + "\", \"signature\":\"" + signature + "\"}", nonce); failNiceWhenSubstringMismatch(expectedError, lastError); } }; struct Core_SecurityUpdateCheckerTest: public TestBase { SecurityUpdateChecker::Schema schema; boost::shared_ptr<TestChecker> checker; Core_SecurityUpdateCheckerTest() { if (defaultLogLevel == (LoggingKit::Level) DEFAULT_LOG_LEVEL) { // If the user did not customize the test's log level, // then we'll want to tone down the noise. LoggingKit::setLevel(LoggingKit::CRIT); } } void init() { Json::Value config; config["server_identifier"] = "testing"; checker = boost::make_shared<TestChecker>(schema, config); checker->resourceLocator = resourceLocator; checker->initialize(); } }; DEFINE_TEST_GROUP(Core_SecurityUpdateCheckerTest); /* * N.B. the signatures need to be calculated with the the key, which is on the server as well as Niels' test environment. */ TEST_METHOD(1) { set_test_name("succeeds with good signature, nonce and data, for update and no update"); init(); checker->testRaw(CURLE_OK, 200, "{\"data\":\"eyJ1cGRhdGUiOjAsImxvZyI6IiIsImJhY2tvZmYiOjAsIm5vbmNlIjoiMTQ3NDkwMDg4MTY3OTQzNDVHc2dOZXJxMU04akdPak9Pd05pTGc9PSJ9\"," "\"signature\":\"FopgXeV0cfvf4ekwR4e9EqOMxqAyQXC7kErf6Lz3sn0GhFG0FEauKtpiElEBvSyoeXi+UeGWhCXHbT449aOdfA0LIH7Bp4clBrF5P+CNUI1HK7C5Y8X2hjEsBi56OkfHF1uT0R8Z7SF/dYgW8LNKLo09hBfzP7RHX7HDrFGhbBuEAOxo+fYpmKmbHduk0FOciNeElJTTyusqtMcN5/QvSalIbRPR04Au61awG9R3ArWK7ocIkKBkyDfPAnmOjnRrEjS7byo/Yw3GBAhQQ+24pzwwMytn0WbZXekk89mgXs/B4OUCTp+TfkVcDJSMA76YMv1wqfQEO9hvlIUCNrUyR1lHRqRP3ZgAXmdX5e6lg+fTiIx35vpS8l4GQ90wk0wzJJLETDURKk97gmATb61Opn8J7kZxtN4itdphqZa9zx8IhpdtRluLBXrlsYj9oolyOL/vOpMD///Gx9hmcShLxJ/qq+taGhpEoqadWnZsQljkSnlfopX9Q1cxQf/Grte+YNOe7FItVguoJBrfg6g8NISFODdMpnigHsFUtsWLtC1HfL0fN7GmOc4F+fhJbmDY0kCcXEIb/N1z65eQDs/MzeoMlzp/9Qsih2i5HtXtaAuV50UGRd4LguOgWSkXENIcRQPB37etOHchC/Q0lDS44merm99q8VCU197SJpDP+Fw=\"}", "14749008816794345GsgNerq1M8jGOjOOwNiLg=="); ensure_equals(checker->lastError.c_str(), checker->lastError.empty(), true); failNiceWhenSubstringMismatch("no update found", checker->lastSuccess); checker->testRaw(CURLE_OK, 200, "{\"data\":\"eyJ1cGRhdGUiOjEsInZlcnNpb24iOiI1LjEuMCIsImxvZyI6IiIsImJhY2tvZmYiOjAsIm5vbmNlIjoiMTQ3NDkwNDE1MDU5MjM5M0FKd1VMcElGWkY3d3VyLzFWMHNBZFE9PSJ9\"," "\"signature\":\"ivK80A0f4ZOOUw3XlbCCnTZJ7CvJ4dQQrvcyMrBDSZXQ4DGoLIV/y39QHE0eh6bj22iGLps0vYups7ZL8FAcpGz3lzAwgSoSCtUUl71zQtJfLreElSBGmtu0zQywlsgvoWgkOxADRStVzY53TaX+1T+VTpx3E5F8aGG93fCC5ilEOM2+yVtpOSOLNAtONqcQ+nq8sIiJSKCljD7sFJLZ3dLu0UlV/lEmw8S/UzoQuTgk8yigkD0D4Gf7FYGuJ4gG5tCst0F3MYYdV9wfl7ZGqJRhE0O2asbH0a7ja1kXlY7nSdO1/MEMVcYVvwNVHDYPO2Jdf4UTTRzXd00b+XvqlmsjXP2lwafZt8854xnbI0DFuEPOFF3rUzzxe7vvadnFjkOt12TM7QqezVfyZkR7NOQ7XiT5KV7m3Iq+K9bFq1OsfCc/MDsqJ6fQZUtWsyfhsWcof0mgZllu/TPYajl/Bz+C4jPP8J+oW215NXz2Q8UuHm3a1IEE36nSlZ8KTilCKIojF3gq/fwS5AiYK7gbdHlQkYkKNowvPgfdegIjr371aW3OMuTB1mUxm8IagjCIe0hZ9udHA1rXGqpH2MkjtU99EJlf9ThL8pH8p+3Gtj3vVFFSjlaNx2LNRf5XHFIh4RJwr6d8HLnvZSCxZyq0bDHqtWsnoQe7LTap97rqGno=\"}", "1474904150592393AJwULpIFZF7wur/1V0sAdQ=="); ensure_equals(checker->lastError.c_str(), checker->lastError.empty(), true); failNiceWhenSubstringMismatch("We strongly recommend upgrading", checker->lastSuccess); } TEST_METHOD(2) { set_test_name("long additional info string doesn't crash"); init(); checker->testRaw(CURLE_OK, 200, "{\"data\":\"eyJ1cGRhdGUiOjEsInZlcnNpb24iOiI1LjEuNSIsImxvZyI6Ii0gW0ZpeGVkIGluIDUuMS41XSBJZiBQYXNzZW5nZXIvQXBhY2hlIGlzIHVzZWQsIGFuZCBBbGxvd092ZXJyaWRlIE9wdGlvbnMgaXMgY29uZmlndXJlZCwgdGhlbiBhIFBhc3NlbmdlckFwcEdyb3VwTmFtZSBvZiBjaG9pY2UgY2FuIGJlIHNwZWNpZmllZCBpbiAuaHRhY2Nlc3MuIFRoaXMgaXMgbm90IGEgc2FmZSBjb25maWd1cmF0aW9uIGluIGEgc2hhcmVkIGhvc3RpbmcgdHlwZSBzZXR1cCB0aGF0IGdpdmVzIGVhY2ggdXNlciBhY2Nlc3MgdG8gdGhlaXIgb3duIERpcmVjdG9yeSAodG8gcGxhY2UgdGhlaXIgYXBwIGluKSwgYnV0IG5vdCB0byB0aGUgbWFpbiBQYXNzZW5nZXIgY29uZmlndXJhdGlvbiBub3IgdG8gb3RoZXIgdXNlcnMuIFBhc3NlbmdlciByb3V0ZXMgcmVxdWVzdHMgYmFzZWQgb24gUGFzc2VuZ2VyQXBwR3JvdXBOYW1lIHNvIGlmIHVzZXIgQSBzcGVjaWZpZXMgdXNlciBCJ3MgYXBwIGdyb3VwIG5hbWUsIHRoZW4gQidzIHJlcXVlc3QgbWlnaHQgYmUgcm91dGVkIHRvIEEuIEluIFBhc3NlbmdlciA1LjEuNSwgUGFzc2VuZ2VyQXBwR3JvdXBOYW1lIGlzIG5vIGxvbmdlciBjb25maWd1cmFibGUgaW4gLmh0YWNjZXNzLiBUaGUgc2FmZSBjb25maWd1cmF0aW9uIGluIHByZXZpb3VzIHZlcnNpb25zIGlzIHRvIG5vdCBBbGxvd092ZXJyaWRlIGluIHN1Y2ggYSBzZXR1cC5XaGF0IGlzIGFmZmVjdGVkOjwgNS4xLjUsIEFwYWNoZSBbQWxsb3dPdmVycmlkZSBPcHRpb25zLCBzaGFyZWQgaG9zdGluZyBzZXR1cF0iLCJiYWNrb2ZmIjowLCJub25jZSI6IjE0NzQ5MDQxNTA1OTIzOTNBSndVTHBJRlpGN3d1ci8xVjBzQWRRPT0ifQ==\"," "\"signature\":\"WWzqDeCVdk16IU8k6POPgfAud1ERuX4xr/wmPzLFr7YOjmNz4CkTXCaRr7WH16YnVesx5H8b3jVdQynQS5QTaLCk2VWGuUSVIo1TdZBaWgNvVN/8sFmin70dfWTOWdayOT3AXhXukoLGblKqNySCXo5MQKtteOaxx4g1k0fk5iV3WR9QJoJipNIPifnR4m+e+LtJ3Ap3Q1XUxxvViLWK2OBamRIvVh4sdcYoG717Z221990C40ue1jNGh7tptx9vgggUsAHJAQ1sNq21ZzJq1Twuvb+WfSIELXZLj7/ZLqSdTuW+Y92+ZUa7CrzWoVUH4I3UWr3aQe3M7hU9uoEV9WxOskIzc3NfxA46KYXMoIs4RK6CHNcrodkpOaRdRpdPfkqgYDxAazxOIrMgZ78YBs4uU1lnoQbSfZAx3Qo0f6gbAI8PqQeZkgxWfSXPusmMlOzJ12MTAGa5+zFx1Qqx1I/noCKgrDRkoHIY+7v6LWpERUc9s8hG3coYdr6aaHk8fS3Dc/nCsvj9DiYJm/RUHWkw/lvc8hJqX6V8LRgHKWCQ4aQsif3q/KQwrxDoaGs9sxYDT/hY0T7F1xQVwBM/Ze/848gxlgLohCb09kQ9v+4c7yoiZr/bPGOtFIKQADWZ+0Br4N6MRw6uVXULq0B6oJ8RMbGgNeANGmL6Pn6jEb8=\"}", "1474904150592393AJwULpIFZF7wur/1V0sAdQ=="); ensure_equals(checker->lastError.c_str(), checker->lastError.empty(), true); failNiceWhenSubstringMismatch("shared hosting setup", checker->lastSuccessAdditional); } TEST_METHOD(3) { set_test_name("correctly reports various signature field errors"); init(); checker->testRaw(CURLE_OK, 200, "{\"data\":\"invalid_base64\"}", ""); failNiceWhenSubstringMismatch("missing response fields", checker->lastError); checker->testContentFail("corrupted signature", CURLE_OK, 200, "invalid_base64", "invalid_base64", ""); checker->testContentFail("forged signature", CURLE_OK, 200, "invalid_base64", "yyyy", ""); } TEST_METHOD(4) { set_test_name("catches replay attack (nonce mismatch)"); init(); checker->testContentFail("replay attack", CURLE_OK, 200, "eyJ1cGRhdGUiOjAsImxvZyI6IiIsImJhY2tvZmYiOjAsIm5vbmNlIjoiMTQ3NDkwMDg4MTY3OTQzNDVHc2dOZXJxMU04akdPak9Pd05pTGc9PSJ9", "FopgXeV0cfvf4ekwR4e9EqOMxqAyQXC7kErf6Lz3sn0GhFG0FEauKtpiElEBvSyoeXi+UeGWhCXHbT449aOdfA0LIH7Bp4clBrF5P+CNUI1HK7C5Y8X2hjEsBi56OkfHF1uT0R8Z7SF/dYgW8LNKLo09hBfzP7RHX7HDrFGhbBuEAOxo+fYpmKmbHduk0FOciNeElJTTyusqtMcN5/QvSalIbRPR04Au61awG9R3ArWK7ocIkKBkyDfPAnmOjnRrEjS7byo/Yw3GBAhQQ+24pzwwMytn0WbZXekk89mgXs/B4OUCTp+TfkVcDJSMA76YMv1wqfQEO9hvlIUCNrUyR1lHRqRP3ZgAXmdX5e6lg+fTiIx35vpS8l4GQ90wk0wzJJLETDURKk97gmATb61Opn8J7kZxtN4itdphqZa9zx8IhpdtRluLBXrlsYj9oolyOL/vOpMD///Gx9hmcShLxJ/qq+taGhpEoqadWnZsQljkSnlfopX9Q1cxQf/Grte+YNOe7FItVguoJBrfg6g8NISFODdMpnigHsFUtsWLtC1HfL0fN7GmOc4F+fhJbmDY0kCcXEIb/N1z65eQDs/MzeoMlzp/9Qsih2i5HtXtaAuV50UGRd4LguOgWSkXENIcRQPB37etOHchC/Q0lDS44merm99q8VCU197SJpDP+Fw=", "non-matching nonce"); } TEST_METHOD(5) { set_test_name("additional log is logged whether update=0 or 1"); init(); // update = 0 checker->testRaw(CURLE_OK, 200, "{\"data\":\"eyJ1cGRhdGUiOjAsImxvZyI6ImFkZGl0aW9uYWxpbmZvIiwiYmFja29mZiI6MCwibm9uY2UiOiIxNDc0OTA1MTkzNDg4Mjc3Y2c4ZmNMdDJDOWZ3dDAweDc3enYvdz09In0=\"," "\"signature\":\"PwbbOmnL7g7hydKKlSDxfUpTLFqaKe4DXLn46kNjQoy0GeP2iMkKsNWRfyDoUm8TFzvlj1bczL6ZMBFKqZjDhnS3u95OUTDb3BvK0S8lvRQcf4EsM7JB7aQ9T2QulU+L5sfqQXF+zGtseqIIkqPJbTb5Wy15QyzD4Jo75FwMvehGokWy1rNx7fPCQTUM4AhyqEebGOt2beuN3ZH4LmPlu/mEyD+2YxI/draczVAIpSH29TRh5vCYLyLzpXsZIkMFgxloG19IGzI5SCWGA7k5s2bBXt4tXk0P7sCKHhtvanO5gp75JEvLd4Kzz/jZN3A9ymjRWuPFWNAExdddzfr7YrwS0uBHaC4kyh1FtSlV52kdngHW5ciLTIg+45gh276Ic/WSEy3B52n0GZ0kJDKP8xZ6fdO/3iXi0xQ8Te7jDh0T78L9jyOQg2p4br9fQUanTGQeSQsN4XTDql/jzegW4cvbz3/tlKGi1xywCKCYglhSAJVw1rWcWyIRRW14qhLlV0081iBHgHIhagD0Ssl0ncI9YJPUtbotvNXLd4DBUmvjnhJmS5jQFgiKbJO+ZYaBJpltXdB0WCexR1EOK0VuJM561mv/FP3c7tmsFReqYaZ+UQNmx0hTA6vel4Uv5XI0qiOcAOsgrxxsVCBdCYA2tyfyhRMQl7x2wZZ6M5/dhso=\"}", "1474905193488277cg8fcLt2C9fwt00x77zv/w=="); ensure_equals(checker->lastError.c_str(), checker->lastError.empty(), true); failNiceWhenSubstringMismatch("additionalinfo", checker->lastSuccessAdditional); checker->testRaw(CURLE_OK, 200, "{\"data\":\"eyJ1cGRhdGUiOjEsInZlcnNpb24iOiI1LjEuMCIsImxvZyI6ImFkZGl0aW9uYWxpbmZvIiwiYmFja29mZiI6MCwibm9uY2UiOiIxNDc0OTA1NjI3MTcwNDY1RGsybE9LcUl6MVdLVlJqYUF3RUtMdz09In0=\"," "\"signature\":\"v/05dhOnw4wi/cS2Emlmki0aLG3Og+t3QkHdnYm9sGmI3/wIl1Pqsng1CQ2utei2eM6ROoDjZLyKtG58NjrnYAG8b7jfo85LiFvaibrej9FC0uDHsbdZuODlpHUuWmBi1uCKKdJ+1dL26W2+nPvExlwQTyEoNhuIbW2Ji7QnY33vbE5dV4luf5aWdwuPtaWKm+NvDBY2mgKxfeeXfPOhTU+H8LQCo59fNIQwBb7vvaTUtIFCwWHGRqJ0asM1yCM7bfT+zyP7J+tvebvFmAX9MVtl5rkvXkkkyiTPFfpZ+EiD9fROipy8ubMB6hxJQnW3xcXPZXiE88Bpssidb0vzLIxpAfz7HjfO2Tt6sl7Ekks4ql4B7GSy/Cw4S3HgoIjD9gq1pI1PMdjrktHCh5TDRLiV2s14mbLFJvxsayn6okO/s4lASt8GQSXYY6Rea0RvPHplbX6HDjGVthydu7+YG/rBTfcT6wKJM9btfsZX6T59n7uZG6EMJEW2TE4C7aIN1v0ztRNCBxKanSGtkvrUIRXtp5bq+lTSDST/4JwCZnyFCB6i3ju4iKsJOmRxjgp5OoS4aEGOMJGcUwokSXTcx072rGoaK13dW8bg7sK1PgXQaFboC/NP5feQlj9fhJkLeOsMJobJwfv6cHwF73HleJSa047KvdNR+iHheegHV1c=\"}", "1474905627170465Dk2lOKqIz1WKVRjaAwEKLw=="); // update = 1 ensure_equals(checker->lastError.c_str(), checker->lastError.empty(), true); failNiceWhenSubstringMismatch("additionalinfo", checker->lastSuccessAdditional); } TEST_METHOD(6) { set_test_name("enriches CURL errors"); init(); checker->testContentFail("check your connection security", CURLE_COULDNT_CONNECT, 0, "", "", ""); checker->testContentFail("try upgrading or reinstalling", CURLE_SSL_CERTPROBLEM, 0, "", "", ""); checker->testContentFail("truststore", CURLE_PEER_FAILED_VERIFICATION, 0, "", "", ""); } TEST_METHOD(7) { set_test_name("enriches HTTP errors"); init(); checker->testContentFail("not found", CURLE_OK, 404, "", "", ""); checker->testContentFail("rate limit", CURLE_OK, 429, "", "", ""); checker->testContentFail("HTTP 500", CURLE_OK, 500, "", "", ""); checker->testContentFail("try again later", CURLE_OK, 503, "", "", ""); } } ControllerTest.cpp 0000644 00000070374 14756504011 0010251 0 ustar 00 #include <TestSupport.h> #include <limits> #include <Constants.h> #include <IOTools/IOUtils.h> #include <IOTools/BufferedIO.h> #include <IOTools/MessageIO.h> #include <Core/ApplicationPool/TestSession.h> #include <Core/Controller.h> using namespace std; using namespace boost; using namespace Passenger; using namespace Passenger::Core; namespace tut { struct Core_ControllerTest: public TestBase { class MyController: public Core::Controller { protected: virtual void asyncGetFromApplicationPool(Request *req, ApplicationPool2::GetCallback callback) { callback(sessionToReturn, exceptionToReturn); sessionToReturn.reset(); } public: ApplicationPool2::AbstractSessionPtr sessionToReturn; ApplicationPool2::ExceptionPtr exceptionToReturn; MyController(ServerKit::Context *context, const Core::ControllerSchema &schema, const Json::Value &initialConfig, const Core::ControllerSingleAppModeSchema &singleAppModeSchema, const Json::Value &singleAppModeConfig) : Core::Controller(context, schema, initialConfig, ConfigKit::DummyTranslator(), &singleAppModeSchema, &singleAppModeConfig, ConfigKit::DummyTranslator()) { } }; BackgroundEventLoop bg; ServerKit::Schema skSchema; ServerKit::Context context; WrapperRegistry::Registry wrapperRegistry; Core::ControllerSchema schema; Core::ControllerSingleAppModeSchema singleAppModeSchema; MyController *controller; SpawningKit::Context::Schema skContextSchema; SpawningKit::Context skContext; SpawningKit::FactoryPtr spawningKitFactory; ApplicationPool2::Context apContext; PoolPtr appPool; Json::Value config, singleAppModeConfig; int serverSocket; TestSession testSession; FileDescriptor clientConnection; BufferedIO clientConnectionIO; string peerRequestHeader; Core_ControllerTest() : bg(false, true), context(skSchema), singleAppModeSchema(&wrapperRegistry), skContext(skContextSchema) { config["thread_number"] = 1; config["multi_app"] = false; config["default_server_name"] = "localhost"; config["default_server_port"] = 80; config["user_switching"] = false; singleAppModeConfig["app_root"] = "stub/rack"; singleAppModeConfig["app_type"] = "rack"; singleAppModeConfig["startup_file"] = "none"; if (defaultLogLevel == (LoggingKit::Level) DEFAULT_LOG_LEVEL) { // If the user did not customize the test's log level, // then we'll want to tone down the noise. LoggingKit::setLevel(LoggingKit::WARN); } wrapperRegistry.finalize(); controller = NULL; serverSocket = createUnixServer("tmp.server"); context.libev = bg.safe; context.libuv = bg.libuv_loop; context.initialize(); skContext.resourceLocator = resourceLocator; skContext.wrapperRegistry = &wrapperRegistry; skContext.integrationMode = "standalone"; skContext.finalize(); spawningKitFactory = boost::make_shared<SpawningKit::Factory>(&skContext); apContext.spawningKitFactory = spawningKitFactory; apContext.finalize(); appPool = boost::make_shared<Pool>(&apContext); appPool->initialize(); } ~Core_ControllerTest() { startLoop(); // Silence error disconnection messages during shutdown. LoggingKit::setLevel(LoggingKit::CRIT); clientConnection.close(); if (controller != NULL) { bg.safe->runSync(boost::bind(&MyController::shutdown, controller, true)); while (getServerState() != MyController::FINISHED_SHUTDOWN) { syscalls::usleep(10000); } bg.safe->runSync(boost::bind(&Core_ControllerTest::destroyController, this)); } safelyClose(serverSocket); unlink("tmp.server"); bg.stop(); } void startLoop() { if (!bg.isStarted()) { bg.start(); } } void destroyController() { delete controller; } void init() { controller = new MyController(&context, schema, config, singleAppModeSchema, singleAppModeConfig); controller->resourceLocator = resourceLocator; controller->wrapperRegistry = &wrapperRegistry; controller->appPool = appPool; controller->initialize(); controller->listen(serverSocket); startLoop(); } FileDescriptor &connectToServer() { startLoop(); clientConnection = FileDescriptor(connectToUnixServer("tmp.server", __FILE__, __LINE__), NULL, 0); clientConnectionIO = BufferedIO(clientConnection); return clientConnection; } void sendRequest(const StaticString &data) { writeExact(clientConnection, data); } void sendRequestAndWait(const StaticString &data) { unsigned long long totalBytesConsumed = getTotalBytesConsumed(); sendRequest(data); EVENTUALLY(5, result = getTotalBytesConsumed() >= totalBytesConsumed + data.size(); ); ensure_equals(getTotalBytesConsumed(), totalBytesConsumed + data.size()); } void useTestSessionObject() { bg.safe->runSync(boost::bind(&Core_ControllerTest::_setTestSessionObject, this)); } void _setTestSessionObject() { controller->sessionToReturn.reset(&testSession, false); } MyController::State getServerState() { Controller::State result; bg.safe->runSync(boost::bind(&Core_ControllerTest::_getServerState, this, &result)); return result; } void _getServerState(MyController::State *state) { *state = controller->serverState; } Json::Value inspectStateAsJson() { Json::Value result; bg.safe->runSync(boost::bind(&Core_ControllerTest::_inspectStateAsJson, this, &result)); return result; } void _inspectStateAsJson(Json::Value *result) { *result = controller->inspectStateAsJson(); } unsigned long long getTotalBytesConsumed() { unsigned long long result; bg.safe->runSync(boost::bind(&Core_ControllerTest::_getTotalBytesConsumed, this, &result)); return result; } void _getTotalBytesConsumed(unsigned long long *result) { *result = controller->totalBytesConsumed; } string readPeerRequestHeader(string *peerRequestHeader = NULL) { if (peerRequestHeader == NULL) { peerRequestHeader = &this->peerRequestHeader; } if (testSession.getProtocol() == "session") { *peerRequestHeader = readScalarMessage(testSession.peerFd()); } else { *peerRequestHeader = readHeader(testSession.getPeerBufferedIO()); } return *peerRequestHeader; } string readPeerBody() { if (testSession.getProtocol() == "session") { return readAll(testSession.peerFd(), std::numeric_limits<size_t>::max()).first; } else { return testSession.getPeerBufferedIO().readAll(); } } void sendPeerResponse(const StaticString &data) { writeExact(testSession.peerFd(), data); testSession.closePeerFd(); } bool tryDrainPeerConnection() { bool drained; SystemException e("", 0); setNonBlocking(testSession.peerFd()); try { readAll(testSession.peerFd(), std::numeric_limits<size_t>::max()); drained = true; } catch (const SystemException &e2) { e = e2; drained = false; } setBlocking(testSession.peerFd()); if (drained) { return true; } else if (e.code() == EAGAIN) { return false; } else { throw e; } } void ensureNeverDrainPeerConnection() { SHOULD_NEVER_HAPPEN(100, result = tryDrainPeerConnection(); ); } void ensureEventuallyDrainPeerConnection() { unsigned long long timeout = 5000000; EVENTUALLY(5, if (!waitUntilReadable(testSession.peerFd(), &timeout)) { fail("Peer connection timed out"); } result = tryDrainPeerConnection(); ); } void waitUntilSessionInitiated() { EVENTUALLY(5, result = testSession.fd() != -1; ); } void waitUntilSessionClosed() { EVENTUALLY(5, result = testSession.isClosed(); ); } string readHeader(BufferedIO &io) { string result; do { string line = io.readLine(); if (line == "\r\n" || line.empty()) { return result; } else { result.append(line); } } while (true); } string readResponseHeader() { return readHeader(clientConnectionIO); } string readResponseBody() { return clientConnectionIO.readAll(); } }; DEFINE_TEST_GROUP_WITH_LIMIT(Core_ControllerTest, 80); /***** Passing request information to the app *****/ TEST_METHOD(1) { set_test_name("Session protocol: request URI"); init(); useTestSessionObject(); connectToServer(); sendRequest( "GET /hello?foo=bar HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: close\r\n" "\r\n"); waitUntilSessionInitiated(); readPeerRequestHeader(); ensure(containsSubstring(peerRequestHeader, P_STATIC_STRING("REQUEST_URI\0/hello?foo=bar\0"))); } TEST_METHOD(2) { set_test_name("HTTP protocol: request URI"); init(); useTestSessionObject(); testSession.setProtocol("http_session"); connectToServer(); sendRequest( "GET /hello?foo=bar HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: close\r\n" "\r\n"); waitUntilSessionInitiated(); readPeerRequestHeader(); ensure(containsSubstring(peerRequestHeader, "GET /hello?foo=bar HTTP/1.1\r\n")); } /***** Passing request body to the app *****/ TEST_METHOD(10) { set_test_name("When body buffering on, Content-Length given:" " it sets Content-Length in the forwarded request," " and forwards the raw data"); init(); useTestSessionObject(); connectToServer(); sendRequest( "POST /hello HTTP/1.1\r\n" "!~: \r\n" "!~FLAGS: B\r\n" "!~: \r\n" "Host: localhost\r\n" "Content-Length: 5\r\n" "Connection: close\r\n" "\r\n" "hello"); waitUntilSessionInitiated(); Json::Value state = inspectStateAsJson(); Json::Value reqState = state["active_clients"]["1-1"]["current_request"]; ensure("Body buffering is on", reqState.isMember("body_bytes_buffered")); ensure(containsSubstring(readPeerRequestHeader(), P_STATIC_STRING("CONTENT_LENGTH\0005\000"))); ensure_equals(readPeerBody(), "hello"); } TEST_METHOD(11) { set_test_name("When body buffering on, Transfer-Encoding given:" " it sets Content-Length and removes Transfer-Encoding in the forwarded request," " and forwards the chunked data"); init(); useTestSessionObject(); connectToServer(); sendRequest( "POST /hello HTTP/1.1\r\n" "!~: \r\n" "!~FLAGS: B\r\n" "!~: \r\n" "Host: localhost\r\n" "Transfer-Encoding: chunked\r\n" "Connection: close\r\n" "\r\n" "5\r\n" "hello\r\n" "0\r\n\r\n"); waitUntilSessionInitiated(); Json::Value state = inspectStateAsJson(); Json::Value reqState = state["active_clients"]["1-1"]["current_request"]; ensure("Body buffering is on", reqState.isMember("body_bytes_buffered")); string header = readPeerRequestHeader(); ensure(containsSubstring(header, P_STATIC_STRING("CONTENT_LENGTH\0005\000"))); ensure(!containsSubstring(header, P_STATIC_STRING("TRANSFER_ENCODING"))); ensure_equals(readPeerBody(), "hello"); } TEST_METHOD(12) { set_test_name("When body buffering on, Connection is upgrade:" " it refuses to buffer the request body," " and forwards the raw data"); init(); useTestSessionObject(); connectToServer(); sendRequest( "POST /hello HTTP/1.1\r\n" "!~: \r\n" "!~FLAGS: B\r\n" "!~: \r\n" "Host: localhost\r\n" "Connection: upgrade\r\n" "Upgrade: text\r\n" "\r\n"); waitUntilSessionInitiated(); Json::Value state = inspectStateAsJson(); Json::Value reqState = state["active_clients"]["1-1"]["current_request"]; ensure("Body buffering is off", !reqState.isMember("body_bytes_buffered")); string header = readPeerRequestHeader(); ensure(!containsSubstring(header, P_STATIC_STRING("CONTENT_LENGTH"))); char buf[15]; unsigned int size; writeExact(clientConnection, "ab"); size = readExact(testSession.peerFd(), buf, 2); ensure_equals(size, 2u); ensure_equals(StaticString(buf, 2), "ab"); writeExact(clientConnection, "cde"); size = readExact(testSession.peerFd(), buf, 3); ensure_equals(size, 3u); ensure_equals(StaticString(buf, 3), "cde"); } TEST_METHOD(13) { set_test_name("When body buffering off, Content-Length given:" " it sets Content-Length in the forwarded request," " and forwards the raw data"); init(); useTestSessionObject(); connectToServer(); sendRequest( "POST /hello HTTP/1.1\r\n" "Host: localhost\r\n" "Content-Length: 5\r\n" "Connection: close\r\n" "\r\n" "hello"); waitUntilSessionInitiated(); Json::Value state = inspectStateAsJson(); Json::Value reqState = state["active_clients"]["1-1"]["current_request"]; ensure("Body buffering is off", !reqState.isMember("body_bytes_buffered")); ensure(containsSubstring(readPeerRequestHeader(), P_STATIC_STRING("CONTENT_LENGTH\0005\000"))); ensure_equals(readPeerBody(), "hello"); } TEST_METHOD(14) { set_test_name("When body buffering off, Transfer-Encoding given:" " it forwards the chunked data"); init(); useTestSessionObject(); connectToServer(); sendRequest( "POST /hello HTTP/1.1\r\n" "Host: localhost\r\n" "Transfer-Encoding: chunked\r\n" "Connection: close\r\n" "\r\n" "5\r\n" "hello\r\n" "0\r\n\r\n"); waitUntilSessionInitiated(); Json::Value state = inspectStateAsJson(); Json::Value reqState = state["active_clients"]["1-1"]["current_request"]; ensure("Body buffering is off", !reqState.isMember("body_bytes_buffered")); string header = readPeerRequestHeader(); ensure(!containsSubstring(header, P_STATIC_STRING("CONTENT_LENGTH"))); ensure(containsSubstring(header, P_STATIC_STRING("HTTP_TRANSFER_ENCODING\000chunked\000"))); ensure_equals(readPeerBody(), "5\r\n" "hello\r\n" "0\r\n\r\n"); } TEST_METHOD(15) { set_test_name("When body buffering off, Connection is upgrade:" " it forwards the raw data"); init(); useTestSessionObject(); connectToServer(); sendRequest( "POST /hello HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: upgrade\r\n" "Upgrade: text\r\n" "\r\n"); waitUntilSessionInitiated(); Json::Value state = inspectStateAsJson(); Json::Value reqState = state["active_clients"]["1-1"]["current_request"]; ensure("Body buffering is off", !reqState.isMember("body_bytes_buffered")); string header = readPeerRequestHeader(); ensure(!containsSubstring(header, P_STATIC_STRING("CONTENT_LENGTH"))); char buf[15]; unsigned int size; writeExact(clientConnection, "ab"); size = readExact(testSession.peerFd(), buf, 2); ensure_equals(size, 2u); ensure_equals(StaticString(buf, 2), "ab"); writeExact(clientConnection, "cde"); size = readExact(testSession.peerFd(), buf, 3); ensure_equals(size, 3u); ensure_equals(StaticString(buf, 3), "cde"); } /***** Application response body handling *****/ TEST_METHOD(20) { set_test_name("Fixed response body"); init(); useTestSessionObject(); connectToServer(); sendRequest( "GET /hello HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: close\r\n" "\r\n"); waitUntilSessionInitiated(); readPeerRequestHeader(); sendPeerResponse( "HTTP/1.1 200 OK\r\n" "Connection: close\r\n" "Content-Length: 5\r\n\r\n" "hello"); string header = readResponseHeader(); string body = readResponseBody(); ensure(containsSubstring(header, "HTTP/1.1 200 OK\r\n")); ensure_equals(body, "hello"); } TEST_METHOD(21) { set_test_name("Response body until EOF"); init(); useTestSessionObject(); connectToServer(); sendRequest( "GET /hello HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: close\r\n" "\r\n"); waitUntilSessionInitiated(); readPeerRequestHeader(); sendPeerResponse( "HTTP/1.1 200 OK\r\n" "Connection: close\r\n\r\n" "hello"); string header = readResponseHeader(); string body = readResponseBody(); ensure("HTTP response OK", containsSubstring(header, "HTTP/1.1 200 OK\r\n")); ensure_equals(body, "hello"); } TEST_METHOD(22) { set_test_name("Chunked response body"); init(); useTestSessionObject(); connectToServer(); sendRequest( "GET /hello HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: close\r\n" "\r\n"); waitUntilSessionInitiated(); readPeerRequestHeader(); sendPeerResponse( "HTTP/1.1 200 OK\r\n" "Connection: close\r\n" "Transfer-Encoding: chunked\r\n\r\n" "5\r\n" "hello\r\n" "0\r\n\r\n"); string header = readResponseHeader(); string body = readResponseBody(); ensure("HTTP response OK", containsSubstring(header, "HTTP/1.1 200 OK\r\n")); ensure_equals(body, "5\r\n" "hello\r\n" "0\r\n\r\n"); } TEST_METHOD(23) { set_test_name("Upgraded response body"); init(); useTestSessionObject(); connectToServer(); sendRequest( "GET /hello HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: close\r\n" "\r\n"); waitUntilSessionInitiated(); readPeerRequestHeader(); sendPeerResponse( "HTTP/1.1 200 OK\r\n" "Connection: upgrade\r\n" "Upgrade: text\r\n\r\n" "hello"); string header = readResponseHeader(); string body = readResponseBody(); ensure("HTTP response OK", containsSubstring(header, "HTTP/1.1 200 OK\r\n")); ensure_equals(body, "hello"); } /***** Application connection keep-alive *****/ TEST_METHOD(30) { set_test_name("Perform keep-alive on application responses that allow it"); init(); useTestSessionObject(); connectToServer(); sendRequest( "GET /hello HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: close\r\n" "\r\n"); waitUntilSessionInitiated(); readPeerRequestHeader(); sendPeerResponse( "HTTP/1.1 200 OK\r\n" "Content-Type: text/plain\r\n" "Content-Length: 2\r\n\r\n" "ok"); waitUntilSessionClosed(); ensure("(1)", testSession.isSuccessful()); ensure("(2)", testSession.wantsKeepAlive()); } TEST_METHOD(31) { set_test_name("Don't perform keep-alive on application responses that don't allow it"); init(); useTestSessionObject(); connectToServer(); sendRequest( "GET /hello HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: close\r\n" "\r\n"); waitUntilSessionInitiated(); readPeerRequestHeader(); sendPeerResponse( "HTTP/1.1 200 OK\r\n" "Content-Type: text/plain\r\n" "Connection: close\r\n" "Content-Length: 2\r\n\r\n" "ok"); waitUntilSessionClosed(); ensure("(1)", testSession.isSuccessful()); ensure("(2)", !testSession.wantsKeepAlive()); } TEST_METHOD(32) { set_test_name("Don't perform keep-alive if an error occurred"); init(); useTestSessionObject(); connectToServer(); sendRequest( "GET /hello HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: close\r\n" "\r\n"); waitUntilSessionInitiated(); readPeerRequestHeader(); if (defaultLogLevel == (LoggingKit::Level) DEFAULT_LOG_LEVEL) { // If the user did not customize the test's log level, // then we'll want to tone down the noise. LoggingKit::setLevel(LoggingKit::CRIT); } sendPeerResponse("invalid response"); waitUntilSessionClosed(); ensure("(1)", !testSession.isSuccessful()); ensure("(2)", !testSession.wantsKeepAlive()); } /***** Passing half-close events to the app *****/ TEST_METHOD(40) { set_test_name("Session protocol: on requests without body, it passes" " a half-close write event to the app on the next request's" " early read error and does not keep-alive the" " application connection"); init(); useTestSessionObject(); connectToServer(); sendRequest( "GET /hello HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: close\r\n" "\r\n"); waitUntilSessionInitiated(); ensureNeverDrainPeerConnection(); shutdown(clientConnection, SHUT_WR); ensureEventuallyDrainPeerConnection(); sendPeerResponse( "HTTP/1.1 200 OK\r\n" "Content-Length: 2\r\n\r\n" "ok"); waitUntilSessionClosed(); ensure("(1)", testSession.isSuccessful()); ensure("(2)", !testSession.wantsKeepAlive()); } TEST_METHOD(41) { set_test_name("Session protocol: on requests with fixed body, it passes" " a half-close write event to the app upon reaching the end" " of the request body and does not keep-alive the" " application connection"); init(); useTestSessionObject(); connectToServer(); sendRequest( "GET /hello HTTP/1.1\r\n" "Host: localhost\r\n" "Content-Length: 2\r\n" "Connection: close\r\n" "\r\n"); waitUntilSessionInitiated(); ensureNeverDrainPeerConnection(); writeExact(clientConnection, "ok"); ensureEventuallyDrainPeerConnection(); sendPeerResponse( "HTTP/1.1 200 OK\r\n" "Content-Length: 2\r\n\r\n" "ok"); waitUntilSessionClosed(); ensure("(1)", testSession.isSuccessful()); ensure("(2)", !testSession.wantsKeepAlive()); } TEST_METHOD(42) { set_test_name("Session protocol: on requests with chunked body, it passes" " a half-close write event to the app upon reaching the end" " of the request body and does not keep-alive the" " application connection"); init(); useTestSessionObject(); connectToServer(); sendRequest( "GET /hello HTTP/1.1\r\n" "Host: localhost\r\n" "Transfer-Encoding: chunked\r\n" "Connection: close\r\n" "\r\n"); waitUntilSessionInitiated(); ensureNeverDrainPeerConnection(); writeExact(clientConnection, "0\r\n\r\n"); ensureEventuallyDrainPeerConnection(); sendPeerResponse( "HTTP/1.1 200 OK\r\n" "Content-Length: 2\r\n\r\n" "ok"); waitUntilSessionClosed(); ensure("(1)", testSession.isSuccessful()); ensure("(2)", !testSession.wantsKeepAlive()); } TEST_METHOD(43) { set_test_name("Session protocol: on upgraded requests, it passes" " a half-close write event to the app upon reaching the end" " of the request body and does not keep-alive the" " application connection"); init(); useTestSessionObject(); connectToServer(); sendRequest( "GET /hello HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: upgrade\r\n" "Upgrade: text\r\n" "\r\n"); waitUntilSessionInitiated(); ensureNeverDrainPeerConnection(); writeExact(clientConnection, "hi"); ensureNeverDrainPeerConnection(); shutdown(clientConnection, SHUT_WR); ensureEventuallyDrainPeerConnection(); sendPeerResponse( "HTTP/1.1 200 OK\r\n" "Content-Length: 2\r\n\r\n" "ok"); waitUntilSessionClosed(); ensure("(1)", testSession.isSuccessful()); ensure("(2)", !testSession.wantsKeepAlive()); } TEST_METHOD(44) { set_test_name("HTTP protocol: on requests without body, it passes" " a half-close write event to the app on the next request's" " early read error and does not keep-alive the application connection"); init(); useTestSessionObject(); testSession.setProtocol("http_session"); connectToServer(); sendRequest( "GET /hello HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: close\r\n" "\r\n"); waitUntilSessionInitiated(); ensureNeverDrainPeerConnection(); shutdown(clientConnection, SHUT_WR); ensureEventuallyDrainPeerConnection(); sendPeerResponse( "HTTP/1.1 200 OK\r\n" "Content-Length: 2\r\n\r\n" "ok"); waitUntilSessionClosed(); ensure("(1)", testSession.isSuccessful()); ensure("(2)", !testSession.wantsKeepAlive()); } TEST_METHOD(45) { set_test_name("HTTP protocol: on requests with fixed body, it passes" " a half-close write event to the app on the next request's" " early read error and does not keep-alive the application connection"); init(); useTestSessionObject(); testSession.setProtocol("http_session"); connectToServer(); sendRequest( "GET /hello HTTP/1.1\r\n" "Host: localhost\r\n" "Content-Length: 2\r\n" "Connection: close\r\n" "\r\n"); waitUntilSessionInitiated(); ensureNeverDrainPeerConnection(); writeExact(clientConnection, "ok"); ensureNeverDrainPeerConnection(); shutdown(clientConnection, SHUT_WR); ensureEventuallyDrainPeerConnection(); sendPeerResponse( "HTTP/1.1 200 OK\r\n" "Content-Length: 2\r\n\r\n" "ok"); waitUntilSessionClosed(); ensure("(1)", testSession.isSuccessful()); ensure("(2)", !testSession.wantsKeepAlive()); } TEST_METHOD(46) { set_test_name("HTTP protocol: on requests with chunked body, it passes" " a half-close write event to the app on the next request's early read error" " and does not keep-alive the application connection"); init(); useTestSessionObject(); testSession.setProtocol("http_session"); connectToServer(); sendRequest( "GET /hello HTTP/1.1\r\n" "Host: localhost\r\n" "Transfer-Encoding: chunked\r\n" "Connection: close\r\n" "\r\n"); waitUntilSessionInitiated(); ensureNeverDrainPeerConnection(); writeExact(clientConnection, "0\r\n\r\n"); ensureNeverDrainPeerConnection(); shutdown(clientConnection, SHUT_WR); ensureEventuallyDrainPeerConnection(); sendPeerResponse( "HTTP/1.1 200 OK\r\n" "Content-Length: 2\r\n\r\n" "ok"); waitUntilSessionClosed(); ensure("(1)", testSession.isSuccessful()); ensure("(2)", !testSession.wantsKeepAlive()); } TEST_METHOD(47) { set_test_name("HTTP protocol: on upgraded requests, it passes" " a half-close write event to the app upon reaching the end" " of the request body and does not keep-alive the" " application connection"); init(); useTestSessionObject(); testSession.setProtocol("http_session"); connectToServer(); sendRequest( "GET /hello HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: upgrade\r\n" "Upgrade: text\r\n" "\r\n"); waitUntilSessionInitiated(); ensureNeverDrainPeerConnection(); writeExact(clientConnection, "ok"); ensureNeverDrainPeerConnection(); shutdown(clientConnection, SHUT_WR); ensureEventuallyDrainPeerConnection(); sendPeerResponse( "HTTP/1.1 200 OK\r\n" "Content-Length: 2\r\n\r\n" "ok"); waitUntilSessionClosed(); ensure("(1)", testSession.isSuccessful()); ensure("(2)", !testSession.wantsKeepAlive()); } TEST_METHOD(48) { set_test_name("Session protocol: if the client prematurely" " closes their outbound connection to us, and the" " application decides not to finish the response (close)," " we still try to send a 502 (which shouldn't log warn)"); init(); useTestSessionObject(); connectToServer(); sendRequest( "GET /hello HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: close\r\n" "\r\n"); waitUntilSessionInitiated(); ensureNeverDrainPeerConnection(); shutdown(clientConnection, SHUT_WR); ensureEventuallyDrainPeerConnection(); close(testSession.peerFd()); string header = readResponseHeader(); ensure(containsSubstring(header, "HTTP/1.1 502")); } TEST_METHOD(49) { set_test_name("HTTP protocol: if the client prematurely" " closes their outbound connection to us, and the" " application decides not to finish the response (close)," " we still try to send a 502 (which shouldn't log warn)"); init(); useTestSessionObject(); testSession.setProtocol("http_session"); connectToServer(); sendRequest( "GET /hello HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: close\r\n" "\r\n"); waitUntilSessionInitiated(); ensureNeverDrainPeerConnection(); shutdown(clientConnection, SHUT_WR); ensureEventuallyDrainPeerConnection(); close(testSession.peerFd()); string header = readResponseHeader(); ensure(containsSubstring(header, "HTTP/1.1 502")); } TEST_METHOD(55) { set_test_name("Session protocol: if application decides not to " " finish the response (close), and the client is still there " " we should send a 502 (which should log warn)"); init(); useTestSessionObject(); connectToServer(); sendRequest( "GET /hello HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: close\r\n" "\r\n"); waitUntilSessionInitiated(); ensureNeverDrainPeerConnection(); if (defaultLogLevel == (LoggingKit::Level) DEFAULT_LOG_LEVEL) { // If the user did not customize the test's log level, // then we'll want to tone down the noise. LoggingKit::setLevel(LoggingKit::CRIT); } close(testSession.peerFd()); string header = readResponseHeader(); ensure(containsSubstring(header, "HTTP/1.1 502")); } TEST_METHOD(56) { set_test_name("HTTP protocol: if application decides not to " " finish the response (close), and the client is still there " " we should send a 502 (which should log warn)"); init(); useTestSessionObject(); testSession.setProtocol("http_session"); connectToServer(); sendRequest( "GET /hello HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: close\r\n" "\r\n"); waitUntilSessionInitiated(); ensureNeverDrainPeerConnection(); if (defaultLogLevel == (LoggingKit::Level) DEFAULT_LOG_LEVEL) { // If the user did not customize the test's log level, // then we'll want to tone down the noise. LoggingKit::setLevel(LoggingKit::CRIT); } close(testSession.peerFd()); string header = readResponseHeader(); ensure(containsSubstring(header, "HTTP/1.1 502")); } } SpawningKit/HandshakePrepareTest.cpp 0000644 00000014313 14756504011 0013560 0 ustar 00 #include <TestSupport.h> #include <Core/SpawningKit/Handshake/Prepare.h> #include <unistd.h> #include <FileTools/FileManip.h> #include <SystemTools/UserDatabase.h> #include <Utils.h> using namespace Passenger; using namespace Passenger::SpawningKit; namespace tut { struct Core_SpawningKit_HandshakePrepareTest: public TestBase { WrapperRegistry::Registry wrapperRegistry; SpawningKit::Context::Schema schema; SpawningKit::Context context; SpawningKit::Config config; boost::shared_ptr<HandshakeSession> session; Core_SpawningKit_HandshakePrepareTest() : context(schema) { wrapperRegistry.finalize(); context.resourceLocator = resourceLocator; context.wrapperRegistry = &wrapperRegistry; context.integrationMode = "standalone"; context.spawnDir = getSystemTempDir(); context.finalize(); config.appGroupName = "appgroup"; config.appRoot = "/tmp/myapp"; config.startCommand = "echo hi"; config.startupFile = "/tmp/myapp/app.py"; config.appType = "wsgi"; config.spawnMethod = "direct"; config.bindAddress = "127.0.0.1"; config.user = lookupSystemUsernameByUid(getuid()); config.group = lookupSystemGroupnameByGid(getgid()); config.internStrings(); } void init(JourneyType type) { vector<StaticString> errors; ensure("Config is valid", config.validate(errors)); session = boost::make_shared<HandshakeSession>(context, config, type); session->journey.setStepInProgress(SPAWNING_KIT_PREPARATION); } void initAndExec(JourneyType type, const Json::Value &extraArgs = Json::Value()) { init(type); HandshakePrepare(*session, extraArgs).execute().finalize(); } }; DEFINE_TEST_GROUP(Core_SpawningKit_HandshakePrepareTest); TEST_METHOD(1) { set_test_name("It resolves the user and group ID"); initAndExec(SPAWN_DIRECTLY); ensure_equals("UID is resolved", session->uid, getuid()); ensure_equals("GID is resolved", session->gid, getgid()); ensure("Home dir is resolved", !session->homedir.empty()); ensure("Shell is resolved", !session->shell.empty()); } TEST_METHOD(2) { set_test_name("It raises an error if the user does not exist"); config.user = "doesnotexist"; try { initAndExec(SPAWN_DIRECTLY); fail("Exception expected"); } catch (const SpawnException &) { // Pass } } TEST_METHOD(3) { set_test_name("It raises an error if the group does not exist"); config.group = "doesnotexist"; try { initAndExec(SPAWN_DIRECTLY); fail("Exception expected"); } catch (const SpawnException &) { // Pass } } TEST_METHOD(5) { set_test_name("It creates a work directory"); initAndExec(SPAWN_DIRECTLY); ensure_equals(getFileType(session->workDir->getPath()), FT_DIRECTORY); ensure_equals(getFileType(session->workDir->getPath() + "/response"), FT_DIRECTORY); } #if 0 TEST_METHOD(6) { set_test_name("It infers the application code revision from a REVISION file"); TempDir tempdir("tmp.app"); createFile("tmp.app/REVISION", "myversion\n"); config.appRoot = "tmp.app"; initAndExec(SPAWN_DIRECTLY); ensure_equals(session->result.codeRevision, "myversion"); } TEST_METHOD(7) { set_test_name("It infers the application code revision from the" " Capistrano-style symlink in the app root path"); TempDir tempdir("tmp.app"); makeDirTree("tmp.app/myversion"); symlink("myversion", "tmp.app/current"); config.appRoot = "tmp.app/current"; initAndExec(SPAWN_DIRECTLY); ensure_equals(session->result.codeRevision, "myversion"); } #endif TEST_METHOD(10) { set_test_name("In case of a generic app, it finds a free port for the app to listen on"); unsigned long long timeout = 1000000; config.genericApp = true; initAndExec(SPAWN_DIRECTLY); Json::Value doc = context.inspectConfig(); ensure("Port found", session->expectedStartPort > 0); ensure("Port is within range (1)", session->expectedStartPort >= doc["min_port_range"]["effective_value"].asUInt()); ensure("Port is within range (2)", session->expectedStartPort <= doc["max_port_range"]["effective_value"].asUInt()); ensure("Port is not used", !pingTcpServer("127.0.0.1", session->expectedStartPort, &timeout)); } TEST_METHOD(11) { set_test_name("If findFreePort is true, it finds a free port for the app to listen on"); unsigned long long timeout = 1000000; config.findFreePort = true; initAndExec(SPAWN_DIRECTLY); Json::Value doc = context.inspectConfig(); ensure("Port found", session->expectedStartPort > 0); ensure("Port is within range (1)", session->expectedStartPort >= doc["min_port_range"]["effective_value"].asUInt()); ensure("Port is within range (2)", session->expectedStartPort <= doc["max_port_range"]["effective_value"].asUInt()); ensure("Port is not used", !pingTcpServer("127.0.0.1", session->expectedStartPort, &timeout)); } TEST_METHOD(15) { set_test_name("It dumps arguments into the work directory"); initAndExec(SPAWN_DIRECTLY); ensure(fileExists(session->workDir->getPath() + "/args.json")); ensure(fileExists(session->workDir->getPath() + "/args/app_root")); ensure_equals(unsafeReadFile(session->workDir->getPath() + "/args/app_root"), config.appRoot); } struct Test16DebugSupport: public HandshakePrepare::DebugSupport { virtual void beforeAdjustTimeout() { usleep(100000); } }; TEST_METHOD(16) { set_test_name("It adjusts the timeout when done"); Test16DebugSupport debugSupport; config.startTimeoutMsec = 1000; init(SPAWN_DIRECTLY); HandshakePrepare preparation(*session); preparation.debugSupport = &debugSupport; preparation.execute().finalize(); ensure("(1)", session->timeoutUsec <= 910000); ensure("(2)", session->timeoutUsec >= 100000); } struct Test17DebugSupport: public HandshakePrepare::DebugSupport { virtual void beforeAdjustTimeout() { throw RuntimeException("oh no"); } }; TEST_METHOD(17) { set_test_name("Upon throwing an exception, it sets the SPAWNING_KIT_PREPARATION step to errored"); Test17DebugSupport debugSupport; init(SPAWN_DIRECTLY); HandshakePrepare preparation(*session); preparation.debugSupport = &debugSupport; try { preparation.execute().finalize(); fail("SpawnException expected"); } catch (const SpawnException &) { ensure_equals(session->journey.getFirstFailedStep(), SPAWNING_KIT_PREPARATION); } } } SpawningKit/HandshakePerformTest.cpp 0000644 00000067424 14756504011 0013607 0 ustar 00 #include <TestSupport.h> #include <Core/SpawningKit/Handshake/Prepare.h> #include <Core/SpawningKit/Handshake/Perform.h> #include <LoggingKit/Context.h> #include <SystemTools/UserDatabase.h> #include <boost/bind/bind.hpp> #include <cstdio> #include <IOTools/IOUtils.h> using namespace std; using namespace Passenger; using namespace Passenger::SpawningKit; namespace tut { struct Core_SpawningKit_HandshakePerformTest: public TestBase { WrapperRegistry::Registry wrapperRegistry; SpawningKit::Context::Schema schema; SpawningKit::Context context; SpawningKit::Config config; boost::shared_ptr<HandshakeSession> session; pid_t pid; Pipe stdoutAndErr; HandshakePerform::DebugSupport *debugSupport; AtomicInt counter; FileDescriptor server; Core_SpawningKit_HandshakePerformTest() : context(schema), pid(getpid()), debugSupport(NULL) { wrapperRegistry.finalize(); context.resourceLocator = resourceLocator; context.wrapperRegistry = &wrapperRegistry; context.integrationMode = "standalone"; context.spawnDir = getSystemTempDir(); config.appGroupName = "appgroup"; config.appRoot = "/tmp/myapp"; config.startCommand = "echo hi"; config.startupFile = "/tmp/myapp/app.py"; config.appType = "wsgi"; config.spawnMethod = "direct"; config.bindAddress = "127.0.0.1"; config.user = lookupSystemUsernameByUid(getuid()); config.group = lookupSystemGroupnameByGid(getgid()); config.internStrings(); } ~Core_SpawningKit_HandshakePerformTest() { Json::Value config; vector<ConfigKit::Error> errors; LoggingKit::ConfigChangeRequest req; config["level"] = DEFAULT_LOG_LEVEL_NAME; config["app_output_log_level"] = DEFAULT_APP_OUTPUT_LOG_LEVEL_NAME; if (LoggingKit::context->prepareConfigChange(config, errors, req)) { LoggingKit::context->commitConfigChange(req); } else { P_BUG("Error configuring LoggingKit: " << ConfigKit::toString(errors)); } } void init(JourneyType type) { vector<StaticString> errors; ensure("Config is valid", config.validate(errors)); context.finalize(); session = boost::make_shared<HandshakeSession>(context, config, type); session->journey.setStepInProgress(SPAWNING_KIT_PREPARATION); HandshakePrepare(*session).execute().finalize(); session->journey.setStepInProgress(SPAWNING_KIT_HANDSHAKE_PERFORM); session->journey.setStepInProgress(SUBPROCESS_BEFORE_FIRST_EXEC); } void execute() { HandshakePerform performer(*session, pid, FileDescriptor(), stdoutAndErr.first); performer.debugSupport = debugSupport; performer.execute(); counter++; } static Json::Value createGoodPropertiesJson() { Json::Value socket, doc; socket["address"] = "tcp://127.0.0.1:3000"; socket["protocol"] = "http"; socket["concurrency"] = 1; socket["accept_http_requests"] = true; doc["sockets"].append(socket); return doc; } void signalFinish() { writeFile(session->responseDir + "/finish", "1"); } void signalFinishWithError() { writeFile(session->responseDir + "/finish", "0"); } }; struct FreePortDebugSupport: public HandshakePerform::DebugSupport { Core_SpawningKit_HandshakePerformTest *test; HandshakeSession *session; AtomicInt expectedStartPort; virtual void beginWaitUntilSpawningFinished() { expectedStartPort = session->expectedStartPort; test->counter++; } }; struct CrashingDebugSupport: public HandshakePerform::DebugSupport { virtual void beginWaitUntilSpawningFinished() { throw RuntimeException("oh no!"); } }; DEFINE_TEST_GROUP_WITH_LIMIT(Core_SpawningKit_HandshakePerformTest, 80); /***** General logic *****/ TEST_METHOD(1) { set_test_name("If the app is generic, it finishes when the app is pingable"); FreePortDebugSupport debugSupport; this->debugSupport = &debugSupport; config.genericApp = true; init(SPAWN_DIRECTLY); debugSupport.test = this; debugSupport.session = session.get(); TempThread thr(boost::bind(&Core_SpawningKit_HandshakePerformTest::execute, this)); EVENTUALLY(1, result = counter == 1; ); server.assign(createTcpServer("127.0.0.1", debugSupport.expectedStartPort.get()), NULL, 0); EVENTUALLY(1, result = counter == 2; ); } TEST_METHOD(2) { set_test_name("If findFreePort is true, it finishes when the app is pingable"); FreePortDebugSupport debugSupport; this->debugSupport = &debugSupport; config.findFreePort = true; init(SPAWN_DIRECTLY); debugSupport.test = this; debugSupport.session = session.get(); TempThread thr(boost::bind(&Core_SpawningKit_HandshakePerformTest::execute, this)); EVENTUALLY(1, result = counter == 1; ); server.assign(createTcpServer("127.0.0.1", debugSupport.expectedStartPort.get()), NULL, 0); EVENTUALLY(1, result = counter == 2; ); } TEST_METHOD(3) { set_test_name("It finishes when the app has sent the finish signal"); init(SPAWN_DIRECTLY); TempThread thr(boost::bind(&Core_SpawningKit_HandshakePerformTest::execute, this)); SHOULD_NEVER_HAPPEN(100, result = counter > 0; ); createFile(session->responseDir + "/properties.json", createGoodPropertiesJson().toStyledString()); signalFinish(); EVENTUALLY(1, result = counter == 1; ); } TEST_METHOD(10) { set_test_name("It raises an error if the process exits prematurely"); init(SPAWN_DIRECTLY); pid = fork(); if (pid == 0) { // Exit child _exit(1); } try { execute(); fail("SpawnException expected"); } catch (const SpawnException &e) { ensure_equals(StaticString(e.what()), "The application process exited prematurely."); } } TEST_METHOD(11) { set_test_name("It raises an error if the procedure took too long"); config.startTimeoutMsec = 50; init(SPAWN_DIRECTLY); pid = fork(); if (pid == 0) { // Exit child usleep(1000000); _exit(1); } try { execute(); fail("SpawnException expected"); } catch (const SpawnException &e) { ensure_equals(StaticString(e.what()), "A timeout occurred while spawning an application process."); } } TEST_METHOD(15) { set_test_name("In the event of an error, it sets the SPAWNING_KIT_HANDSHAKE_PERFORM step to the errored state"); CrashingDebugSupport debugSupport; this->debugSupport = &debugSupport; init(SPAWN_DIRECTLY); try { execute(); fail("SpawnException expected"); } catch (const SpawnException &) { ensure_equals(session->journey.getFirstFailedStep(), SPAWNING_KIT_HANDSHAKE_PERFORM); } } TEST_METHOD(16) { set_test_name("In the event of an error, the exception contains journey state information from the response directory"); CrashingDebugSupport debugSupport; this->debugSupport = &debugSupport; init(SPAWN_DIRECTLY); createFile(session->responseDir + "/steps/subprocess_listen/state", "STEP_ERRORED"); try { execute(); fail("SpawnException expected"); } catch (const SpawnException &) { ensure_equals(session->journey.getStepInfo(SUBPROCESS_LISTEN).state, STEP_ERRORED); } } TEST_METHOD(17) { set_test_name("In the event of an error, the exception contains subprocess stdout and stderr data"); Pipe p = createPipe(__FILE__, __LINE__); CrashingDebugSupport debugSupport; init(SPAWN_DIRECTLY); HandshakePerform performer(*session, pid, FileDescriptor(), p.first); performer.debugSupport = &debugSupport; Json::Value config; vector<ConfigKit::Error> errors; LoggingKit::ConfigChangeRequest req; config["app_output_log_level"] = "debug"; if (LoggingKit::context->prepareConfigChange(config, errors, req)) { LoggingKit::context->commitConfigChange(req); } else { P_BUG("Error configuring LoggingKit: " << ConfigKit::toString(errors)); } writeExact(p.second, "hi\n"); try { performer.execute(); fail("SpawnException expected"); } catch (const SpawnException &e) { ensure_equals(e.getStdoutAndErrData(), "hi\n"); } } TEST_METHOD(18) { set_test_name("In the event of an error caused by the subprocess, the exception contains messages from" " the subprocess as dumped in the response directory"); init(SPAWN_DIRECTLY); pid = fork(); if (pid == 0) { // Exit child _exit(1); } createFile(session->responseDir + "/error/summary", "the summary"); createFile(session->responseDir + "/error/problem_description.txt", "the <problem>"); createFile(session->responseDir + "/error/advanced_problem_details", "the advanced problem details"); createFile(session->responseDir + "/error/solution_description.html", "the <b>solution</b>"); try { execute(); fail("SpawnException expected"); } catch (const SpawnException &e) { ensure_equals(e.getSummary(), "the summary"); ensure_equals(e.getProblemDescriptionHTML(), "the <problem>"); ensure_equals(e.getAdvancedProblemDetails(), "the advanced problem details"); ensure_equals(e.getSolutionDescriptionHTML(), "the <b>solution</b>"); } } TEST_METHOD(19) { set_test_name("In the event of success, it loads the journey state information from the response directory"); init(SPAWN_DIRECTLY); TempThread thr(boost::bind(&Core_SpawningKit_HandshakePerformTest::execute, this)); createFile(session->responseDir + "/properties.json", createGoodPropertiesJson().toStyledString()); createFile(session->responseDir + "/steps/subprocess_listen/state", "STEP_PERFORMED"); signalFinish(); EVENTUALLY(5, result = counter == 1; ); ensure_equals(session->journey.getStepInfo(SUBPROCESS_LISTEN).state, STEP_PERFORMED); } TEST_METHOD(20) { // Limited test of whether the code mitigates symlink attacks. set_test_name("It does not read from symlinks"); init(SPAWN_DIRECTLY); createFile(session->responseDir + "/properties-real.json", createGoodPropertiesJson().toStyledString()); symlink("properties-real.json", (session->responseDir + "/properties.json").c_str()); TempThread thr(boost::bind(&Core_SpawningKit_HandshakePerformTest::signalFinish, this)); try { execute(); fail("SpawnException expected"); } catch (const SpawnException &e) { ensure(containsSubstring(e.getSummary(), "Cannot open 'properties.json'")); } } /***** Success response handling *****/ TEST_METHOD(30) { set_test_name("The result object contains basic information such as FDs and time"); init(SPAWN_DIRECTLY); TempThread thr(boost::bind(&Core_SpawningKit_HandshakePerformTest::execute, this)); createFile(session->responseDir + "/properties.json", createGoodPropertiesJson().toStyledString()); createFile(session->responseDir + "/steps/subprocess_listen/state", "STEP_PERFORMED"); signalFinish(); EVENTUALLY(5, result = counter == 1; ); ensure_equals(session->result.pid, pid); ensure(session->result.spawnStartTime != 0); ensure(session->result.spawnEndTime >= session->result.spawnStartTime); ensure(session->result.spawnStartTimeMonotonic != 0); ensure(session->result.spawnEndTimeMonotonic >= session->result.spawnStartTimeMonotonic); } TEST_METHOD(31) { set_test_name("The result object contains sockets specified in properties.json"); init(SPAWN_DIRECTLY); TempThread thr(boost::bind(&Core_SpawningKit_HandshakePerformTest::execute, this)); createFile(session->responseDir + "/properties.json", createGoodPropertiesJson().toStyledString()); createFile(session->responseDir + "/steps/subprocess_listen/state", "STEP_PERFORMED"); signalFinish(); EVENTUALLY(5, result = counter == 1; ); ensure_equals(session->result.sockets.size(), 1u); ensure_equals(session->result.sockets[0].address, "tcp://127.0.0.1:3000"); ensure_equals(session->result.sockets[0].protocol, "http"); ensure_equals(session->result.sockets[0].concurrency, 1); ensure(session->result.sockets[0].acceptHttpRequests); } TEST_METHOD(32) { set_test_name("If the app is generic, it automatically registers the free port as a request-handling socket"); FreePortDebugSupport debugSupport; this->debugSupport = &debugSupport; config.genericApp = true; init(SPAWN_DIRECTLY); debugSupport.test = this; debugSupport.session = session.get(); TempThread thr(boost::bind(&Core_SpawningKit_HandshakePerformTest::execute, this)); EVENTUALLY(1, result = counter == 1; ); server.assign(createTcpServer("127.0.0.1", debugSupport.expectedStartPort.get()), NULL, 0); EVENTUALLY(1, result = counter == 2; ); ensure_equals(session->result.sockets.size(), 1u); ensure_equals(session->result.sockets[0].address, "tcp://127.0.0.1:" + toString(session->expectedStartPort)); ensure_equals(session->result.sockets[0].protocol, "http"); ensure_equals(session->result.sockets[0].concurrency, -1); ensure(session->result.sockets[0].acceptHttpRequests); } TEST_METHOD(33) { set_test_name("If findFreePort is true, it automatically registers the free port as a request-handling socket"); FreePortDebugSupport debugSupport; this->debugSupport = &debugSupport; config.findFreePort = true; init(SPAWN_DIRECTLY); debugSupport.test = this; debugSupport.session = session.get(); TempThread thr(boost::bind(&Core_SpawningKit_HandshakePerformTest::execute, this)); EVENTUALLY(1, result = counter == 1; ); server.assign(createTcpServer("127.0.0.1", debugSupport.expectedStartPort.get()), NULL, 0); EVENTUALLY(1, result = counter == 2; ); ensure_equals(session->result.sockets.size(), 1u); ensure_equals(session->result.sockets[0].address, "tcp://127.0.0.1:" + toString(session->expectedStartPort)); ensure_equals(session->result.sockets[0].protocol, "http"); ensure_equals(session->result.sockets[0].concurrency, -1); ensure(session->result.sockets[0].acceptHttpRequests); } TEST_METHOD(34) { set_test_name("It raises an error if we expected the subprocess to create a properties.json," " but the file does not conform to the required format"); init(SPAWN_DIRECTLY); createFile(session->responseDir + "/properties.json", "{ \"sockets\": {} }"); TempThread thr(boost::bind(&Core_SpawningKit_HandshakePerformTest::signalFinish, this)); try { execute(); fail("SpawnException expected"); } catch (const SpawnException &e) { ensure(containsSubstring(e.getSummary(), "'sockets' must be an array")); } } TEST_METHOD(35) { set_test_name("It raises an error if we expected the subprocess to specify at" " least one request-handling socket in properties.json, yet the file does" " not specify any"); Json::Value socket, doc; socket["address"] = "tcp://127.0.0.1:3000"; socket["protocol"] = "http"; socket["concurrency"] = 1; doc["sockets"].append(socket); init(SPAWN_DIRECTLY); createFile(session->responseDir + "/properties.json", doc.toStyledString()); TempThread thr(boost::bind(&Core_SpawningKit_HandshakePerformTest::signalFinish, this)); try { execute(); fail("SpawnException expected"); } catch (const SpawnException &e) { ensure(containsSubstring(e.getSummary(), "the application did not report any sockets to receive requests on")); } } TEST_METHOD(36) { set_test_name("It raises an error if we expected the subprocess to specify at" " least one request-handling socket in properties.json, yet properties.json" " does not exist"); init(SPAWN_DIRECTLY); TempThread thr(boost::bind(&Core_SpawningKit_HandshakePerformTest::signalFinish, this)); try { execute(); fail("SpawnException expected"); } catch (const SpawnException &e) { ensure(containsSubstring(e.getSummary(), "sockets are not supplied")); } } TEST_METHOD(37) { set_test_name("It raises an error if we expected the subprocess to specify at" " least one preloader command socket in properties.json, yet the file does" " not specify any"); Json::Value socket, doc; socket["address"] = "tcp://127.0.0.1:3000"; socket["protocol"] = "http"; socket["concurrency"] = 1; doc["sockets"].append(socket); init(START_PRELOADER); createFile(session->responseDir + "/properties.json", doc.toStyledString()); TempThread thr(boost::bind(&Core_SpawningKit_HandshakePerformTest::signalFinish, this)); try { execute(); fail("SpawnException expected"); } catch (const SpawnException &e) { ensure(containsSubstring(e.getSummary(), "the application did not report any sockets to receive preloader commands on")); } } TEST_METHOD(38) { set_test_name("It raises an error if we expected the subprocess to specify at" " least one preloader command socket in properties.json, yet properties.json" " does not exist"); init(START_PRELOADER); TempThread thr(boost::bind(&Core_SpawningKit_HandshakePerformTest::signalFinish, this)); try { execute(); fail("SpawnException expected"); } catch (const SpawnException &e) { ensure(containsSubstring(e.getSummary(), "sockets are not supplied")); } } TEST_METHOD(39) { set_test_name("It raises an error if properties.json specifies a Unix domain socket" " that is not located in the apps.s subdir of the instance directory"); TempDir tmpDir("tmp.instance"); context.instanceDir = absolutizePath("tmp.instance"); init(SPAWN_DIRECTLY); Json::Value doc = createGoodPropertiesJson(); doc["sockets"][0]["address"] = "unix:/foo"; createFile(session->responseDir + "/properties.json", doc.toStyledString()); TempThread thr(boost::bind(&Core_SpawningKit_HandshakePerformTest::signalFinish, this)); try { execute(); fail("SpawnException expected"); } catch (const SpawnException &e) { ensure(containsSubstring(e.getSummary(), "must be an absolute path to a file in")); } } TEST_METHOD(40) { set_test_name("It raises an error if properties.json specifies a Unix domain socket" " that is not owned by the app"); if (geteuid() != 0) { return; } TempDir tmpDir("tmp.instance"); mkdir("tmp.instance/apps.s", 0700); string socketPath = absolutizePath("tmp.instance/apps.s/foo.sock"); init(SPAWN_DIRECTLY); Json::Value doc = createGoodPropertiesJson(); doc["sockets"][0]["address"] = "unix:" + socketPath; createFile(session->responseDir + "/properties.json", doc.toStyledString()); safelyClose(createUnixServer(socketPath)); chown(socketPath.c_str(), 1, 1); TempThread thr(boost::bind(&Core_SpawningKit_HandshakePerformTest::signalFinish, this)); try { execute(); fail("SpawnException expected"); } catch (const SpawnException &e) { ensure("(1)", containsSubstring(e.getSummary(), "must be owned by user")); } } /***** Error response handling *****/ TEST_METHOD(50) { set_test_name("It raises an error if the application responded with an error"); init(SPAWN_DIRECTLY); TempThread thr(boost::bind(&Core_SpawningKit_HandshakePerformTest::signalFinishWithError, this)); try { execute(); fail("SpawnException expected"); } catch (const SpawnException &e) { ensure_equals(e.getSummary(), "The web application aborted with an error during startup."); } } TEST_METHOD(51) { set_test_name("The exception contains error messages provided by the application"); init(SPAWN_DIRECTLY); writeFile(session->workDir->getPath() + "/response/error/summary", "the summary"); writeFile(session->workDir->getPath() + "/response/error/problem_description.html", "the problem description"); writeFile(session->workDir->getPath() + "/response/error/solution_description.html", "the solution description"); TempThread thr(boost::bind(&Core_SpawningKit_HandshakePerformTest::signalFinishWithError, this)); try { execute(); fail("SpawnException expected"); } catch (const SpawnException &e) { ensure_equals(e.getSummary(), "the summary"); ensure_equals(e.getProblemDescriptionHTML(), "the problem description"); ensure_equals(e.getSolutionDescriptionHTML(), "the solution description"); } } TEST_METHOD(52) { set_test_name("The exception describes which steps in the journey had failed"); init(SPAWN_DIRECTLY); TempThread thr(boost::bind(&Core_SpawningKit_HandshakePerformTest::signalFinishWithError, this)); try { execute(); } catch (const SpawnException &e) { ensure_equals(e.getJourney().getFirstFailedStep(), SUBPROCESS_BEFORE_FIRST_EXEC); } } TEST_METHOD(53) { set_test_name("The exception contains the subprocess' output"); Json::Value config; vector<ConfigKit::Error> errors; LoggingKit::ConfigChangeRequest req; config["app_output_log_level"] = "debug"; if (LoggingKit::context->prepareConfigChange(config, errors, req)) { LoggingKit::context->commitConfigChange(req); } else { P_BUG("Error configuring LoggingKit: " << ConfigKit::toString(errors)); } init(SPAWN_DIRECTLY); stdoutAndErr = createPipe(__FILE__, __LINE__); writeExact(stdoutAndErr.second, "oh no"); TempThread thr(boost::bind(&Core_SpawningKit_HandshakePerformTest::signalFinishWithError, this)); try { execute(); fail("SpawnException expected"); } catch (const SpawnException &e) { ensure_equals(e.getStdoutAndErrData(), "oh no"); } } TEST_METHOD(54) { set_test_name("The exception contains the subprocess' environment variables dump"); init(SPAWN_DIRECTLY); writeFile(session->workDir->getPath() + "/envdump/envvars", "the env dump"); TempThread thr(boost::bind(&Core_SpawningKit_HandshakePerformTest::signalFinishWithError, this)); try { execute(); fail("SpawnException expected"); } catch (const SpawnException &e) { ensure_equals(e.getSubprocessEnvvars(), "the env dump"); } } TEST_METHOD(55) { set_test_name("If the subprocess fails without setting a specific" " journey step to the ERRORED state," " and there is a subprocess journey step in the IN_PROGRESS state," " then we set that latter step to the ERRORED state"); init(SPAWN_DIRECTLY); pid = fork(); if (pid == 0) { // Exit child _exit(1); } createFile(session->responseDir + "/steps/subprocess_before_first_exec/state", "STEP_PERFORMED"); createFile(session->responseDir + "/steps/subprocess_before_first_exec/duration", "1"); createFile(session->responseDir + "/steps/subprocess_spawn_env_setupper_before_shell/state", "STEP_IN_PROGRESS"); try { execute(); fail("SpawnException expected"); } catch (const SpawnException &e) { ensure_equals("SPAWNING_KIT_HANDSHAKE_PERFORM is in the IN_PROGRESS state", e.getJourney().getStepInfo(SPAWNING_KIT_HANDSHAKE_PERFORM).state, STEP_IN_PROGRESS); ensure_equals("SUBPROCESS_BEFORE_FIRST_EXEC is in the PERFORMED state", e.getJourney().getStepInfo(SUBPROCESS_BEFORE_FIRST_EXEC).state, STEP_PERFORMED); ensure_equals("SUBPROCESS_SPAWN_ENV_SETUPPER_BEFORE_SHELL is in the ERRORED state", e.getJourney().getStepInfo(SUBPROCESS_SPAWN_ENV_SETUPPER_BEFORE_SHELL).state, STEP_ERRORED); ensure_equals("SUBPROCESS_OS_SHELL is in the NOT_STARTED state", e.getJourney().getStepInfo(SUBPROCESS_OS_SHELL).state, STEP_NOT_STARTED); ensure_equals("SUBPROCESS_SPAWN_ENV_SETUPPER_AFTER_SHELL is in the NOT_STARTED state", e.getJourney().getStepInfo(SUBPROCESS_SPAWN_ENV_SETUPPER_AFTER_SHELL).state, STEP_NOT_STARTED); } } TEST_METHOD(56) { set_test_name("If the subprocess fails without setting a specific" " journey step to the ERRORED state," " and there is no subprocess journey step in the IN_PROGRESS state," " and no subprocess journey steps are in the PERFORMED state," " then we set the first subprocess journey step to the ERRORED state"); init(SPAWN_DIRECTLY); pid = fork(); if (pid == 0) { // Exit child _exit(1); } try { execute(); fail("SpawnException expected"); } catch (const SpawnException &e) { ensure_equals("SPAWNING_KIT_HANDSHAKE_PERFORM is in the IN_PROGRESS state", e.getJourney().getStepInfo(SPAWNING_KIT_HANDSHAKE_PERFORM).state, STEP_IN_PROGRESS); ensure_equals("SUBPROCESS_BEFORE_FIRST_EXEC is in the ERRORED state", e.getJourney().getStepInfo(SUBPROCESS_BEFORE_FIRST_EXEC).state, STEP_ERRORED); ensure_equals("SUBPROCESS_SPAWN_ENV_SETUPPER_BEFORE_SHELL is in the NOT_STARTED state", e.getJourney().getStepInfo(SUBPROCESS_SPAWN_ENV_SETUPPER_BEFORE_SHELL).state, STEP_NOT_STARTED); } } TEST_METHOD(57) { set_test_name("If the subprocess fails without setting a specific" " journey step to the ERRORED state," " and there is no subprocess journey step in the IN_PROGRESS state," " and some but not all subprocess journey steps are in the PERFORMED state," " then we set the step that comes right after the last PERFORMED step," " to the ERRORED state"); init(SPAWN_DIRECTLY); pid = fork(); if (pid == 0) { // Exit child _exit(1); } createFile(session->responseDir + "/steps/subprocess_before_first_exec/state", "STEP_PERFORMED"); createFile(session->responseDir + "/steps/subprocess_before_first_exec/duration", "1"); try { execute(); fail("SpawnException expected"); } catch (const SpawnException &e) { ensure_equals("SPAWNING_KIT_HANDSHAKE_PERFORM is in the IN_PROGRESS state", e.getJourney().getStepInfo(SPAWNING_KIT_HANDSHAKE_PERFORM).state, STEP_IN_PROGRESS); ensure_equals("SUBPROCESS_BEFORE_FIRST_EXEC is in the PERFORMED state", e.getJourney().getStepInfo(SUBPROCESS_BEFORE_FIRST_EXEC).state, STEP_PERFORMED); ensure_equals("SUBPROCESS_SPAWN_ENV_SETUPPER_BEFORE_SHELL is in the ERRORED state", e.getJourney().getStepInfo(SUBPROCESS_SPAWN_ENV_SETUPPER_BEFORE_SHELL).state, STEP_ERRORED); ensure_equals("SUBPROCESS_OS_SHELL is in the NOT_STARTED state", e.getJourney().getStepInfo(SUBPROCESS_OS_SHELL).state, STEP_NOT_STARTED); ensure_equals("SUBPROCESS_SPAWN_ENV_SETUPPER_AFTER_SHELL is in the NOT_STARTED state", e.getJourney().getStepInfo(SUBPROCESS_SPAWN_ENV_SETUPPER_AFTER_SHELL).state, STEP_NOT_STARTED); } } TEST_METHOD(58) { set_test_name("If the subprocess fails without setting a specific" " journey step to the ERRORED state," " and there is no subprocess journey step in the IN_PROGRESS state," " and all subprocess journey steps are in the PERFORMED state," " then we set the last subprocess step to the ERRORED state"); init(SPAWN_DIRECTLY); pid = fork(); if (pid == 0) { // Exit child _exit(1); } JourneyStep firstStep = getFirstSubprocessJourneyStep(); JourneyStep lastStep = getLastSubprocessJourneyStep(); JourneyStep step; for (step = firstStep; step < lastStep; step = JourneyStep((int) step + 1)) { if (!session->journey.hasStep(step)) { continue; } createFile(session->responseDir + "/steps/" + journeyStepToStringLowerCase(step) + "/state", "STEP_PERFORMED"); createFile(session->responseDir + "/steps/" + journeyStepToStringLowerCase(step) + "/duration", "1"); } try { execute(); fail("SpawnException expected"); } catch (const SpawnException &e) { ensure_equals("SPAWNING_KIT_HANDSHAKE_PERFORM is in the IN_PROGRESS state", e.getJourney().getStepInfo(SPAWNING_KIT_HANDSHAKE_PERFORM).state, STEP_IN_PROGRESS); ensure_equals("SUBPROCESS_BEFORE_FIRST_EXEC is in the PERFORMED state", e.getJourney().getStepInfo(SUBPROCESS_BEFORE_FIRST_EXEC).state, STEP_PERFORMED); ensure_equals("SUBPROCESS_SPAWN_ENV_SETUPPER_BEFORE_SHELL is in the PERFORMED state", e.getJourney().getStepInfo(SUBPROCESS_SPAWN_ENV_SETUPPER_BEFORE_SHELL).state, STEP_PERFORMED); ensure_equals("SUBPROCESS_OS_SHELL is in the PERFORMED state", e.getJourney().getStepInfo(SUBPROCESS_OS_SHELL).state, STEP_PERFORMED); ensure_equals("SUBPROCESS_SPAWN_ENV_SETUPPER_AFTER_SHELL is in the PERFORMED state", e.getJourney().getStepInfo(SUBPROCESS_SPAWN_ENV_SETUPPER_AFTER_SHELL).state, STEP_PERFORMED); ensure_equals("SUBPROCESS_APP_LOAD_OR_EXEC is in the PERFORMED state", e.getJourney().getStepInfo(SUBPROCESS_APP_LOAD_OR_EXEC).state, STEP_PERFORMED); ensure_equals("SUBPROCESS_APP_LOAD_OR_EXEC is in the PERFORMED state", e.getJourney().getStepInfo(SUBPROCESS_APP_LOAD_OR_EXEC).state, STEP_PERFORMED); ensure_equals("SUBPROCESS_LISTEN is in the PERFORMED state", e.getJourney().getStepInfo(SUBPROCESS_LISTEN).state, STEP_PERFORMED); ensure_equals("SUBPROCESS_FINISH is in the ERRORED state", e.getJourney().getStepInfo(SUBPROCESS_FINISH).state, STEP_ERRORED); } } } SpawningKit/SmartSpawnerTest.cpp 0000644 00000017552 14756504011 0013011 0 ustar 00 #include <TestSupport.h> #include <jsoncpp/json.h> #include <Core/ApplicationPool/Options.h> #include <Core/SpawningKit/SmartSpawner.h> #include <LoggingKit/LoggingKit.h> #include <LoggingKit/Context.h> #include <FileDescriptor.h> #include <IOTools/IOUtils.h> #include <unistd.h> #include <climits> #include <signal.h> #include <fcntl.h> using namespace std; using namespace Passenger; using namespace Passenger::SpawningKit; namespace tut { struct Core_SpawningKit_SmartSpawnerTest: public TestBase { WrapperRegistry::Registry wrapperRegistry; SpawningKit::Context::Schema schema; SpawningKit::Context context; SpawningKit::Result result; Core_SpawningKit_SmartSpawnerTest() : context(schema) { wrapperRegistry.finalize(); context.resourceLocator = resourceLocator; context.wrapperRegistry = &wrapperRegistry; context.integrationMode = "standalone"; context.spawnDir = getSystemTempDir(); context.finalize(); Json::Value config; vector<ConfigKit::Error> errors; LoggingKit::ConfigChangeRequest req; config["level"] = "warn"; config["app_output_log_level"] = "debug"; if (LoggingKit::context->prepareConfigChange(config, errors, req)) { LoggingKit::context->commitConfigChange(req); } else { P_BUG("Error configuring LoggingKit: " << ConfigKit::toString(errors)); } } ~Core_SpawningKit_SmartSpawnerTest() { Json::Value config; vector<ConfigKit::Error> errors; LoggingKit::ConfigChangeRequest req; config["level"] = DEFAULT_LOG_LEVEL_NAME; config["app_output_log_level"] = DEFAULT_APP_OUTPUT_LOG_LEVEL_NAME; if (LoggingKit::context->prepareConfigChange(config, errors, req)) { LoggingKit::context->commitConfigChange(req); } else { P_BUG("Error configuring LoggingKit: " << ConfigKit::toString(errors)); } unlink("stub/wsgi/passenger_wsgi.pyc"); } boost::shared_ptr<SmartSpawner> createSpawner(const SpawningKit::AppPoolOptions &options, bool exitImmediately = false) { char buf[PATH_MAX + 1]; getcwd(buf, PATH_MAX); vector<string> command; command.push_back("ruby"); command.push_back(string(buf) + "/support/placebo-preloader.rb"); if (exitImmediately) { command.push_back("exit-immediately"); } return boost::make_shared<SmartSpawner>(&context, command, options); } SpawningKit::AppPoolOptions createOptions() { SpawningKit::AppPoolOptions options; options.appType = "directly-through-start-command"; options.spawnMethod = "smart"; options.loadShellEnvvars = false; return options; } }; DEFINE_TEST_GROUP(Core_SpawningKit_SmartSpawnerTest); #include "SpawnerTestCases.cpp" TEST_METHOD(10) { set_test_name("If the preloader has crashed then SmartSpawner will " "restart it and try again"); SpawningKit::AppPoolOptions options = createOptions(); options.appRoot = "stub/rack"; options.appStartCommand = "ruby start.rb"; options.startupFile = "start.rb"; boost::shared_ptr<SmartSpawner> spawner = createSpawner(options); if (defaultLogLevel == (LoggingKit::Level) DEFAULT_LOG_LEVEL) { // If the user did not customize the test's log level, // then we'll want to tone down the noise. LoggingKit::setLevel(LoggingKit::CRIT); } spawner->spawn(options); kill(spawner->getPreloaderPid(), SIGTERM); // Give it some time to exit. usleep(300000); // No exception at next spawn. spawner->spawn(options); } TEST_METHOD(11) { set_test_name("If the preloader still crashes after the restart then " "SmartSpawner will throw an exception"); SpawningKit::AppPoolOptions options = createOptions(); options.appRoot = "stub/rack"; options.appStartCommand = "ruby start.rb"; options.startupFile = "start.rb"; if (defaultLogLevel == (LoggingKit::Level) DEFAULT_LOG_LEVEL) { // If the user did not customize the test's log level, // then we'll want to tone down the noise. LoggingKit::setLevel(LoggingKit::CRIT); } boost::shared_ptr<SmartSpawner> spawner = createSpawner(options, true); try { spawner->spawn(options); fail("SpawnException expected"); } catch (const SpawnException &) { // Pass. } } TEST_METHOD(12) { set_test_name("If the preloader didn't start within the timeout" " then it's killed and an exception is thrown, which" " contains whatever it printed to stdout and stderr"); SpawningKit::AppPoolOptions options = createOptions(); options.appRoot = "stub/rack"; options.appStartCommand = "ruby start.rb"; options.startupFile = "start.rb"; options.startTimeout = 100; vector<string> preloaderCommand; preloaderCommand.push_back("bash"); preloaderCommand.push_back("-c"); preloaderCommand.push_back("echo hello world; sleep 60"); SmartSpawner spawner(&context, preloaderCommand, options); if (defaultLogLevel == (LoggingKit::Level) DEFAULT_LOG_LEVEL) { // If the user did not customize the test's log level, // then we'll want to tone down the noise. LoggingKit::setLevel(LoggingKit::CRIT); } try { spawner.spawn(options); fail("SpawnException expected"); } catch (const SpawnException &e) { ensure_equals(e.getErrorCategory(), SpawningKit::TIMEOUT_ERROR); if (e.getStdoutAndErrData().find("hello world\n") == string::npos) { // This might be caused by the machine being too slow. // Try again with a higher timeout. options.startTimeout = 10000; SmartSpawner spawner2(&context, preloaderCommand, options); try { spawner2.spawn(options); fail("SpawnException expected"); } catch (const SpawnException &e2) { ensure_equals(e2.getErrorCategory(), SpawningKit::TIMEOUT_ERROR); if (e2.getStdoutAndErrData().find("hello world\n") == string::npos) { fail(("Unexpected stdout/stderr output:\n" + e2.getStdoutAndErrData()).c_str()); } } } } } TEST_METHOD(13) { set_test_name("If the preloader crashed during startup," " then the resulting exception contains the stdout" " and stderr output"); SpawningKit::AppPoolOptions options = createOptions(); options.appRoot = "stub/rack"; options.appStartCommand = "ruby start.rb"; options.startupFile = "start.rb"; vector<string> preloaderCommand; preloaderCommand.push_back("bash"); preloaderCommand.push_back("-c"); preloaderCommand.push_back("echo hello world; exit 1"); SmartSpawner spawner(&context, preloaderCommand, options); if (defaultLogLevel == (LoggingKit::Level) DEFAULT_LOG_LEVEL) { // If the user did not customize the test's log level, // then we'll want to tone down the noise. LoggingKit::setLevel(LoggingKit::CRIT); } try { spawner.spawn(options); fail("SpawnException expected"); } catch (const SpawnException &e) { ensure_equals(e.getErrorCategory(), SpawningKit::INTERNAL_ERROR); ensure(e.getStdoutAndErrData().find("hello world\n") != string::npos); } } TEST_METHOD(14) { set_test_name("If the preloader encountered an error," " then the resulting exception" " takes note of the process's environment variables"); string envvars = modp::b64_encode("PASSENGER_FOO\0foo\0", sizeof("PASSENGER_FOO\0foo\0") - 1); SpawningKit::AppPoolOptions options = createOptions(); options.appRoot = "stub/rack"; options.appStartCommand = "ruby start.rb"; options.startupFile = "start.rb"; options.environmentVariables = envvars; vector<string> preloaderCommand; preloaderCommand.push_back("bash"); preloaderCommand.push_back("-c"); preloaderCommand.push_back("echo hello world >&2; exit 1"); SmartSpawner spawner(&context, preloaderCommand, options); if (defaultLogLevel == (LoggingKit::Level) DEFAULT_LOG_LEVEL) { // If the user did not customize the test's log level, // then we'll want to tone down the noise. LoggingKit::setLevel(LoggingKit::CRIT); } try { spawner.spawn(options); fail("SpawnException expected"); } catch (const SpawnException &e) { ensure(containsSubstring(e.getSubprocessEnvvars(), "PASSENGER_FOO=foo\n")); } } } SpawningKit/SpawnerTestCases.cpp 0000644 00000004540 14756504011 0012752 0 ustar 00 // Included in DirectSpawnerTest.cpp and SmartSpawnerTest.cpp. typedef boost::shared_ptr<Spawner> SpawnerPtr; TEST_METHOD(1) { set_test_name("Basic spawning test"); SpawningKit::AppPoolOptions options = createOptions(); options.appRoot = "stub/rack"; options.appStartCommand = "ruby start.rb"; options.startupFile = "start.rb"; SpawnerPtr spawner = createSpawner(options); result = spawner->spawn(options); ensure_equals(result.sockets.size(), 1u); FileDescriptor fd(connectToServer(result.sockets[0].address, __FILE__, __LINE__), NULL, 0); writeExact(fd, "ping\n"); ensure_equals(readAll(fd, 1024).first, "pong\n"); } TEST_METHOD(2) { set_test_name("It enforces the given start timeout"); SpawningKit::AppPoolOptions options = createOptions(); options.appRoot = "stub"; options.appStartCommand = "sleep 60"; options.startupFile = "."; options.startTimeout = 100; if (defaultLogLevel == (LoggingKit::Level) DEFAULT_LOG_LEVEL) { // If the user did not customize the test's log level, // then we'll want to tone down the noise. LoggingKit::setLevel(LoggingKit::CRIT); } EVENTUALLY(5, SpawnerPtr spawner = createSpawner(options); try { spawner->spawn(options); fail("SpawnException expected"); } catch (const SpawnException &e) { result = e.getErrorCategory() == SpawningKit::TIMEOUT_ERROR; if (!result) { // It didn't work, maybe because the server is too busy. // Try again with higher timeout. options.startTimeout = std::min<unsigned int>( options.startTimeout * 2, 1000); } if (!result) { // It didn't work, maybe because the server is too busy. // Try again with higher timeout. options.startTimeout = std::min<unsigned int>( options.startTimeout * 2, 1000); } } ); } TEST_METHOD(6) { set_test_name("The reported PID is correct"); SpawningKit::AppPoolOptions options = createOptions(); options.appRoot = "stub/rack"; options.appStartCommand = "ruby start.rb"; options.startupFile = "start.rb"; SpawnerPtr spawner = createSpawner(options); result = spawner->spawn(options); ensure_equals(result.sockets.size(), 1u); FileDescriptor fd(connectToServer(result.sockets[0].address, __FILE__, __LINE__), NULL, 0); writeExact(fd, "pid\n"); ensure_equals(readAll(fd, 1024).first, toString(result.pid) + "\n"); } SpawningKit/JourneyTest.cpp 0000644 00000003207 14756504011 0012006 0 ustar 00 #include <TestSupport.h> #include <Core/SpawningKit/Journey.h> using namespace Passenger; using namespace Passenger::SpawningKit; namespace tut { struct Core_SpawningKit_JourneyTest: public TestBase { }; DEFINE_TEST_GROUP(Core_SpawningKit_JourneyTest); TEST_METHOD(1) { set_test_name("Constructing a SPAWN_DIRECTLY journey results in" " the appropriate steps being defined in the journey"); Journey journey(SPAWN_DIRECTLY, true); ensure("(1)", journey.hasStep(SPAWNING_KIT_PREPARATION)); ensure("(2)", journey.hasStep(SUBPROCESS_EXEC_WRAPPER)); ensure("(3)", !journey.hasStep(SPAWNING_KIT_CONNECT_TO_PRELOADER)); ensure("(4)", !journey.hasStep(SUBPROCESS_PREPARE_AFTER_FORKING_FROM_PRELOADER)); } TEST_METHOD(2) { set_test_name("Constructing a START_PRELOADER journey results in" " the appropriate steps being defined in the journey"); Journey journey(START_PRELOADER, true); ensure("(1)", journey.hasStep(SPAWNING_KIT_PREPARATION)); ensure("(2)", journey.hasStep(SUBPROCESS_EXEC_WRAPPER)); ensure("(3)", !journey.hasStep(SPAWNING_KIT_CONNECT_TO_PRELOADER)); ensure("(4)", !journey.hasStep(SUBPROCESS_PREPARE_AFTER_FORKING_FROM_PRELOADER)); } TEST_METHOD(3) { set_test_name("Constructing a SPAWN_THROUGH_PRELOADER journey results in" " the appropriate steps being defined in the journey"); Journey journey(SPAWN_THROUGH_PRELOADER, true); ensure("(1)", journey.hasStep(SPAWNING_KIT_PREPARATION)); ensure("(2)", !journey.hasStep(SUBPROCESS_BEFORE_FIRST_EXEC)); ensure("(3)", journey.hasStep(SPAWNING_KIT_CONNECT_TO_PRELOADER)); ensure("(4)", journey.hasStep(SUBPROCESS_PREPARE_AFTER_FORKING_FROM_PRELOADER)); } } SpawningKit/UserSwitchingRulesTest.cpp 0000644 00000052707 14756504011 0014175 0 ustar 00 #include <TestSupport.h> #include <Core/ApplicationPool/Options.h> #include <Core/SpawningKit/UserSwitchingRules.h> #include <SystemTools/UserDatabase.h> #include <Utils.h> using namespace Passenger; using namespace Passenger::SpawningKit; namespace tut { struct Core_SpawningKit_UserSwitchingRulesTest: public TestBase { WrapperRegistry::Registry wrapperRegistry; AppPoolOptions options; UserSwitchingInfo result; Core_SpawningKit_UserSwitchingRulesTest() { wrapperRegistry.finalize(); options.spawnMethod = "direct"; options.loadShellEnvvars = false; options.appRoot = "tmp.wsgi"; options.appType = "wsgi"; options.defaultUser = testConfig["default_user"].asCString(); options.defaultGroup = testConfig["default_group"].asCString(); } }; #define SETUP_USER_SWITCHING_TEST(code) \ if (geteuid() != 0) { \ return; \ } \ TempDirCopy stub("stub/wsgi", "tmp.wsgi"); \ code #define RUN_USER_SWITCHING_TEST() \ result = prepareUserSwitching(options, wrapperRegistry) static uid_t uidFor(const string &userName) { OsUser osUser; if (lookupSystemUserByName(userName, osUser)) { return osUser.pwd.pw_uid; } else { throw RuntimeException("OS user account " + userName + " does not exist"); } } static gid_t gidFor(const string &groupName) { OsGroup osGroup; if (lookupSystemGroupByName(groupName, osGroup)) { return osGroup.grp.gr_gid; } else { throw RuntimeException("OS group account " + groupName + " does not exist"); } } DEFINE_TEST_GROUP(Core_SpawningKit_UserSwitchingRulesTest); // If 'user' is set // and 'user' is 'root' TEST_METHOD(1) { set_test_name("If user is set and user is root," " then it changes the user to the value of 'defaultUser'"); // It changes the user to the value of 'defaultUser'. SETUP_USER_SWITCHING_TEST( options.user = "root"; ); RUN_USER_SWITCHING_TEST(); ensure_equals(lookupSystemUsernameByUid(result.uid), testConfig["default_user"]); } TEST_METHOD(2) { set_test_name("If user is set and user is root," " and 'group' is given," " then it changes group to the given group name"); SETUP_USER_SWITCHING_TEST( options.user = "root"; options.group = testConfig["normal_group_1"].asCString(); ); RUN_USER_SWITCHING_TEST(); ensure_equals(lookupSystemGroupnameByGid(result.gid), testConfig["normal_group_1"].asString()); } TEST_METHOD(3) { set_test_name("If user is set and user is root," " and 'group' is set to the root group," " then it changes group to defaultGroup"); string rootGroup = lookupSystemGroupnameByGid(0); SETUP_USER_SWITCHING_TEST( options.user = "root"; options.group = rootGroup; ); RUN_USER_SWITCHING_TEST(); ensure_equals(lookupSystemGroupnameByGid(result.gid), testConfig["default_group"].asString()); } // and 'group' is set to '!STARTUP_FILE!'" TEST_METHOD(4) { set_test_name("If user is set, user is root," " and 'group' is set to '!STARTUP_FILE!'," " then it changes the group to the startup file's group"); SETUP_USER_SWITCHING_TEST( options.user = "root"; options.group = "!STARTUP_FILE!"; ); lchown("tmp.wsgi/passenger_wsgi.py", (uid_t) -1, gidFor(testConfig["normal_group_1"].asString())); RUN_USER_SWITCHING_TEST(); ensure_equals(lookupSystemGroupnameByGid(result.gid), testConfig["normal_group_1"].asString()); } TEST_METHOD(5) { set_test_name("If user is set, user is root," " and 'group' is set to '!STARTUP_FILE!'," " and the startup file is a symlink," " then it uses the symlink's group, not the target's group"); SETUP_USER_SWITCHING_TEST( options.user = "root"; options.group = "!STARTUP_FILE!"; ); lchown("tmp.wsgi/passenger_wsgi.py", (uid_t) -1, gidFor(testConfig["normal_group_2"].asString())); chown("tmp.wsgi/passenger_wsgi.py.real", (uid_t) -1, gidFor(testConfig["normal_group_1"].asString())); RUN_USER_SWITCHING_TEST(); ensure_equals(lookupSystemGroupnameByGid(result.gid), testConfig["normal_group_2"].asString()); } TEST_METHOD(6) { set_test_name("If user is set and user is root," " and 'group' is not given," " then it changes the group to defaultUser's primary group"); SETUP_USER_SWITCHING_TEST( options.user = "root"; ); RUN_USER_SWITCHING_TEST(); ensure_equals(lookupSystemGroupnameByGid(result.gid), getPrimaryGroupName(testConfig["default_user"].asString())); } // and 'user' is not 'root' TEST_METHOD(10) { set_test_name("If user is set and user is not root," " then it changes the user to the given username"); SETUP_USER_SWITCHING_TEST( options.user = testConfig["normal_user_1"].asCString(); ); RUN_USER_SWITCHING_TEST(); ensure_equals(lookupSystemUsernameByUid(result.uid), testConfig["normal_user_1"].asString()); } TEST_METHOD(11) { set_test_name("If user is set and user is not root," " and 'group' is given," " then it changes group to the given group name"); SETUP_USER_SWITCHING_TEST( options.user = testConfig["normal_user_1"].asCString(); options.group = testConfig["normal_group_1"].asCString(); ); RUN_USER_SWITCHING_TEST(); ensure_equals(lookupSystemGroupnameByGid(result.gid), testConfig["normal_group_1"].asString()); } TEST_METHOD(12) { set_test_name("If user is set and user is not root," " and 'group' is given," " then it changes the user to the given username"); SETUP_USER_SWITCHING_TEST( options.user = testConfig["normal_user_1"].asCString(); options.group = testConfig["normal_group_1"].asCString(); ); RUN_USER_SWITCHING_TEST(); ensure_equals(lookupSystemUsernameByUid(result.uid), testConfig["normal_user_1"].asString()); } TEST_METHOD(13) { set_test_name("If user is set and user is not root," " and 'group' is set to the root group," " then it changes group to defaultGroup"); string rootGroup = lookupSystemGroupnameByGid(0); SETUP_USER_SWITCHING_TEST( options.user = testConfig["normal_user_1"].asCString(); options.group = rootGroup; ); RUN_USER_SWITCHING_TEST(); ensure_equals(lookupSystemGroupnameByGid(result.gid), testConfig["default_group"].asString()); } TEST_METHOD(14) { set_test_name("If user is set and user is not root," " and 'group' is set to the root group," " then it changes the user to the given username"); string rootGroup = lookupSystemGroupnameByGid(0); SETUP_USER_SWITCHING_TEST( options.user = testConfig["normal_user_1"].asCString(); options.group = rootGroup; ); RUN_USER_SWITCHING_TEST(); ensure_equals(lookupSystemUsernameByUid(result.uid), testConfig["normal_user_1"].asString()); } // and 'group' is set to '!STARTUP_FILE!' TEST_METHOD(15) { set_test_name("If user is set and user is not root," " and 'group' is set to '!STARTUP_FILE!'," " then it changes the group to the startup file's group"); SETUP_USER_SWITCHING_TEST( options.user = testConfig["normal_user_1"].asCString(); options.group = "!STARTUP_FILE!"; ); lchown("tmp.wsgi/passenger_wsgi.py", (uid_t) -1, gidFor(testConfig["normal_group_1"].asString())); RUN_USER_SWITCHING_TEST(); ensure_equals(lookupSystemGroupnameByGid(result.gid), testConfig["normal_group_1"].asString()); } TEST_METHOD(16) { set_test_name("If user is set and user is not root," " and 'group' is set to '!STARTUP_FILE!'," " and the startup file is a symlink," " then it uses the symlink's group, not the target's group"); SETUP_USER_SWITCHING_TEST( options.user = testConfig["normal_user_1"].asCString(); options.group = "!STARTUP_FILE!"; ); lchown("tmp.wsgi/passenger_wsgi.py", (uid_t) -1, gidFor(testConfig["normal_group_2"].asString())); chown("tmp.wsgi/passenger_wsgi.py.real", (uid_t) -1, gidFor(testConfig["normal_group_1"].asString())); RUN_USER_SWITCHING_TEST(); ensure_equals(lookupSystemGroupnameByGid(result.gid), testConfig["normal_group_2"].asString()); } TEST_METHOD(17) { set_test_name("If user is set and user is not root," " and 'group' is not given," " then it changes the group to the user's primary group"); SETUP_USER_SWITCHING_TEST( options.user = testConfig["normal_user_1"].asCString(); ); RUN_USER_SWITCHING_TEST(); ensure_equals(lookupSystemGroupnameByGid(result.gid), getPrimaryGroupName(testConfig["normal_user_1"].asString())); } // and the given username does not exist TEST_METHOD(20) { set_test_name("If user is set and the given username does not exist," " then it changes the user to the value of defaultUser"); // It changes the user to the value of defaultUser. SETUP_USER_SWITCHING_TEST( options.user = testConfig["nonexistant_user"].asCString(); ); RUN_USER_SWITCHING_TEST(); ensure_equals(lookupSystemUsernameByUid(result.uid), testConfig["default_user"].asString()); } TEST_METHOD(21) { set_test_name("If user is set and the given username does not exist," " and 'group' is given," " then it changes group to the given group name"); // If 'group' is given, it changes group to the given group name. SETUP_USER_SWITCHING_TEST( options.user = testConfig["nonexistant_user"].asCString(); options.group = testConfig["normal_group_1"].asCString(); ); RUN_USER_SWITCHING_TEST(); ensure_equals(lookupSystemGroupnameByGid(result.gid), testConfig["normal_group_1"].asString()); } TEST_METHOD(22) { set_test_name("If user is set and the given username does not exist," " and 'group' is set to the root group," " then it changes group to defaultGroup"); string rootGroup = lookupSystemGroupnameByGid(0); SETUP_USER_SWITCHING_TEST( options.user = testConfig["nonexistant_user"].asCString(); options.group = rootGroup; ); RUN_USER_SWITCHING_TEST(); ensure_equals(lookupSystemGroupnameByGid(result.gid), testConfig["default_group"].asString()); } // and 'group' is set to '!STARTUP_FILE!' TEST_METHOD(23) { set_test_name("If user is set and the given username does not exist," " and 'group' is set to '!STARTUP_FILE!'," " then it changes the group to the startup file's group"); SETUP_USER_SWITCHING_TEST( options.user = testConfig["nonexistant_user"].asCString(); options.group = "!STARTUP_FILE!"; ); lchown("tmp.wsgi/passenger_wsgi.py", (uid_t) -1, gidFor(testConfig["normal_group_1"].asString())); RUN_USER_SWITCHING_TEST(); ensure_equals(lookupSystemGroupnameByGid(result.gid), testConfig["normal_group_1"].asString()); } TEST_METHOD(24) { set_test_name("If user is set and the given username does not exist," " and 'group' is set to '!STARTUP_FILE!'," " and the startup file is a symlink," " then it uses the symlink's group, not the target's group"); SETUP_USER_SWITCHING_TEST( options.user = testConfig["nonexistant_user"].asCString(); options.group = "!STARTUP_FILE!"; ); lchown("tmp.wsgi/passenger_wsgi.py", (uid_t) -1, gidFor(testConfig["normal_group_2"].asString())); chown("tmp.wsgi/passenger_wsgi.py.real", (uid_t) -1, gidFor(testConfig["normal_group_1"].asString())); RUN_USER_SWITCHING_TEST(); ensure_equals(lookupSystemGroupnameByGid(result.gid), testConfig["normal_group_2"].asString()); } TEST_METHOD(25) { set_test_name("If user is set and the given username does not exist," " and 'group' is not given," " then it changes the group to defaultUser's primary group"); SETUP_USER_SWITCHING_TEST( options.user = testConfig["nonexistant_user"].asCString(); ); RUN_USER_SWITCHING_TEST(); ensure_equals(lookupSystemGroupnameByGid(result.gid), getPrimaryGroupName(testConfig["default_user"].asString())); } // If 'user' is not set // and the startup file's owner exists TEST_METHOD(30) { set_test_name("If user is not set," " and the startup file's owner exists," " it changes the user to the owner of the startup file"); SETUP_USER_SWITCHING_TEST( (void) 0; ); lchown("tmp.wsgi/passenger_wsgi.py", uidFor(testConfig["normal_user_1"].asString()), (gid_t) -1); RUN_USER_SWITCHING_TEST(); ensure_equals(lookupSystemUsernameByUid(result.uid), testConfig["normal_user_1"].asString()); } TEST_METHOD(31) { set_test_name("If user is not set," " and the startup file's owner exists," " and the startup file is a symlink," " it uses the symlink's owner, not the target's owner"); SETUP_USER_SWITCHING_TEST( (void) 0; ); lchown("tmp.wsgi/passenger_wsgi.py", uidFor(testConfig["normal_user_2"].asString()), (gid_t) -1); chown("tmp.wsgi/passenger_wsgi.py.real", uidFor(testConfig["normal_user_1"].asString()), (gid_t) -1); RUN_USER_SWITCHING_TEST(); ensure_equals(lookupSystemUsernameByUid(result.uid), testConfig["normal_user_2"].asString()); } TEST_METHOD(32) { set_test_name("If user is not set," " and the startup file's owner exists," " and 'group' is given," " then it changes group to the given group name"); SETUP_USER_SWITCHING_TEST( options.group = testConfig["normal_group_1"].asCString(); ); lchown("tmp.wsgi/passenger_wsgi.py", uidFor(testConfig["normal_user_1"].asString()), (gid_t) -1); RUN_USER_SWITCHING_TEST(); ensure_equals(lookupSystemGroupnameByGid(result.gid), testConfig["normal_group_1"].asString()); } TEST_METHOD(33) { set_test_name("If user is not set," " and the startup file's owner exists," " and 'group' is set to the root group," " then it changes group to defaultGroup"); string rootGroup = lookupSystemGroupnameByGid(0); SETUP_USER_SWITCHING_TEST( options.group = rootGroup; ); lchown("tmp.wsgi/passenger_wsgi.py", uidFor(testConfig["normal_user_1"].asString()), (gid_t) -1); RUN_USER_SWITCHING_TEST(); ensure_equals(lookupSystemGroupnameByGid(result.gid), testConfig["default_group"].asString()); } // and 'group' is set to '!STARTUP_FILE!' TEST_METHOD(34) { set_test_name("If user is not set," " and the startup file's owner exists," " and 'group' is set to '!STARTUP_FILE!'," " then it changes the group to the startup file's group"); SETUP_USER_SWITCHING_TEST( options.group = "!STARTUP_FILE!"; ); lchown("tmp.wsgi/passenger_wsgi.py", (uid_t) -1, gidFor(testConfig["normal_group_1"].asString())); RUN_USER_SWITCHING_TEST(); ensure_equals(lookupSystemGroupnameByGid(result.gid), testConfig["normal_group_1"].asString()); } TEST_METHOD(35) { set_test_name("If user is not set," " and the startup file's owner exists," " and 'group' is set to '!STARTUP_FILE!'," " and the startup file is a symlink," " then it uses the symlink's group, not the target's group"); SETUP_USER_SWITCHING_TEST( options.group = "!STARTUP_FILE!"; ); lchown("tmp.wsgi/passenger_wsgi.py", (uid_t) -1, gidFor(testConfig["normal_group_2"].asString())); chown("tmp.wsgi/passenger_wsgi.py.real", (uid_t) -1, gidFor(testConfig["normal_group_1"].asString())); RUN_USER_SWITCHING_TEST(); ensure_equals(lookupSystemGroupnameByGid(result.gid), testConfig["normal_group_2"].asString()); } TEST_METHOD(36) { set_test_name("If user is not set," " and the startup file's owner exists," " and 'group' is not given," " then it changes the group to the startup file's owner's primary group"); SETUP_USER_SWITCHING_TEST( (void) 0; ); lchown("tmp.wsgi/passenger_wsgi.py", uidFor(testConfig["normal_user_1"].asString()), (gid_t) -1); RUN_USER_SWITCHING_TEST(); ensure_equals(lookupSystemGroupnameByGid(result.gid), getPrimaryGroupName(testConfig["normal_user_1"].asString())); } // and the startup file's owner doesn't exist TEST_METHOD(40) { set_test_name("If user is not set," " and the startup file's owner doesn't exist," " then it changes the user to the value of defaultUser"); SETUP_USER_SWITCHING_TEST( (void) 0; ); lchown("tmp.wsgi/passenger_wsgi.py", (uid_t) testConfig["nonexistant_uid"].asInt64(), (gid_t) -1); RUN_USER_SWITCHING_TEST(); ensure_equals(lookupSystemUsernameByUid(result.uid), testConfig["default_user"].asString()); } TEST_METHOD(41) { set_test_name("If user is not set," " and the startup file's owner doesn't exist," " and 'group' is given," " then it changes group to the given group name"); SETUP_USER_SWITCHING_TEST( options.group = testConfig["normal_group_1"].asCString(); ); lchown("tmp.wsgi/passenger_wsgi.py", (uid_t) testConfig["nonexistant_uid"].asInt64(), (gid_t) -1); RUN_USER_SWITCHING_TEST(); ensure_equals(lookupSystemGroupnameByGid(result.gid), testConfig["normal_group_1"].asString()); } TEST_METHOD(42) { set_test_name("If user is not set," " and the startup file's owner doesn't exist," " and 'group' is set to the root group," " then it changes group to defaultGroup"); string rootGroup = lookupSystemGroupnameByGid(0); SETUP_USER_SWITCHING_TEST( options.group = rootGroup; ); lchown("tmp.wsgi/passenger_wsgi.py", (uid_t) testConfig["nonexistant_uid"].asInt64(), (gid_t) -1); RUN_USER_SWITCHING_TEST(); ensure_equals(lookupSystemGroupnameByGid(result.gid), testConfig["default_group"].asString()); } // and 'group' is set to '!STARTUP_FILE!' // and the startup file's group doesn't exist TEST_METHOD(43) { set_test_name("If user is not set," " and the startup file's owner doesn't exist," " and 'group' is set to '!STARTUP_FILE!'," " and the startup file's group doesn't exist," " then it changes the group to the value given by defaultGroup"); SETUP_USER_SWITCHING_TEST( options.group = "!STARTUP_FILE!"; ); lchown("tmp.wsgi/passenger_wsgi.py", (uid_t) testConfig["nonexistant_uid"].asInt64(), (gid_t) testConfig["nonexistant_gid"].asInt64()); RUN_USER_SWITCHING_TEST(); ensure_equals(lookupSystemGroupnameByGid(result.gid), testConfig["default_group"].asString()); } // and the startup file's group exists TEST_METHOD(44) { set_test_name("If user is not set," " and the startup file's owner doesn't exist," " and 'group' is set to '!STARTUP_FILE!'," " and the startup file's group exists," " then it changes the group to the startup file's group"); SETUP_USER_SWITCHING_TEST( options.group = "!STARTUP_FILE!"; ); lchown("tmp.wsgi/passenger_wsgi.py", (uid_t) testConfig["nonexistant_uid"].asInt64(), gidFor(testConfig["normal_group_1"].asString())); RUN_USER_SWITCHING_TEST(); ensure_equals(lookupSystemGroupnameByGid(result.gid), testConfig["normal_group_1"].asString()); } TEST_METHOD(45) { set_test_name("If user is not set," " and the startup file's owner doesn't exist," " and 'group' is set to '!STARTUP_FILE!'," " and the startup file's group exists," " and the startup file is a symlink," " then it uses the symlink's group, not the target's group"); SETUP_USER_SWITCHING_TEST( options.group = "!STARTUP_FILE!"; ); lchown("tmp.wsgi/passenger_wsgi.py", (uid_t) testConfig["nonexistant_uid"].asInt64(), gidFor(testConfig["normal_group_2"].asString())); chown("tmp.wsgi/passenger_wsgi.py.real", (uid_t) -1, gidFor(testConfig["normal_group_1"].asString())); RUN_USER_SWITCHING_TEST(); ensure_equals(lookupSystemGroupnameByGid(result.gid), testConfig["normal_group_2"].asString()); } TEST_METHOD(46) { set_test_name("If user is not set," " and the startup file's owner doesn't exist," " and 'group' is not given," " then it changes the group to defaultUser's primary group"); SETUP_USER_SWITCHING_TEST( (void) 0; ); lchown("tmp.wsgi/passenger_wsgi.py", (uid_t) testConfig["nonexistant_uid"].asInt64(), (gid_t) -1); RUN_USER_SWITCHING_TEST(); ensure_equals(lookupSystemGroupnameByGid(result.gid), getPrimaryGroupName(testConfig["default_user"].asString())); } TEST_METHOD(50) { set_test_name("It raises an error if it tries to lower to 'defaultUser'," " but that user doesn't exist"); SETUP_USER_SWITCHING_TEST( options.user = "root"; options.defaultUser = testConfig["nonexistant_user"].asCString(); ); try { RUN_USER_SWITCHING_TEST(); fail("RuntimeException expected"); } catch (const RuntimeException &e) { ensure(containsSubstring(e.what(), "Cannot determine a user to lower privilege to")); } } TEST_METHOD(51) { set_test_name("It raises an error if it tries to lower to 'default_group'," " but that group doesn't exist"); string rootGroup = lookupSystemGroupnameByGid(0); SETUP_USER_SWITCHING_TEST( options.user = testConfig["normal_user_1"].asCString(); options.group = rootGroup; options.defaultGroup = testConfig["nonexistant_group"].asCString(); ); try { RUN_USER_SWITCHING_TEST(); fail("RuntimeException expected"); } catch (const RuntimeException &e) { ensure(containsSubstring(e.what(), "Cannot determine a group to lower privilege to")); } } } SpawningKit/DirectSpawnerTest.cpp 0000644 00000005577 14756504011 0013141 0 ustar 00 #include <TestSupport.h> #include <jsoncpp/json.h> #include <Core/ApplicationPool/Options.h> #include <Core/SpawningKit/DirectSpawner.h> #include <LoggingKit/Context.h> #include <FileDescriptor.h> #include <IOTools/IOUtils.h> #include <algorithm> #include <fcntl.h> using namespace Passenger; using namespace Passenger::SpawningKit; namespace tut { struct Core_SpawningKit_DirectSpawnerTest: public TestBase { WrapperRegistry::Registry wrapperRegistry; SpawningKit::Context::Schema schema; SpawningKit::Context context; SpawningKit::Result result; Core_SpawningKit_DirectSpawnerTest() : context(schema) { wrapperRegistry.finalize(); context.resourceLocator = resourceLocator; context.wrapperRegistry = &wrapperRegistry; context.integrationMode = "standalone"; context.spawnDir = getSystemTempDir(); context.finalize(); Json::Value config; vector<ConfigKit::Error> errors; LoggingKit::ConfigChangeRequest req; config["level"] = "warn"; config["app_output_log_level"] = "debug"; if (LoggingKit::context->prepareConfigChange(config, errors, req)) { LoggingKit::context->commitConfigChange(req); } else { P_BUG("Error configuring LoggingKit: " << ConfigKit::toString(errors)); } } ~Core_SpawningKit_DirectSpawnerTest() { Json::Value config; vector<ConfigKit::Error> errors; LoggingKit::ConfigChangeRequest req; config["level"] = DEFAULT_LOG_LEVEL_NAME; config["app_output_log_level"] = DEFAULT_APP_OUTPUT_LOG_LEVEL_NAME; if (LoggingKit::context->prepareConfigChange(config, errors, req)) { LoggingKit::context->commitConfigChange(req); } else { P_BUG("Error configuring LoggingKit: " << ConfigKit::toString(errors)); } unlink("stub/wsgi/passenger_wsgi.pyc"); } boost::shared_ptr<DirectSpawner> createSpawner(const SpawningKit::AppPoolOptions &options) { return boost::make_shared<DirectSpawner>(&context); } SpawningKit::AppPoolOptions createOptions() { SpawningKit::AppPoolOptions options; options.appType = "directly-through-start-command"; options.spawnMethod = "direct"; options.loadShellEnvvars = false; return options; } }; DEFINE_TEST_GROUP(Core_SpawningKit_DirectSpawnerTest); #include "SpawnerTestCases.cpp" TEST_METHOD(10) { set_test_name("Test that everything works correctly if the app re-execs() itself"); // https://code.google.com/p/phusion-passenger/issues/detail?id=842#c19 SpawningKit::AppPoolOptions options = createOptions(); options.appRoot = "stub/rack"; options.appStartCommand = "ruby start.rb --execself"; options.startupFile = "start.rb"; SpawnerPtr spawner = createSpawner(options); result = spawner->spawn(options); ensure_equals(result.sockets.size(), 1u); FileDescriptor fd(connectToServer(result.sockets[0].address, __FILE__, __LINE__), NULL, 0); writeExact(fd, "ping\n"); ensure_equals(readAll(fd, 1024).first, "pong\n"); } } SpawningKit/ConfigTest.cpp 0000644 00000003120 14756504011 0011552 0 ustar 00 #include <TestSupport.h> #include <Core/SpawningKit/Config.h> #include <cstdlib> #include <cstring> using namespace std; using namespace Passenger; using namespace Passenger::SpawningKit; namespace tut { struct Core_SpawningKit_ConfigTest: public TestBase { SpawningKit::Config config; }; DEFINE_TEST_GROUP(Core_SpawningKit_ConfigTest); TEST_METHOD(1) { set_test_name("internStrings() internalizes all strings into the object"); char *str1 = (char *) malloc(32); strncpy(str1, "hello", 32); config.appType = str1; char *str2 = (char *) malloc(32); strncpy(str2, "world", 32); config.appRoot = str2; config.internStrings(); strncpy(str1, "olleh", 32); strncpy(str2, "dlrow", 32); free(str1); free(str2); ensure_equals(config.appType, P_STATIC_STRING("hello")); ensure_equals(config.appRoot, P_STATIC_STRING("world")); } TEST_METHOD(2) { set_test_name("internStrings() works when called twice"); config.appType = "hello"; config.appRoot = "world"; config.internStrings(); config.internStrings(); ensure_equals(config.appType, P_STATIC_STRING("hello")); ensure_equals(config.appRoot, P_STATIC_STRING("world")); } TEST_METHOD(3) { set_test_name("validate() works"); vector<StaticString> errors; unsigned int nErrors; ensure("Validation fails", !config.validate(errors)); ensure("There are errors", !errors.empty()); nErrors = errors.size(); config.appRoot = "/foo"; errors.clear(); ensure("Validation fails again", !config.validate(errors)); ensure_equals("There are fewer errors", (unsigned int) errors.size(), nErrors - 1); } } ResponseCacheTest.cpp 0000644 00000045545 14756504011 0010652 0 ustar 00 #include <TestSupport.h> #include <time.h> #include <ServerKit/HttpRequest.h> #include <MemoryKit/palloc.h> #include <Core/Controller/Request.h> #include <Core/Controller/AppResponse.h> #include <Core/ResponseCache.h> using namespace Passenger; using namespace Passenger::Core; using namespace Passenger::ServerKit; using namespace std; namespace tut { typedef ResponseCache<Request> ResponseCacheType; struct Core_ResponseCacheTest: public TestBase { ResponseCacheType responseCache; Request req; Core::ControllerSchema schema; ConfigKit::Store config; Core_ResponseCacheTest() : config(schema) { req.pool = psg_create_pool(PSG_DEFAULT_POOL_SIZE); config["multi_app"] = false; config["default_server_name"] = "localhost"; config["default_server_port"] = "80"; reset(); } ~Core_ResponseCacheTest() { psg_destroy_pool(req.pool); } void reset() { req.config.reset(new ControllerRequestConfig(config)); req.headers.clear(); req.secureHeaders.clear(); req.httpMajor = 1; req.httpMinor = 0; req.httpState = Request::COMPLETE; req.bodyType = Request::RBT_NO_BODY; req.method = HTTP_GET; req.wantKeepAlive = false; req.detectingNextRequestEarlyReadError = false; req.responseBegun = false; req.client = NULL; req.hooks.impl = NULL; req.hooks.userData = NULL; psg_lstr_init(&req.path); psg_lstr_append(&req.path, req.pool, "/"); req.bodyAlreadyRead = 0; req.lastDataReceiveTime = 0; req.lastDataSendTime = 0; req.queryStringIndex = -1; req.bodyError = 0; req.nextRequestEarlyReadError = 0; req.startedAt = 0; req.state = Request::ANALYZING_REQUEST; req.dechunkResponse = false; req.requestBodyBuffering = false; req.https = false; req.stickySession = false; req.sessionCheckoutTry = 0; req.halfClosePolicy = Request::HALF_CLOSE_POLICY_UNINITIALIZED; req.appResponseInitialized = false; req.strip100ContinueHeader = false; req.hasPragmaHeader = false; req.host = createHostString(); req.bodyBytesBuffered = 0; req.cacheKey = HashedStaticString(); req.cacheControl = NULL; req.varyCookie = NULL; req.envvars = NULL; req.appResponse.headers.clear(); req.appResponse.secureHeaders.clear(); req.appResponse.httpMajor = 1; req.appResponse.httpMinor = 1; req.appResponse.httpState = AppResponse::COMPLETE; req.appResponse.wantKeepAlive = false; req.appResponse.oneHundredContinueSent = false; req.appResponse.bodyType = AppResponse::RBT_NO_BODY; req.appResponse.statusCode = 200; req.appResponse.bodyAlreadyRead = 0; req.appResponse.date = NULL; req.appResponse.setCookie = NULL; req.appResponse.cacheControl = NULL; req.appResponse.expiresHeader = NULL; req.appResponse.lastModifiedHeader = NULL; req.appResponse.headerCacheBuffers = NULL; req.appResponse.nHeaderCacheBuffers = 0; psg_lstr_init(&req.appResponse.bodyCacheBuffer); insertAppResponseHeader(createHeader( "date", createTodayString(req.pool)), req.pool); } LString *createHostString() { LString *str = (LString *) psg_palloc(req.pool, sizeof(LString)); psg_lstr_init(str); psg_lstr_append(str, req.pool, "foo.com"); return str; } StaticString createTodayString(psg_pool_t *pool) { time_t the_time = time(NULL); struct tm the_tm; gmtime_r(&the_time, &the_tm); char *buf = (char *) psg_pnalloc(pool, 64); size_t size = strftime(buf, 64, "%a, %d %b %Y %H:%M:%S GMT", &the_tm); return StaticString(buf, size); } Header *createHeader(const HashedStaticString &key, const StaticString &val) { Header *header = (Header *) psg_palloc(req.pool, sizeof(Header)); psg_lstr_init(&header->key); psg_lstr_init(&header->origKey); psg_lstr_init(&header->val); psg_lstr_append(&header->key, req.pool, key.data(), key.size()); psg_lstr_append(&header->origKey, req.pool, key.data(), key.size()); psg_lstr_append(&header->val, req.pool, val.data(), val.size()); header->hash = key.hash(); return header; } void insertReqHeader(Header *header, psg_pool_t *pool) { req.headers.insert(&header, pool); } void insertAppResponseHeader(Header *header, psg_pool_t *pool) { req.appResponse.headers.insert(&header, pool); } void initCacheableResponse() { insertAppResponseHeader(createHeader( "cache-control", "public,max-age=99999"), req.pool); } void initUncacheableResponse() { insertAppResponseHeader(createHeader( "cache-control", "private"), req.pool); } void initResponseBody(const string &body) { req.appResponse.bodyType = AppResponse::RBT_CONTENT_LENGTH; req.appResponse.aux.bodyInfo.contentLength = body.size(); } }; DEFINE_TEST_GROUP_WITH_LIMIT(Core_ResponseCacheTest, 100); /***** Preparation *****/ TEST_METHOD(1) { set_test_name("It works on a GET request with no body"); ensure(responseCache.prepareRequest(this, &req)); } TEST_METHOD(2) { set_test_name("It fails on upgraded requests"); req.bodyType = Request::RBT_UPGRADE; ensure(!responseCache.prepareRequest(this, &req)); ensure_equals(req.cacheKey.size(), 0u); } TEST_METHOD(3) { set_test_name("It fails on requests without a host name"); req.host = NULL; ensure(!responseCache.prepareRequest(this, &req)); ensure_equals(req.cacheKey.size(), 0u); } TEST_METHOD(4) { set_test_name("It fails if the path is too long"); psg_lstr_append(&req.path, req.pool, "fooooooooooooooooooooooooooo" "ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo" "ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo" "ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo" "ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo" "ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo" "ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo" "ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo"); ensure(!responseCache.prepareRequest(this, &req)); ensure_equals(req.cacheKey.size(), 0u); } TEST_METHOD(7) { set_test_name("It generates a cache key on success"); ensure(responseCache.prepareRequest(this, &req)); ensure(req.cacheKey.size() > 0); } /***** Storing and fetching *****/ TEST_METHOD(10) { set_test_name("Storing and fetching works"); string responseHeadersStr = "content-length: 5\r\n" "cache-control: public,max-age=99999\r\n"; string responseBodyStr = "hello"; initCacheableResponse(); initResponseBody(responseBodyStr); ensure("(1)", responseCache.prepareRequest(this, &req)); ensure("(2)", responseCache.requestAllowsStoring(&req)); ensure("(3)", responseCache.prepareRequestForStoring(&req)); ResponseCacheType::Entry entry(responseCache.store(&req, time(NULL), responseHeadersStr.size(), responseBodyStr.size())); ensure("(5)", entry.valid()); ensure_equals("(6)", entry.index, 0u); reset(); ensure("(10)", responseCache.prepareRequest(this, &req)); ensure("(11)", responseCache.requestAllowsFetching(&req)); ResponseCacheType::Entry entry2(responseCache.fetch(&req, time(NULL))); ensure("(12)", entry2.valid()); ensure_equals("(13)", entry2.index, 0u); ensure_equals<int>("(14)", entry2.body->httpHeaderSize, responseHeadersStr.size()); ensure_equals<int>("(15)", entry2.body->httpBodySize, responseBodyStr.size()); } TEST_METHOD(11) { set_test_name("Fetching fails if there is no entry with the given cache"); ensure("(1)", responseCache.prepareRequest(this, &req)); ensure("(2)", responseCache.requestAllowsFetching(&req)); ResponseCacheType::Entry entry2(responseCache.fetch(&req, time(NULL))); ensure("(3)", !entry2.valid()); } /***** Checking whether request should be fetched from cache *****/ TEST_METHOD(15) { set_test_name("It succeeds on GET requests"); ensure(responseCache.prepareRequest(this, &req)); ensure(responseCache.requestAllowsFetching(&req)); } TEST_METHOD(16) { set_test_name("It succeeds on HEAD requests"); req.method = HTTP_HEAD; ensure(responseCache.prepareRequest(this, &req)); ensure(responseCache.requestAllowsFetching(&req)); } TEST_METHOD(17) { set_test_name("It fails on POST requests"); req.method = HTTP_POST; ensure("(1)", responseCache.prepareRequest(this, &req)); ensure("(2)", !responseCache.requestAllowsFetching(&req)); } TEST_METHOD(18) { set_test_name("It fails on non-GET and non-HEAD requests"); req.method = HTTP_OPTIONS; ensure("(1)", responseCache.prepareRequest(this, &req)); ensure("(2)", !responseCache.requestAllowsFetching(&req)); } TEST_METHOD(19) { set_test_name("It fails if the request has a Cache-Control header"); insertReqHeader(createHeader( "cache-control", "xyz"), req.pool); ensure("(1)", responseCache.prepareRequest(this, &req)); ensure("(2)", !responseCache.requestAllowsFetching(&req)); } TEST_METHOD(20) { set_test_name("It fails if the request has a Pragma header"); insertReqHeader(createHeader( "pragma", "xyz"), req.pool); ensure("(1)", responseCache.prepareRequest(this, &req)); ensure("(2)", !responseCache.requestAllowsFetching(&req)); } /***** Checking whether response should be stored to cache *****/ TEST_METHOD(30) { set_test_name("It fails on HEAD requests"); initCacheableResponse(); req.method = HTTP_HEAD; ensure("(1)", responseCache.prepareRequest(this, &req)); ensure("(2)", !responseCache.requestAllowsStoring(&req)); } TEST_METHOD(31) { set_test_name("It fails on all non-GET requests"); initCacheableResponse(); req.method = HTTP_POST; ensure("(1)", responseCache.prepareRequest(this, &req)); ensure("(2)", !responseCache.requestAllowsStoring(&req)); } TEST_METHOD(32) { set_test_name("It fails if the request's Cache-Control header contains no-store"); initCacheableResponse(); insertReqHeader(createHeader( "cache-control", "no-store"), req.pool); ensure("(1)", responseCache.prepareRequest(this, &req)); ensure("(2)", !responseCache.requestAllowsStoring(&req)); } TEST_METHOD(33) { set_test_name("It fails if the request's Cache-Control header contains no-cache"); initCacheableResponse(); insertReqHeader(createHeader( "cache-control", "no-cache"), req.pool); ensure("(1)", responseCache.prepareRequest(this, &req)); ensure("(2)", !responseCache.requestAllowsStoring(&req)); } TEST_METHOD(34) { set_test_name("It fails if the request is not default cacheable"); initCacheableResponse(); req.appResponse.statusCode = 205; ensure("(1)", responseCache.prepareRequest(this, &req)); ensure("(2)", responseCache.requestAllowsStoring(&req)); ensure("(3)", !responseCache.prepareRequestForStoring(&req)); } TEST_METHOD(35) { set_test_name("It fails if the request is default cacheable, but the response has " "no Cache-Control and no Expires header that allow caching"); ensure_equals(req.appResponse.statusCode, 200); ensure("(1)", responseCache.prepareRequest(this, &req)); ensure("(2)", responseCache.requestAllowsStoring(&req)); ensure("(3)", !responseCache.prepareRequestForStoring(&req)); } TEST_METHOD(36) { set_test_name("It succeeds if the response contains a Cache-Control header with public directive"); insertAppResponseHeader(createHeader( "cache-control", "public"), req.pool); ensure("(1)", responseCache.prepareRequest(this, &req)); ensure("(2)", responseCache.requestAllowsStoring(&req)); ensure("(3)", responseCache.prepareRequestForStoring(&req)); } TEST_METHOD(37) { set_test_name("It succeeds if the response contains a Cache-Control header with max-age directive"); insertAppResponseHeader(createHeader( "cache-control", "max-age=999"), req.pool); ensure("(1)", responseCache.prepareRequest(this, &req)); ensure("(2)", responseCache.requestAllowsStoring(&req)); ensure("(3)", responseCache.prepareRequestForStoring(&req)); } TEST_METHOD(38) { set_test_name("It succeeds if the response contains an Expires header"); insertAppResponseHeader(createHeader( "expires", "Tue, 01 Jan 2030 00:00:00 GMT"), req.pool); ensure("(1)", responseCache.prepareRequest(this, &req)); ensure("(2)", responseCache.requestAllowsStoring(&req)); ensure("(3)", responseCache.prepareRequestForStoring(&req)); } TEST_METHOD(39) { set_test_name("It fails if the response's Cache-Control header contains no-store"); initCacheableResponse(); insertAppResponseHeader(createHeader( "cache-control", "no-store"), req.pool); ensure("(1)", responseCache.prepareRequest(this, &req)); ensure("(2)", responseCache.requestAllowsStoring(&req)); ensure("(3)", !responseCache.prepareRequestForStoring(&req)); } TEST_METHOD(45) { set_test_name("It fails if the response's Cache-Control header contains private"); initCacheableResponse(); insertAppResponseHeader(createHeader( "cache-control", "private"), req.pool); ensure("(1)", responseCache.prepareRequest(this, &req)); ensure("(2)", responseCache.requestAllowsStoring(&req)); ensure("(3)", !responseCache.prepareRequestForStoring(&req)); } TEST_METHOD(46) { set_test_name("It fails if the response's Cache-Control header contains no-cache"); initCacheableResponse(); insertAppResponseHeader(createHeader( "cache-control", "no-cache"), req.pool); ensure("(1)", responseCache.prepareRequest(this, &req)); ensure("(2)", responseCache.requestAllowsStoring(&req)); ensure("(3)", !responseCache.prepareRequestForStoring(&req)); } TEST_METHOD(47) { set_test_name("It fails if the request has an Authorization header"); initCacheableResponse(); insertReqHeader(createHeader( "authorization", "foo"), req.pool); ensure("(1)", responseCache.prepareRequest(this, &req)); ensure("(2)", responseCache.requestAllowsStoring(&req)); ensure("(3)", !responseCache.prepareRequestForStoring(&req)); } TEST_METHOD(48) { set_test_name("It fails if the response has a Vary header"); initCacheableResponse(); insertAppResponseHeader(createHeader( "vary", "foo"), req.pool); ensure("(1)", responseCache.prepareRequest(this, &req)); ensure("(2)", responseCache.requestAllowsStoring(&req)); ensure("(3)", !responseCache.prepareRequestForStoring(&req)); } TEST_METHOD(49) { set_test_name("It fails if the response has a WWW-Authenticate header"); initCacheableResponse(); insertAppResponseHeader(createHeader( "www-authenticate", "foo"), req.pool); ensure("(1)", responseCache.prepareRequest(this, &req)); ensure("(2)", responseCache.requestAllowsStoring(&req)); ensure("(3)", !responseCache.prepareRequestForStoring(&req)); } TEST_METHOD(50) { set_test_name("It fails if the response has an X-Sendfile header"); initCacheableResponse(); insertAppResponseHeader(createHeader( "x-sendfile", "foo"), req.pool); ensure("(1)", responseCache.prepareRequest(this, &req)); ensure("(2)", responseCache.requestAllowsStoring(&req)); ensure("(3)", !responseCache.prepareRequestForStoring(&req)); } TEST_METHOD(51) { set_test_name("It fails if the response has an X-Accel-Redirect header"); initCacheableResponse(); insertAppResponseHeader(createHeader( "x-accel-redirect", "foo"), req.pool); ensure("(1)", responseCache.prepareRequest(this, &req)); ensure("(2)", responseCache.requestAllowsStoring(&req)); ensure("(3)", !responseCache.prepareRequestForStoring(&req)); } /***** Invalidation *****/ TEST_METHOD(60) { set_test_name("Direct invalidation"); string responseHeadersStr = "content-length: 5\r\n" "cache-control: public,max-age=99999\r\n"; string responseBodyStr = "hello"; initCacheableResponse(); initResponseBody(responseBodyStr); ensure("(1)", responseCache.prepareRequest(this, &req)); ensure("(2)", responseCache.requestAllowsStoring(&req)); ensure("(3)", responseCache.prepareRequestForStoring(&req)); ResponseCacheType::Entry entry(responseCache.store(&req, time(NULL), responseHeadersStr.size(), responseBodyStr.size())); ensure("(5)", entry.valid()); ensure_equals("(6)", entry.index, 0u); reset(); req.method = HTTP_POST; ensure("(10)", responseCache.prepareRequest(this, &req)); ensure("(11)", !responseCache.requestAllowsStoring(&req)); ensure("(12)", responseCache.requestAllowsInvalidating(&req)); responseCache.invalidate(&req); reset(); ensure("(20)", responseCache.prepareRequest(this, &req)); ensure("(21)", responseCache.requestAllowsFetching(&req)); ResponseCacheType::Entry entry2(responseCache.fetch(&req, time(NULL))); ensure("(22)", !entry2.valid()); } TEST_METHOD(61) { set_test_name("Invalidation via Location response header"); string responseHeadersStr = "content-length: 5\r\n" "cache-control: public,max-age=99999\r\n"; string responseBodyStr = "hello"; initCacheableResponse(); initResponseBody(responseBodyStr); ensure("(1)", responseCache.prepareRequest(this, &req)); ensure("(2)", responseCache.requestAllowsStoring(&req)); ensure("(3)", responseCache.prepareRequestForStoring(&req)); ResponseCacheType::Entry entry(responseCache.store(&req, time(NULL), responseHeadersStr.size(), responseBodyStr.size())); ensure("(5)", entry.valid()); ensure_equals("(6)", entry.index, 0u); reset(); req.method = HTTP_POST; psg_lstr_init(&req.path); psg_lstr_append(&req.path, req.pool, "/foo"); insertAppResponseHeader(createHeader( "location", "/"), req.pool); ensure("(10)", responseCache.prepareRequest(this, &req)); ensure("(11)", !responseCache.requestAllowsStoring(&req)); ensure("(12)", responseCache.requestAllowsInvalidating(&req)); responseCache.invalidate(&req); reset(); ensure("(20)", responseCache.prepareRequest(this, &req)); ensure("(21)", responseCache.requestAllowsFetching(&req)); ResponseCacheType::Entry entry2(responseCache.fetch(&req, time(NULL))); ensure("(22)", !entry2.valid()); } TEST_METHOD(62) { set_test_name("Invalidation via Content-Location response header"); string responseHeadersStr = "content-length: 5\r\n" "cache-control: public,max-age=99999\r\n"; string responseBodyStr = "hello"; initCacheableResponse(); initResponseBody(responseBodyStr); ensure("(1)", responseCache.prepareRequest(this, &req)); ensure("(2)", responseCache.requestAllowsStoring(&req)); ensure("(3)", responseCache.prepareRequestForStoring(&req)); ResponseCacheType::Entry entry(responseCache.store(&req, time(NULL), responseHeadersStr.size(), responseBodyStr.size())); ensure("(5)", entry.valid()); ensure_equals("(6)", entry.index, 0u); reset(); req.method = HTTP_POST; psg_lstr_init(&req.path); psg_lstr_append(&req.path, req.pool, "/foo"); insertAppResponseHeader(createHeader( "content-location", "/"), req.pool); ensure("(10)", responseCache.prepareRequest(this, &req)); ensure("(11)", !responseCache.requestAllowsStoring(&req)); ensure("(12)", responseCache.requestAllowsInvalidating(&req)); responseCache.invalidate(&req); reset(); ensure("(20)", responseCache.prepareRequest(this, &req)); ensure("(21)", responseCache.requestAllowsFetching(&req)); ResponseCacheType::Entry entry2(responseCache.fetch(&req, time(NULL))); ensure("(22)", !entry2.valid()); } }
| ver. 1.4 |
Github
|
.
| PHP 8.0.30 | Génération de la page: 0.46 |
proxy
|
phpinfo
|
Réglages