Skip to content

Commit

Permalink
Add exception for response packages of type STDERR, #26
Browse files Browse the repository at this point in the history
* Refers mainly to the error "Primary script unknown" resp. the response
  "File not found."
* Adds new ProjectManagerException
* Adds integration tests for network and unix domain socket
  • Loading branch information
hollodotme committed Jan 28, 2019
1 parent 777e97c commit a0dcdcd
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 22 deletions.
8 changes: 8 additions & 0 deletions src/Exceptions/ProcessManagerException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php declare(strict_types=1);

namespace hollodotme\FastCGI\Exceptions;

class ProcessManagerException extends FastCGIClientException
{

}
69 changes: 55 additions & 14 deletions src/Socket.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@

namespace hollodotme\FastCGI;

use ErrorException;
use hollodotme\FastCGI\Exceptions\ConnectException;
use hollodotme\FastCGI\Exceptions\ForbiddenException;
use hollodotme\FastCGI\Exceptions\ProcessManagerException;
use hollodotme\FastCGI\Exceptions\ReadFailedException;
use hollodotme\FastCGI\Exceptions\TimedoutException;
use hollodotme\FastCGI\Exceptions\WriteFailedException;
Expand All @@ -34,6 +36,25 @@
use hollodotme\FastCGI\Interfaces\ProvidesRequestData;
use hollodotme\FastCGI\Interfaces\ProvidesResponseData;
use hollodotme\FastCGI\Responses\Response;
use Throwable;
use function chr;
use function error_get_last;
use function fclose;
use function fflush;
use function floor;
use function fread;
use function fwrite;
use function is_resource;
use function microtime;
use function ord;
use function random_int;
use function str_repeat;
use function stream_get_meta_data;
use function stream_select;
use function stream_set_timeout;
use function stream_socket_client;
use function strlen;
use function substr;

/**
* Class Socket
Expand Down Expand Up @@ -183,7 +204,7 @@ private function connect() : void
$this->connection->getConnectTimeout() / 1000
);
}
catch ( \Throwable $e )
catch ( Throwable $e )
{
throw new ConnectException( $e->getMessage(), $e->getCode(), $e );
}
Expand Down Expand Up @@ -214,7 +235,7 @@ private function handleFailedResource( $errorNumber, $errorString ) : void

if ( null !== $lastError )
{
$lastErrorException = new \ErrorException(
$lastErrorException = new ErrorException(
$lastError['message'] ?? '[No message available]',
0,
$lastError['type'] ?? E_ERROR,
Expand Down Expand Up @@ -244,7 +265,7 @@ private function getRequestPackets( ProvidesRequestData $request ) : string
# Keep alive bit always set to 1
$requestPackets = $this->packetEncoder->encodePacket(
self::BEGIN_REQUEST,
\chr( 0 ) . \chr( self::RESPONDER ) . \chr( 1 ) . str_repeat( \chr( 0 ), 5 ),
chr( 0 ) . chr( self::RESPONDER ) . chr( 1 ) . str_repeat( chr( 0 ), 5 ),
$this->id
);

Expand All @@ -262,7 +283,15 @@ private function getRequestPackets( ProvidesRequestData $request ) : string
$offset = 0;
do
{
$requestPackets .= $this->packetEncoder->encodePacket( self::STDIN, substr( $request->getContent(), $offset, self::REQ_MAX_CONTENT_SIZE ), $this->id );
$requestPackets .= $this->packetEncoder->encodePacket(
self::STDIN,
substr(
$request->getContent(),
$offset,
self::REQ_MAX_CONTENT_SIZE
),
$this->id
);
$offset += self::REQ_MAX_CONTENT_SIZE;
}
while ( $offset < $request->getContentLength() );
Expand All @@ -284,7 +313,7 @@ private function write( string $data ) : void
$writeResult = fwrite( $this->resource, $data );
$flushResult = fflush( $this->resource );

if ( $writeResult === false || $flushResult === false )
if ( $writeResult === false || !$flushResult )
{
$info = stream_get_meta_data( $this->resource );

Expand All @@ -300,8 +329,12 @@ private function write( string $data ) : void
/**
* @param int|null $timeoutMs
*
* @throws ForbiddenException
* @throws ProcessManagerException
* @throws ReadFailedException
* @throws TimedoutException
* @throws WriteFailedException
* @return ProvidesResponseData
* @throws \Throwable
*/
public function fetchResponse( ?int $timeoutMs = null ) : ProvidesResponseData
{
Expand All @@ -313,17 +346,20 @@ public function fetchResponse( ?int $timeoutMs = null ) : ProvidesResponseData
// Reset timeout on socket for reading
$this->setStreamTimeout( $timeoutMs ?? $this->connection->getReadWriteTimeout() );

$errorContent = '';
$responseContent = '';

$this->status = self::REQ_STATE_OK;

do
{
$packet = $this->readPacket();

switch ( (int)$packet['type'] )
{
case self::STDERR:
$this->status = self::REQ_STATE_ERR;
$responseContent .= $packet['content'];
$this->status = self::REQ_STATE_ERR;
$errorContent .= $packet['content'];
$this->notifyPassThroughCallbacks( $packet['content'] );
break;

Expand All @@ -335,7 +371,6 @@ public function fetchResponse( ?int $timeoutMs = null ) : ProvidesResponseData
case self::END_REQUEST:
if ( $packet['requestId'] === $this->id )
{
$this->status = self::REQ_STATE_OK;
break 2;
}
break;
Expand All @@ -346,7 +381,12 @@ public function fetchResponse( ?int $timeoutMs = null ) : ProvidesResponseData
try
{
$this->handleNullPacket( $packet );
$this->guardRequestCompleted( \ord( $packet['content']{4} ) );
$this->guardRequestCompleted( ord( $packet['content']{4} ) );

if ( $this->status === self::REQ_STATE_ERR )
{
throw new ProcessManagerException( 'An error occurred: ' . $errorContent );
}

$this->response = new Response(
$this->id,
Expand All @@ -356,7 +396,7 @@ public function fetchResponse( ?int $timeoutMs = null ) : ProvidesResponseData

return $this->response;
}
catch ( \Throwable $e )
catch ( WriteFailedException | ReadFailedException $e )
{
throw $e;
}
Expand All @@ -379,13 +419,14 @@ private function readPacket() : ?array

while ( $length && ($buffer = fread( $this->resource, $length )) !== false )
{
$length -= \strlen( $buffer );
$length -= strlen( $buffer );
$packet['content'] .= $buffer;
}
}

if ( $packet['paddingLength'] )
{
/** @noinspection UnusedFunctionResultInspection */
fread( $this->resource, $packet['paddingLength'] );
}

Expand Down Expand Up @@ -459,7 +500,7 @@ private function guardRequestCompleted( int $flag ) : void

private function disconnect() : void
{
if ( \is_resource( $this->resource ) )
if ( is_resource( $this->resource ) )
{
fclose( $this->resource );
}
Expand All @@ -478,7 +519,7 @@ public function notifyResponseCallbacks( ProvidesResponseData $response ) : void
}
}

public function notifyFailureCallbacks( \Throwable $throwable ) : void
public function notifyFailureCallbacks( Throwable $throwable ) : void
{
foreach ( $this->failureCallbacks as $failureCallback )
{
Expand Down
48 changes: 43 additions & 5 deletions tests/Integration/NetworkSocketTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@
namespace hollodotme\FastCGI\Tests\Integration;

use hollodotme\FastCGI\Client;
use hollodotme\FastCGI\Exceptions\ConnectException;
use hollodotme\FastCGI\Exceptions\ProcessManagerException;
use hollodotme\FastCGI\Interfaces\ProvidesResponseData;
use hollodotme\FastCGI\Requests\GetRequest;
use hollodotme\FastCGI\Requests\PostRequest;
use hollodotme\FastCGI\SocketConnections\Defaults;
use hollodotme\FastCGI\SocketConnections\NetworkSocket;
Expand Down Expand Up @@ -255,6 +258,7 @@ public function testReadingSyncResponseCanTimeOut() : void
$content = http_build_query( ['sleep' => 2, 'test-key' => 'unit'] );
$request = new PostRequest( __DIR__ . '/Workers/sleepWorker.php', $content );

/** @noinspection UnusedFunctionResultInspection */
$client->sendRequest( $request );
}

Expand Down Expand Up @@ -290,8 +294,6 @@ function ( ProvidesResponseData $response ) use ( $unitTest )
}

/**
* @throws \Exception
* @throws \PHPUnit\Framework\Exception
* @throws \hollodotme\FastCGI\Exceptions\ConnectException
* @throws \hollodotme\FastCGI\Exceptions\TimedoutException
* @throws \hollodotme\FastCGI\Exceptions\WriteFailedException
Expand Down Expand Up @@ -365,9 +367,7 @@ public function testReadResponsesSkipsUnknownRequestIds() : void

sleep( 1 );

$responses = $client->readResponses( null, ...$requestIds );

foreach ( $responses as $response )
foreach ( $client->readResponses( null, ...$requestIds ) as $response )
{
echo $response->getBody();
}
Expand Down Expand Up @@ -482,4 +482,42 @@ public function contentLengthProvider() : array
],
];
}

/**
* @param string $scriptFilename
*
* @throws ConnectException
* @throws \PHPUnit\Framework\AssertionFailedError
* @throws \Throwable
* @throws \hollodotme\FastCGI\Exceptions\TimedoutException
* @throws \hollodotme\FastCGI\Exceptions\WriteFailedException
* @dataProvider invalidScriptFileNamesProvider
*/
public function testRequestingAnUnknownScriptPathThrowsException( string $scriptFilename ) : void
{
$connection = new NetworkSocket( '127.0.0.1', 9000 );
$client = new Client( $connection );
$request = new GetRequest( $scriptFilename, '' );

$this->expectException( ProcessManagerException::class );
$this->expectExceptionMessage( 'An error occurred: Primary script unknown' );

/** @noinspection UnusedFunctionResultInspection */
$client->sendRequest( $request );

$this->fail( 'Expecteed exception to be thrown that the primary script is unknown.' );
}

public function invalidScriptFileNamesProvider() : array
{
return [
[
'scriptFilename' => '/unknown/script.php',
],
[
# Existing script filenames containing path traversals do not work either
'scriptFilename' => __DIR__ . '/../Integration/Workers/worker.php',
],
];
}
}
45 changes: 42 additions & 3 deletions tests/Integration/UnixDomainSocketTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
namespace hollodotme\FastCGI\Tests\Integration;

use hollodotme\FastCGI\Client;
use hollodotme\FastCGI\Exceptions\ProcessManagerException;
use hollodotme\FastCGI\Interfaces\ProvidesResponseData;
use hollodotme\FastCGI\Requests\GetRequest;
use hollodotme\FastCGI\Requests\PostRequest;
use hollodotme\FastCGI\SocketConnections\Defaults;
use hollodotme\FastCGI\SocketConnections\UnixDomainSocket;
Expand Down Expand Up @@ -255,6 +257,7 @@ public function testReadingSyncResponseCanTimeOut() : void
$content = http_build_query( ['sleep' => 2, 'test-key' => 'unit'] );
$request = new PostRequest( __DIR__ . '/Workers/sleepWorker.php', $content );

/** @noinspection UnusedFunctionResultInspection */
$client->sendRequest( $request );
}

Expand Down Expand Up @@ -365,9 +368,7 @@ public function testReadResponsesSkipsUnknownRequestIds() : void

sleep( 1 );

$responses = $client->readResponses( null, ...$requestIds );

foreach ( $responses as $response )
foreach ( $client->readResponses( null, ...$requestIds ) as $response )
{
echo $response->getBody();
}
Expand Down Expand Up @@ -479,4 +480,42 @@ public function contentLengthProvider() : array
],
];
}

/**
* @param string $scriptFilename
*
* @throws \PHPUnit\Framework\AssertionFailedError
* @throws \Throwable
* @throws \hollodotme\FastCGI\Exceptions\ConnectException
* @throws \hollodotme\FastCGI\Exceptions\TimedoutException
* @throws \hollodotme\FastCGI\Exceptions\WriteFailedException
* @dataProvider invalidScriptFileNamesProvider
*/
public function testRequestingAnUnknownScriptPathThrowsException( string $scriptFilename ) : void
{
$connection = new UnixDomainSocket( '/var/run/php-uds.sock' );
$client = new Client( $connection );
$request = new GetRequest( $scriptFilename, '' );

$this->expectException( ProcessManagerException::class );
$this->expectExceptionMessage( 'An error occurred: Primary script unknown' );

/** @noinspection UnusedFunctionResultInspection */
$client->sendRequest( $request );

$this->fail( 'Expecteed exception to be thrown that the primary script is unknown.' );
}

public function invalidScriptFileNamesProvider() : array
{
return [
[
'scriptFilename' => '/unknown/script.php',
],
[
# Existing script filenames containing path traversals do not work either
'scriptFilename' => __DIR__ . '/../Integration/Workers/worker.php',
],
];
}
}

0 comments on commit a0dcdcd

Please sign in to comment.