Skip to content

Instantly share code, notes, and snippets.

@ClosetGeek-Git
Last active December 12, 2022 18:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ClosetGeek-Git/0e830460504db8d941d956c42bf92c8c to your computer and use it in GitHub Desktop.
Save ClosetGeek-Git/0e830460504db8d941d956c42bf92c8c to your computer and use it in GitHub Desktop.
FastCGI Server / Responder over Swoole Unix domain socket
<?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();
@ClosetGeek-Git
Copy link
Author

ClosetGeek-Git commented Dec 12, 2022

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.

@ClosetGeek-Git
Copy link
Author

@ClosetGeek-Git
Copy link
Author

ClosetGeek-Git commented Dec 12, 2022

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()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment