Last active
December 12, 2022 18:49
-
-
Save ClosetGeek-Git/0e830460504db8d941d956c42bf92c8c to your computer and use it in GitHub Desktop.
FastCGI Server / Responder over Swoole Unix domain socket
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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()