-
-
Save ClosetGeek-Git/0e830460504db8d941d956c42bf92c8c to your computer and use it in GitHub Desktop.
<?php | |
// based on Stone FastCGI https://github.com/StoneGroup/stone copyright MIT according to it's composer.json | |
class FastCGIConnection | |
{ | |
private $server; | |
private $from_id; | |
private $fd; | |
public function __construct($server, $fd, $from_id) { | |
$this->server = $server; | |
$this->fd = $fd; | |
$this->from_id = $from_id; | |
} | |
public function write($data) { | |
return $this->server->send($this->fd, $data, $this->from_id); | |
} | |
} | |
class FastCGIProtocol | |
{ | |
const FCGI_LISTENSOCK_FILENO = 0; | |
const FCGI_VERSION_1 = 1; | |
const FCGI_BEGIN_REQUEST = 1; | |
const FCGI_ABORT_REQUEST = 2; | |
const FCGI_END_REQUEST = 3; | |
const FCGI_PARAMS = 4; | |
const FCGI_STDIN = 5; | |
const FCGI_STDOUT = 6; | |
const FCGI_STDERR = 7; | |
const FCGI_DATA = 8; | |
const FCGI_GET_VALUES = 9; | |
const FCGI_RESPONDER = 1; | |
const FCGI_AUTHORIZER = 2; | |
const FCGI_FILTER = 3; | |
const FCGI_KEEP_CONNECTION = 1; | |
const FCGI_REQUEST_COMPLETE = 0; | |
const FCGI_CANT_MPX_CONN = 1; | |
const FCGI_OVERLOADED = 2; | |
const FCGI_UNKNOWN_ROLE = 3; | |
private $requests; | |
private $connection; | |
private $buffer; | |
private $bufferLength; | |
public function __construct(FastCGIConnection $connection) { | |
$this->buffer = ''; | |
$this->bufferLength = 0; | |
$this->connection = $connection; | |
} | |
public function readFromString($data) { | |
$this->buffer .= $data; | |
$this->bufferLength += strlen($data); | |
while(null !== ($record = $this->readRecord())) { | |
$this->processRecord($record); | |
} | |
return $this->requests; | |
} | |
public function readRecord() { | |
if($this->bufferLength < 8) { | |
return; | |
} | |
$headerData = substr($this->buffer, 0, 8); | |
$headerFormat = 'Cversion/Ctype/nrequestId/ncontentLength/CpaddingLength/x'; | |
$record = unpack($headerFormat, $headerData); | |
if($this->bufferLength - 8 < $record['contentLength'] + $record['paddingLength']) { | |
return; | |
} | |
$record['contentData'] = substr($this->buffer, 8, $record['contentLength']); | |
$recordSize = 8 + $record['contentLength'] + $record['paddingLength']; | |
$this->buffer = substr($this->buffer, $recordSize); | |
$this->bufferLength -= $recordSize; | |
return $record; | |
} | |
public function processRecord($record) { | |
$requestId = $record['requestId']; | |
$content = 0 === $record['contentLength'] ? null : $record['contentData']; | |
if(self::FCGI_BEGIN_REQUEST === $record['type']) { | |
$this->processBeginRequestRecord($requestId, $content); | |
}elseif(!isset($this->requests[$requestId])) { | |
throw new Exception('Invalid request id for record of type: '.$record['type']); | |
}elseif(self::FCGI_PARAMS === $record['type']) { | |
while(strlen($content) > 0) { | |
$this->readNameValuePair($requestId, $content); | |
} | |
}elseif(self::FCGI_STDIN === $record['type']) { | |
if(null !== $content) { | |
fwrite($this->requests[$requestId]['stdin'], $content); | |
$this->requests[$requestId]['rawPost'] = $content; | |
}else { | |
return 1; | |
} | |
}elseif(self::FCGI_ABORT_REQUEST === $record['type']) { | |
$this->endRequest($requestId); | |
}else { | |
throw new Exception('Unexpected packet of type: '.$record['type']); | |
} | |
return 0; | |
} | |
private function processBeginRequestRecord($requestId, $contentData) { | |
if(isset($this->requests[$requestId])) { | |
throw new Exception('Unexpected FCGI_BEGIN_REQUEST record'); | |
} | |
$contentFormat = 'nrole/Cflags/x5'; | |
$content = unpack($contentFormat, $contentData); | |
$keepAlive = self::FCGI_KEEP_CONNECTION & $content['flags']; | |
$this->requests[$requestId] = [ | |
'keepAlive' => $keepAlive, | |
'stdin' => fopen('php://temp', 'r+'), | |
'params' => [], | |
]; | |
if(self::FCGI_RESPONDER !== $content['role']) { | |
$this->endRequest($requestId, 0, self::FCGI_UNKNOWN_ROLE); | |
return; | |
} | |
} | |
private function readNameValuePair($requestId, &$buffer) { | |
$nameLength = $this->readFieldLength($buffer); | |
$valueLength = $this->readFieldLength($buffer); | |
$contentFormat = ( | |
'a'.$nameLength.'name/'. | |
'a'.$valueLength.'value/' | |
); | |
$content = unpack($contentFormat, $buffer); | |
$this->requests[$requestId]['params'][$content['name']] = $content['value']; | |
$buffer = substr($buffer, $nameLength + $valueLength); | |
} | |
private function readFieldLength(&$buffer) { | |
$block = unpack('C4', $buffer); | |
$length = $block[1]; | |
$skip = 1; | |
if($length & 0x80) { | |
$fullBlock = unpack('N', $buffer); | |
$length = $fullBlock[1] & 0x7FFFFFFF; | |
$skip = 4; | |
} | |
$buffer = substr($buffer, $skip); | |
return $length; | |
} | |
private function beginRequest($requestId, $appStatus = 0, $protocolStatus = self::FCGI_BEGIN_REQUEST) { | |
$c = pack('NC', $appStatus, $protocolStatus) | |
. "\x00\x00\x00"; | |
return $this->connection->write( | |
"\x01" | |
. "\x01" | |
. pack('nn', $req->id, strlen($c)) | |
. "\x00" | |
. "\x00" | |
. $c | |
); | |
$content = pack('NCx3', $appStatus, $protocolStatus); | |
$this->writeRecord($requestId, self::FCGI_END_REQUEST, $content); | |
$keepAlive = $this->requests[$requestId]['keepAlive']; | |
unset($this->requests[$requestId]); | |
} | |
private function endRequest($requestId, $appStatus = 0, $protocolStatus = self::FCGI_REQUEST_COMPLETE) { | |
$content = pack('NCx3', $appStatus, $protocolStatus); | |
$this->writeRecord($requestId, self::FCGI_END_REQUEST, $content); | |
$keepAlive = $this->requests[$requestId]['keepAlive']; | |
unset($this->requests[$requestId]); | |
} | |
private function writeRecord($requestId, $type, $content = null) { | |
$contentLength = null === $content ? 0 : strlen($content); | |
$headerData = pack('CCnnxx', self::FCGI_VERSION_1, $type, $requestId, $contentLength); | |
$this->connection->write($headerData); | |
if(null !== $content) { | |
$this->connection->write($content); | |
} | |
} | |
public function sendDataToClient($requestId, $data, $header = []) { | |
$dataLength = strlen($data); | |
if($dataLength <= 65535) { | |
$this->writeRecord($requestId, self::FCGI_STDOUT, $data); | |
}else { | |
$start = 0; | |
$chunkSize = 8092; | |
do { | |
$this->writeRecord($requestId, self::FCGI_STDOUT, substr($data, $start, $chunkSize)); | |
$start += $chunkSize; | |
}while($start < $dataLength); | |
$this->writeRecord($requestId, self::FCGI_STDOUT); | |
} | |
$this->endRequest($requestId); | |
} | |
} | |
$serv = new Swoole\Server("/tmp/fcgi.sock", 0, SWOOLE_PROCESS, SWOOLE_UNIX_STREAM); | |
$serv->set(array( | |
'worker_num' => 1, | |
)); | |
$serv->on('receive', function (Swoole\Server $serv, $fd, $reactor_id, $data) { | |
$fastCGI = new FastCGIProtocol(new FastCGIConnection($serv, $fd, $reactor_id)); | |
$requestData = $fastCGI->readFromString($data); | |
var_dump($requestData); | |
}); | |
$serv->start(); |
Specific functions to start at:
Stone readRecord:
https://github.com/StoneGroup/stone/blob/97b209198fd8e4f98b8d87032f005824d4114164/src/Stone/FastCGI/Protocol.php#L76
Adoy readPacket:
https://github.com/adoy/PHP-FastCGI-Client/blob/aa6611b1af00f9c5e867f6f8485b7bac071a4c1a/src/Adoy/FastCGI/Client.php#L393
Swoole uses parseFrame which is called in a loop within the Coroutine\FastCGI\Client->execute method
parseFrame: https://github.com/swoole/library/blob/1a84cb1930d88ceded926f272106c3ef66b85f1b/src/core/FastCGI/FrameParser.php#L69
execute: https://github.com/swoole/library/blob/1a84cb1930d88ceded926f272106c3ef66b85f1b/src/core/Coroutine/FastCGI/Client.php#L80-L123
Also note stdin handling
Adoy:
https://github.com/adoy/PHP-FastCGI-Client/blob/aa6611b1af00f9c5e867f6f8485b7bac071a4c1a/src/Adoy/FastCGI/Client.php#L496-L504
Swoole
https://github.com/swoole/library/blob/1a84cb1930d88ceded926f272106c3ef66b85f1b/src/core/FastCGI/Request.php#L32-L41
Swoole's example is more dynamic. By using _toString on request objects you are able to get the actual fastcgi request in one go, basically send((string) $request)
. Note Swoole's use of Record objects as wrappers. Example see FastCGI\Record\Stdin Stdin extends FastCGI\Record.
This allows packets to be built using $record->type and $record->getContent()
The best FastCGI client is probably https://github.dev/adoy/PHP-FastCGI-Client/blob/master/src/Adoy/FastCGI/Client.php . Big plus is that it doesn't use pack/unpack so it can be easily converted to C/C++. Should be able use to replace pack/unpack in stone because it is the same packet format/protocol.