-
-
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.