├── .State ├── .gitignore ├── CHANGELOG.md ├── Connection.php ├── Documentation ├── En │ └── Index.xyl └── Fr │ └── Index.xyl ├── Exception ├── CannotMultiplex.php ├── Exception.php ├── Overloaded.php ├── UnknownRole.php └── UnknownStatus.php ├── README.md ├── Responder.php └── composer.json /.State: -------------------------------------------------------------------------------- 1 | finalized 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /composer.lock 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 3.19.03.08 2 | 3 | * feat(responder) Parse HTTP response with a new `buildResponseHeaders` method. (Taylor Otwell, 2019-03-04T12:47:24+01:00) 4 | 5 | # 3.19.02.19 6 | 7 | * fix(Exception) Add the `UnknownStatus` exception. (Ivan Enderlin, 2017-10-20T10:17:52+02:00) 8 | * fix(Responder) Use `else if` instead of two `if`. (Ivan Enderlin, 2017-10-20T09:34:07+02:00) 9 | * feat(Responder) Decorrelate error from output. (Ivan Enderlin, 2017-10-20T09:28:38+02:00) 10 | * Pass stderr as reference (Florent Viel, 2017-10-18T16:08:27+02:00) 11 | * Responder: Simplify a little bit the code. (Ivan Enderlin, 2017-06-26T14:42:54+02:00) 12 | * Support for requests larger than 65535 bytes (Armen Baghumian, 2017-06-25T07:55:39+10:00) 13 | 14 | # 3.17.01.10 15 | 16 | * Quality: Happy new year! (Alexis von Glasow, 2017-01-09T21:37:36+01:00) 17 | * Documentation: New `README.md` file. (Ivan Enderlin, 2016-10-21T07:50:40+02:00) 18 | * Documentation: Update `support` properties. (Ivan Enderlin, 2016-10-05T20:41:29+02:00) 19 | 20 | # 3.16.01.11 21 | 22 | * Quality: Drop PHP5.4. (Ivan Enderlin, 2016-01-11T09:15:26+01:00) 23 | * Quality: Run devtools:cs. (Ivan Enderlin, 2016-01-09T09:01:49+01:00) 24 | * Core: Remove `Hoa\Core`. (Ivan Enderlin, 2016-01-09T08:16:26+01:00) 25 | * Consistency: Use `Hoa\Consistency`. (Ivan Enderlin, 2015-12-08T11:12:07+01:00) 26 | * Exception: Use `Hoa\Exception`. (Ivan Enderlin, 2015-11-20T07:45:56+01:00) 27 | 28 | # 2.15.11.09 29 | 30 | * Add a `.gitignore` file. (Stéphane HULARD, 2015-08-03T11:29:59+02:00) 31 | 32 | # 2.15.05.29 33 | 34 | * Move to PSR-1 and PSR-2. (Ivan Enderlin, 2015-05-15T09:45:15+02:00) 35 | 36 | # 2.15.04.16 37 | 38 | * Reformulate a sentence in the documentation. (Ivan Enderlin, 2015-04-15T08:41:57+02:00) 39 | * Fix typos in the documentation. (Rogerio Prado de Jesus, 2015-04-14T11:06:06-03:00) 40 | 41 | # 2.15.02.19 42 | 43 | * Add the CHANGELOG.md file. (Ivan Enderlin, 2015-02-19T08:55:45+01:00) 44 | * Update links and schemas in the documentation. (Ivan Enderlin, 2015-01-23T19:25:46+01:00) 45 | * Happy new year! (Ivan Enderlin, 2015-01-05T14:29:42+01:00) 46 | 47 | # 2.14.12.10 48 | 49 | * Move to PSR-4. (Ivan Enderlin, 2014-12-09T13:47:15+01:00) 50 | 51 | # 2.14.11.26 52 | 53 | * Format the `composer.json` file. (Ivan Enderlin, 2014-11-25T14:17:51+01:00) 54 | * Require hoa/test. (Alexis von Glasow, 2014-11-25T13:50:18+01:00) 55 | 56 | # 2.14.11.15 57 | 58 | * Remove `from`/`import` and update to PHP5.4. (Ivan Enderlin, 2014-11-13T10:26:17+01:00) 59 | 60 | # 2.14.09.23 61 | 62 | * Format code. #mania (Ivan Enderlin, 2014-09-23T16:36:30+02:00) 63 | * Add `branch-alias`. (Stéphane PY, 2014-09-23T12:00:39+02:00) 64 | 65 | # 2.14.09.17 66 | 67 | * Drop PHP5.3. (Ivan Enderlin, 2014-09-17T17:49:58+02:00) 68 | * Add the installation section. (Ivan Enderlin, 2014-09-17T17:49:49+02:00) 69 | 70 | (first snapshot) 71 | -------------------------------------------------------------------------------- /Connection.php: -------------------------------------------------------------------------------- 1 | > 8) & 0xff) . // ID B1 119 | chr($id & 0xff) . // ID B0 120 | chr(($length >> 8) & 0xff) . // length B1 121 | chr($length & 0xff) . // length b0 122 | chr(0) . // padding length 123 | chr(0) . // reserved 124 | $content; 125 | } 126 | 127 | /** 128 | * Pack pairs (key/value). 129 | * 130 | * @param array $pairs Keys/values array. 131 | * @return string 132 | */ 133 | public function packPairs(array $pairs) 134 | { 135 | $out = null; 136 | 137 | foreach ($pairs as $key => $value) { 138 | foreach ([$key, $value] as $handle) { 139 | $length = strlen($handle); 140 | 141 | // B0 142 | if ($length < 0x80) { 143 | $out .= chr($length); 144 | } 145 | // B3 & B2 & B1 & B0 146 | else { 147 | $out .= 148 | chr(($length >> 24) | 0x80) . 149 | chr(($length >> 16) & 0xff) . 150 | chr(($length >> 8) & 0xff) . 151 | chr($length & 0xff); 152 | } 153 | } 154 | 155 | $out .= $key . $value; 156 | } 157 | 158 | return $out; 159 | } 160 | 161 | /** 162 | * Unpack pairs (key/value). 163 | * 164 | * @param string $pack Packet to unpack. 165 | * @return string 166 | */ 167 | public function unpackPairs($pack) 168 | { 169 | if (null === $length) { 170 | $length = strlen($pack); 171 | } 172 | 173 | $out = []; 174 | $i = 0; 175 | 176 | for ($i = 0; $length >= $i; $i += $keyLength + $valueLength) { 177 | $keyLength = ord($pack[$i++]); 178 | 179 | if ($keyLength >= 0x80) { 180 | $keyLength = 181 | ($keyLength & 0x7f << 24) 182 | | (ord($pack[$i++]) << 16) 183 | | (ord($pack[$i++]) << 8) 184 | | ord($pack[$i++]); 185 | } 186 | 187 | $valueLength = ord($pack[$i++]); 188 | 189 | if ($valueLength >= 0x80) { 190 | $valueLength = 191 | ($valueLength & 0x7f << 24) 192 | | (ord($pack[$i++]) << 16) 193 | | (ord($pack[$i++]) << 8) 194 | | ord($pack[$i++]); 195 | } 196 | 197 | $out[substr($pack, $i, $keyLength)] 198 | = substr($pack, $i + $keyLength, $valueLength); 199 | } 200 | 201 | return $out; 202 | } 203 | 204 | /** 205 | * Read a packet. 206 | * 207 | * @return array 208 | */ 209 | protected function readPack() 210 | { 211 | if ((null === $pack = $this->read(8)) || 212 | empty($pack)) { 213 | return false; 214 | } 215 | 216 | $headers = [ 217 | self::HEADER_VERSION => ord($pack[0]), 218 | self::HEADER_TYPE => ord($pack[1]), 219 | self::HEADER_REQUEST_ID => (ord($pack[2]) << 8) + 220 | ord($pack[3]), 221 | self::HEADER_CONTENT_LENGTH => (ord($pack[4]) << 8) + 222 | ord($pack[5]), 223 | self::HEADER_PADDING_LENGTH => ord($pack[6]), 224 | self::HEADER_RESERVED => ord($pack[7]), 225 | self::HEADER_CONTENT => null 226 | ]; 227 | $length = 228 | $headers[self::HEADER_CONTENT_LENGTH] + 229 | $headers[self::HEADER_PADDING_LENGTH]; 230 | 231 | if (0 === $length) { 232 | return $headers; 233 | } 234 | 235 | $headers[self::HEADER_CONTENT] = substr( 236 | $this->read($length), 237 | 0, 238 | $headers[self::HEADER_CONTENT_LENGTH] 239 | ); 240 | 241 | return $headers; 242 | } 243 | 244 | /** 245 | * Read data. 246 | * 247 | * @param int $length Length of data to read. 248 | * @return string 249 | */ 250 | abstract protected function read($length); 251 | } 252 | -------------------------------------------------------------------------------- /Documentation/En/Index.xyl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

The FastCGI protocol offers an interface between an 7 | HTTP server and an external program. When 8 | PHP runs behind a CGI server, the Hoa\Fastcgi 9 | library allows to easily create new PHP executions.

10 | 11 |

Table of contents

12 | 13 | 14 | 15 |

Introduction

16 | 17 |
18 | 35 |

CGI is a protocol ensuring the 36 | communication between an HTTP server and an 37 | external program, for example PHP, and is specified in the 38 | RFC3875. CGI embraces the 39 | “one new execution per request” model. In general, each 40 | execution or processus, lives while computing a 41 | response and then dies. However, the HTTP servers load are 42 | growing, CGI has began to reach its limits, notably in term 43 | of speed: too much messages were exchanged on the network and the processus 44 | managing was no longer efficient. That's why the 45 | FastCGI protocol has been proposed in order 46 | to resolve all these problems. Historically, even though we use CGI or 47 | FastCGI, those servers are always called CGI servers.

48 |

The HTTP server receives requests. Thanks to HTTP headers, it detects what 49 | external program is concerned by this request. It keeps some 50 | headers, add new ones, and send a new request to the CGI 51 | server. This latter will then create a new processus and when a response is 52 | computed, kill it and send the response to the HTTP server, which will send it 53 | back to the client.

54 |

CGI relies on TCP, according to the 55 | RFC793, and usually, the 56 | servers listen on the 9000 port and on the local network (or on a protected 57 | network) for security reasons.

58 | 59 |

PHP tools

60 | 61 |

PHP provides the php-cgi tool that allows to 62 | start a CGI server (based on the FastCGI protocol). To use 63 | it:

64 |
$ php-cgi -b 127.0.0.1:9000
65 |

PHP provides another tool: php-fpm that uses 66 | FPM (that stands for FastCGI Process 67 | Manager). This is a CGI server for high performances and 68 | restricted to PHP. To use it:

69 |
$ php-fpm -d listen=127.0.0.1:9000
70 |

Be careful to compile PHP with the 71 | --enable-cgi or --enable-fpm options to get the tool 72 | of your choice.

73 |

However, it is possible that HTTP servers are likely to use their own CGI 74 | server, such as mod_php for 75 | Apache.

76 | 77 |

Execute PHP

78 | 79 |

The Hoa\Fastcgi library allows in a certain way to play the 80 | role of the HTTP server: it allows to send HTTP requests on a 81 | CGI server and get a response. Thus, we will not need an HTTP server but only 82 | the PHP tools.

83 | 84 |

Send a request

85 | 86 |

To send a request, we need to open a connection to the CGI 87 | server and give it to the constructor of Hoa\Fastcgi\Responder. 88 | Thus, we have the Responder.php file:

89 |
$fastcgi = new Hoa\Fastcgi\Responder(
 90 |     new Hoa\Socket\Client('tcp://127.0.0.1:9000')
 91 | );
92 |

To send a request, we use the 93 | Hoa\Fastcgi\Responder::send method that takes as the first 94 | argument a list of HTTP headers and as a the second argument the content of 95 | the request (it means the body, optional). The minimal required headers 96 | are:

97 |
    98 |
  • SCRIPT_FILENAME: the absolute path to the PHP file that 99 | will be executed (this is the real required header),
  • 100 |
  • REQUEST_METHOD: the HTTP method among GET, 101 | POST, PUT, DELETE etc.,
  • 102 |
  • REQUEST_URI: the identifier of the resource we are trying 103 | to reach.
  • 104 |
105 |

This method returns a response to the request. Thus, we are preparing the 106 | Echo.php file:

107 |
&lt;?php
108 | 
109 | echo 'foobar';
110 |

And we add in the Responder.php file:

111 |
var_dump($fastcgi->send([
112 |     'REQUEST_METHOD'  => 'GET',
113 |     'REQUEST_URI'     => '/',
114 |     'SCRIPT_FILENAME' => __DIR__ . DS . 'Echo.php'
115 | ]));
116 | 
117 | /**
118 |  * Will output:
119 |  *    string(6) "foobar"
120 |  */
121 |

The HTTP headers returned by our PHP program do not appear in the response. 122 | But they need to be returned as a response to the HTTP server for the client. 123 | To get them, we only need to call the 124 | Hoa\Fastcgi\Responder::getResponseHeaders method (sister of the 125 | Hoa\Fastcgi\Responder::getResponseContent that returns the same 126 | result that the Hoa\Fastcgi\Responder::send method):

127 |
print_r($fastcgi->getResponseHeaders());
128 | var_dump($fastcgi->getResponseContent());
129 | 
130 | /**
131 |  * Will output:
132 |  *     Array
133 |  *     (
134 |  *         [x-powered-by] => PHP/x.y.z
135 |  *         [content-type] => text/html
136 |  *     )
137 |  *     string(6) "foobar"
138 |  */
139 |

That's it!

140 | 141 |

Possible errors

142 | 143 |

The FastCGI protocol can throw three errors in addition to 144 | those added by Hoa\Fastcgi\Responder:

145 |
    146 |
  • Hoa\Fastcgi\Exception\CannotMultiplex when an external 147 | program does not support multiplexing: several requests 148 | through the same connection,
  • 149 |
  • Hoa\Fastcgi\Exception\Overloaded when an external program 150 | is too busy and reject the request,
  • 151 |
  • Hoa\Fastcgi\Exception\UnknownRole when a role (a pack of 152 | the protocol) is unknown by the external program,
  • 153 |
  • Hoa\Fastcgi\Exception\Exception when an 154 | error from the connection, the protocol or something else 155 | happens (added by Hoa\Fastcgi\Responder).
  • 156 |
157 |

The best way to catch all the exceptions is to catch 158 | Hoa\Fastcgi\Exception\Exception:

159 |
try {
160 |     $fastcgi->send(…);
161 | } catch (Hoa\Fastcgi\Exception\Exception $e) {
162 |     // compute $e.
163 | }
164 |

So far, the most used CGI servers support the multiplexing, high load and 165 | also understand all the roles. The most frequent errors we may encounter will 166 | be related to network and not the protocol.

167 |

Notice that FastCGI supports three types of stream: 168 | STDIN, STDOUT and STDERR. The incoming 169 | stream is used to send a request. The outgoing stream is used 170 | to receive a response. And finally, the error stream is 171 | concatenated to the outgoing stream by 172 | Hoa\Fastcgi\Responder.

173 | 174 |

Conclusion

175 | 176 |

Hoa\Fastcgi allows to execute PHP programs 177 | (or other external programs) in a very simple and 178 | fast way without worry about the location of PHP binaries, 179 | sub-shells to execute them, manage errors etc. We only need a CGI server to be 180 | started.

181 |

Manipulating FastCGI requests in this manner allows also to 182 | learn more about PHP and to manipulate PHP processus in 183 | another way (please, see the 184 | Hoa\Zombie library).

185 | 186 |
187 |
188 | -------------------------------------------------------------------------------- /Documentation/Fr/Index.xyl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

Le protocole FastCGI offre une interface entre un 7 | serveur HTTP et un programme externe. Quand 8 | PHP est exécuté derrière un serveur CGI, la bibliothèque 9 | Hoa\Fastcgi permet de créer facilement de nouvelles exécutions 10 | de PHP.

11 | 12 |

Table des matières

13 | 14 | 15 | 16 |

Introduction

17 | 18 |
19 | 36 |

CGI est un protocole permettant d'assurer la 37 | communication entre un serveur HTTP et un 38 | programme externe, par exemple PHP, et est spécifié dans la 39 | RFC3875. CGI adopte le 40 | modèle « une nouvelle exécution par requête ». En général, 41 | chaque exécution, ou processus, vit le temps de calculer une 42 | réponse à la requête pour ensuite mourir. Toutefois, la 43 | charge des serveurs HTTP grandissant, CGI a commencé à montrer ses 44 | limites, notamment en terme de vitesse : trop de messages 45 | transitaient sur le réseau et la gestion des processus n'était plus 46 | efficace. C'est pourquoi, le protocole 47 | FastCGI a été proposé afin de résoudre tous ces problèmes. Historiquement, 48 | bien que nous utilisons CGI ou FastCGI, ces serveurs sont toujours appelés 49 | serveurs CGI.

50 |

Le serveur HTTP reçoit des requêtes. Grâce aux en-têtes HTTP, il détecte 51 | quel programme externe est concerné par cette requête. Il 52 | conserve certaines en-têtes, en ajoute de nouvelles, et envoie une 53 | nouvelle requête auprès du serveur CGI. Ce dernier va alors 54 | créer un nouveau processus et une fois une réponse obtenue, tuer ce processus 55 | puis envoyer la réponse au serveur HTTP, qui s'occupera alors de la retourner 56 | au client.

57 |

CGI fonctionne en TCP, selon la 58 | RFC793, et usuellement, les 59 | serveurs écoutent sur le port 9000 et en local (ou sur un réseau protégé) pour 60 | des raisons de sécurité.

61 | 62 |

Les outils de PHP

63 | 64 |

PHP propose l'outil php-cgi qui permet de 65 | démarrer un serveur CGI (reposant sur le protocole FastCGI). 66 | Pour l'utiliser :

67 |
$ php-cgi -b 127.0.0.1:9000
68 |

PHP propose un autre outil : php-fpm, qui utilise 69 | FPM (pour FastCGI 70 | Process Manager). C'est un serveur CGI dédié à la haute 71 | performance et uniquement réservé à PHP. Pour l'utiliser :

72 |
$ php-fpm -d listen=127.0.0.1:9000
73 |

Attention à bien compiler PHP avec les options 74 | --enable-cgi ou --enable-fpm pour obtenir l'outil de 75 | votre choix.

76 |

Toutefois, il se peut que des serveurs HTTP préfèrent utiliser leur propre 77 | serveur CGI, comme c'est le cas avec le mod_php pour 78 | Apache.

79 | 80 |

Exécuter PHP

81 | 82 |

La bibliothèque Hoa\Fastcgi permet d'une certaine façon de 83 | jouer le rôle du serveur HTTP : elle permet d'envoyer des 84 | requêtes HTTP sur un serveur CGI et de récupérer une réponse. Ainsi, nous 85 | n'aurons pas besoin de serveur HTTP, mais uniquement des outils de PHP.

86 | 87 |

Envoyer une requête

88 | 89 |

Pour envoyer une requête, nous devons ouvrir une connexion 90 | vers le serveur CGI, et la donner au constructeur de 91 | Hoa\Fastcgi\Responder. Ainsi, nous avons le fichier 92 | Responder.php :

93 |
$fastcgi = new Hoa\Fastcgi\Responder(
 94 |     new Hoa\Socket\Client('tcp://127.0.0.1:9000')
 95 | );
96 |

Pour envoyer une requête, nous utilisons la méthode 97 | Hoa\Fastcgi\Responder::send qui prend en premier argument la 98 | liste des en-têtes HTTP et en second argument le contenu de la requête 99 | (i.e. le corps, optionnel). Les en-têtes minimales sont :

100 |
    101 |
  • SCRIPT_FILENAME : le chemin absolu vers le fichier PHP qui 102 | sera exécuté (la seule réellement nécessaire) ;
  • 103 |
  • REQUEST_METHOD : méthode HTTP parmi GET, 104 | POST, PUT, DELETE etc. ;
  • 105 |
  • REQUEST_URI : l'identifiant de la ressource à laquelle nous 106 | tentons d'accéder.
  • 107 |
108 |

Cette méthode retourne la réponse de la requête. Ainsi, nous préparons 109 | notre fichier Echo.php :

110 |
&lt;?php
111 | 
112 | echo 'foobar';
113 |

Et nous ajoutons à Responder.php :

114 |
var_dump($fastcgi->send([
115 |     'REQUEST_METHOD'  => 'GET',
116 |     'REQUEST_URI'     => '/',
117 |     'SCRIPT_FILENAME' => __DIR__ . DS . 'Echo.php'
118 | ]));
119 | 
120 | /**
121 |  * Will output:
122 |  *    string(6) "foobar"
123 |  */
124 |

Les en-têtes HTTP retournées par notre programme PHP n'apparaissent pas 125 | dans la réponse. Elles doivent pourtant être retournées au serveur HTTP en 126 | tant que réponse au client. Pour les obtenir, il suffit d'appeler la méthode 127 | Hoa\Fastcgi\Responder::getResponseHeaders (sœur de 128 | Hoa\Fastcgi\Responder::getResponseContent qui retourne la même 129 | chose que la méthode Hoa\Fastcgi\Responder::send) :

130 |
print_r($fastcgi->getResponseHeaders());
131 | var_dump($fastcgi->getResponseContent());
132 | 
133 | /**
134 |  * Will output:
135 |  *     Array
136 |  *     (
137 |  *         [x-powered-by] => PHP/x.y.z
138 |  *         [content-type] => text/html
139 |  *     )
140 |  *     string(6) "foobar"
141 |  */
142 |

C'est aussi simple que ça !

143 | 144 |

Erreurs possibles

145 | 146 |

Le protocole FastCGI peut lever trois erreurs possibles en 147 | plus de celles ajoutées par Hoa\Fastcgi\Responder :

148 |
    149 |
  • Hoa\Fastcgi\Exception\CannotMultiplex quand le 150 | programme externe ne supporte pas le multiplexage : 151 | plusieurs requêtes à travers une seule connexion ;
  • 152 |
  • Hoa\Fastcgi\Exception\Overloaded quand le 153 | programme externe est trop occupé et a 154 | rejeté la requête ;
  • 155 |
  • Hoa\Fastcgi\Exception\UnknownRole quand un rôle 156 | (un paquet du protocole) n'est pas reconnu par le programme 157 | externe ;
  • 158 |
  • Hoa\Fastcgi\Exception\Exception quand une 159 | erreur de connexion, de protocole ou autre intervient 160 | (ajouté par Hoa\Fastcgi\Responder).
  • 161 |
162 |

Le meilleur moyen de capturer toutes les exceptions est de capturer 163 | Hoa\Fastcgi\Exception\Exception :

164 |
try {
165 |     $fastcgi->send(…);
166 | } catch (Hoa\Fastcgi\Exception\Exception $e) {
167 |     // compute $e.
168 | }
169 |

Actuellement, les serveurs CGI les plus répandus supportent le 170 | multiplexage, gèrent bien la charge et supportent tous les rôles. Les erreurs 171 | que nous aurons le plus souvent seront des erreurs de réseaux 172 | et non de protocole.

173 |

Notons que FastCGI supporte trois types de flux : 174 | STDIN, STDOUT et STDERR. Le flux 175 | entrant sert à envoyer la requête. Le flux sortant sert à 176 | recevoir la réponse. Et enfin, le flux d'erreur est 177 | concaténé par Hoa\Fastcgi\Responder au flux 178 | sortant.

179 | 180 |

Conclusion

181 | 182 |

Hoa\Fastcgi permet d'exécuter des programmes 183 | PHP (ou d'autres programmes externes) en toute 184 | simplicité et rapidement sans se préoccuper 185 | de la localisation des binaires PHP, des sous-shells pour les exécuter, de 186 | gérer les erreurs etc. Il suffit qu'un serveur CGI soit démarré, c'est tout ce 187 | qui est nécessaire.

188 |

Manipuler les requêtes FastCGI de cette manière permet également d'en 189 | apprendre plus sur PHP et de manipuler les processus PHP 190 | d'une autre manière (voir le chapitre sur 191 | Hoa\Zombie).

192 | 193 |
194 |
195 | -------------------------------------------------------------------------------- /Exception/CannotMultiplex.php: -------------------------------------------------------------------------------- 1 | 2 | Hoa 3 |

4 | 5 | --- 6 | 7 |

8 | Build status 9 | Code coverage 10 | Packagist 11 | License 12 |

13 |

14 | Hoa is a modular, extensible and 15 | structured set of PHP libraries.
16 | Moreover, Hoa aims at being a bridge between industrial and research worlds. 17 |

18 | 19 | # Hoa\Fastcgi 20 | 21 | [![Help on IRC](https://img.shields.io/badge/help-%23hoaproject-ff0066.svg)](https://webchat.freenode.net/?channels=#hoaproject) 22 | [![Help on Gitter](https://img.shields.io/badge/help-gitter-ff0066.svg)](https://gitter.im/hoaproject/central) 23 | [![Documentation](https://img.shields.io/badge/documentation-hack_book-ff0066.svg)](https://central.hoa-project.net/Documentation/Library/Fastcgi) 24 | [![Board](https://img.shields.io/badge/organisation-board-ff0066.svg)](https://waffle.io/hoaproject/fastcgi) 25 | 26 | This library allows to manipulate the [FastCGI](http://fastcgi.com/) protocol, 27 | which ensures the communication between a HTTP server and an external program 28 | (such as PHP). 29 | 30 | [Learn more](https://central.hoa-project.net/Documentation/Library/Fastcgi). 31 | 32 | ## Installation 33 | 34 | With [Composer](https://getcomposer.org/), to include this library into 35 | your dependencies, you need to 36 | require [`hoa/fastcgi`](https://packagist.org/packages/hoa/fastcgi): 37 | 38 | ```sh 39 | $ composer require hoa/fastcgi '~3.0' 40 | ``` 41 | 42 | For more installation procedures, please read [the Source 43 | page](https://hoa-project.net/Source.html). 44 | 45 | ## Testing 46 | 47 | Before running the test suites, the development dependencies must be installed: 48 | 49 | ```sh 50 | $ composer install 51 | ``` 52 | 53 | Then, to run all the test suites: 54 | 55 | ```sh 56 | $ vendor/bin/hoa test:run 57 | ``` 58 | 59 | For more information, please read the [contributor 60 | guide](https://hoa-project.net/Literature/Contributor/Guide.html). 61 | 62 | ## Quick usage 63 | 64 | As a quick overview, we propose to execute a PHP file through the FastCGI 65 | protocol directly. 66 | 67 | Before starting, we need to know that PHP proposes two tools that support 68 | FastCGI: `php-cgi` and `php-fpm` (for 69 | [FastCGI Process Manager](http://php.net/install.fpm)). We will use `php-cgi` in 70 | local with the standard port `9000` in TCP: 71 | 72 | ```sh 73 | $ php-cgi -b 127.0.0.1:9000 74 | ``` 75 | 76 | First, we write the `Echo.php` file, the one we are likely to execute: 77 | 78 | ```php 79 | send([ 101 | 'REQUEST_METHOD' => 'GET', 102 | 'REQUEST_URI' => '/', 103 | 'SCRIPT_FILENAME' => __DIR__ . DS . 'Echo.php' 104 | ])); 105 | // string(6) "foobar" 106 | ``` 107 | 108 | We can get the headers from the executed file by calling the 109 | `Hoa\Fastcgi\Responder::getResponseHeaders` method. 110 | 111 | This is a good and fast way to execute PHP files (or other programs that support 112 | FastCGI) without worrying about binary locations, subshell calls, error 113 | handling, etc. 114 | 115 | ## Documentation 116 | 117 | The 118 | [hack book of `Hoa\Fastcgi`](https://central.hoa-project.net/Documentation/Library/Fastcgi) 119 | contains detailed information about how to use this library and how it works. 120 | 121 | To generate the documentation locally, execute the following commands: 122 | 123 | ```sh 124 | $ composer require --dev hoa/devtools 125 | $ vendor/bin/hoa devtools:documentation --open 126 | ``` 127 | 128 | More documentation can be found on the project's website: 129 | [hoa-project.net](https://hoa-project.net/). 130 | 131 | ## Getting help 132 | 133 | There are mainly two ways to get help: 134 | 135 | * On the [`#hoaproject`](https://webchat.freenode.net/?channels=#hoaproject) 136 | IRC channel, 137 | * On the forum at [users.hoa-project.net](https://users.hoa-project.net). 138 | 139 | ## Contribution 140 | 141 | Do you want to contribute? Thanks! A detailed [contributor 142 | guide](https://hoa-project.net/Literature/Contributor/Guide.html) explains 143 | everything you need to know. 144 | 145 | ## License 146 | 147 | Hoa is under the New BSD License (BSD-3-Clause). Please, see 148 | [`LICENSE`](https://hoa-project.net/LICENSE) for details. 149 | -------------------------------------------------------------------------------- /Responder.php: -------------------------------------------------------------------------------- 1 | setClient($client); 171 | 172 | return; 173 | } 174 | 175 | /** 176 | * Send data on a FastCGI. 177 | * 178 | * @param array $headers Headers. 179 | * @param string $content Content (e.g. key=value for POST). 180 | * @return string 181 | * @throws \Hoa\Socket\Exception 182 | * @throws \Hoa\Fastcgi\Exception 183 | * @throws \Hoa\Fastcgi\Exception\CannotMultiplex 184 | * @throws \Hoa\Fastcgi\Exception\Overloaded 185 | * @throws \Hoa\Fastcgi\Exception\UnknownRole 186 | * @throws \Hoa\Fastcgi\Exception\UnknownStatus 187 | */ 188 | public function send(array $headers, $content = null) 189 | { 190 | $this->_responseOutput = null; 191 | $this->_responseError = null; 192 | $this->_responseHeaders = []; 193 | 194 | $client = $this->getClient(); 195 | $client->connect(); 196 | $client->setStreamBlocking(true); 197 | 198 | $parameters = null; 199 | $responseOutput = null; 200 | 201 | $request = $this->pack( 202 | self::REQUEST_BEGIN, 203 | // ┌───────────────────┐ 204 | // │ “I'm a responder” │ 205 | // └─────────⌵─────────┘ 206 | chr(0) . chr(1) . chr((int) $client->isPersistent()) . 207 | chr(0) . chr(0) . chr(0) . chr(0) . chr(0) 208 | ); 209 | 210 | $parameters .= $this->packPairs($headers); 211 | 212 | if (null !== $parameters) { 213 | $request .= $this->pack(self::REQUEST_PARAMETERS, $parameters); 214 | } 215 | 216 | $request .= $this->pack(self::REQUEST_PARAMETERS, ''); 217 | 218 | if (null !== $content) { 219 | // The maximum length of each record is 65535 bytes. 220 | // Pack multiple records if the length is larger than the 221 | // 65535 bytes. 222 | foreach (str_split($content, 65535) as $chunk) { 223 | $request .= $this->pack(self::STREAM_STDIN, $chunk); 224 | } 225 | } 226 | 227 | $request .= $this->pack(self::STREAM_STDIN, ''); 228 | $client->writeAll($request); 229 | $handle = null; 230 | 231 | do { 232 | if (false === $handle = $this->readPack()) { 233 | throw new Exception( 234 | 'Bad request (not a well-formed FastCGI request).', 235 | 0 236 | ); 237 | } 238 | 239 | if (self::STREAM_STDOUT === $handle[parent::HEADER_TYPE]) { 240 | $responseOutput .= $handle[parent::HEADER_CONTENT]; 241 | } elseif (self::STREAM_STDERR === $handle[parent::HEADER_TYPE]) { 242 | $this->_responseError .= $handle[parent::HEADER_CONTENT]; 243 | } 244 | } while (self::REQUEST_END !== $handle[parent::HEADER_TYPE]); 245 | 246 | $client->disconnect(); 247 | 248 | $status = ord($handle[parent::HEADER_CONTENT][4]); 249 | 250 | switch ($status) { 251 | case self::STATUS_CANNOT_MULTIPLEX: 252 | throw new Exception\CannotMultiplex( 253 | 'Application %s that you are trying to reach does not ' . 254 | 'support multiplexing.', 255 | 1, 256 | $this->getClient()->getSocket()->__toString() 257 | ); 258 | 259 | break; 260 | 261 | case self::STATUS_OVERLOADED: 262 | throw new Exception\Overloaded( 263 | 'Application %s is too busy and rejects your request.', 264 | 2, 265 | $this->getClient()->getSocket()->__toString() 266 | ); 267 | 268 | break; 269 | 270 | case self::STATUS_UNKNOWN_ROLE: 271 | throw new Exception\UnknownRole( 272 | 'Server for the application %s returns an unknown role.', 273 | 3, 274 | $this->getClient()->getSocket()->__toString() 275 | ); 276 | 277 | break; 278 | 279 | case self::STATUS_COMPLETE: 280 | break; 281 | 282 | default: 283 | throw new Exception\UnknownStatus( 284 | 'Server for the application %s returns an unknown status: %d.', 285 | 4, 286 | [ 287 | $this->getClient()->getSocket()->__toString(), 288 | $status 289 | ] 290 | ); 291 | } 292 | 293 | $pos = strpos($responseOutput, "\r\n\r\n"); 294 | 295 | $this->buildResponseHeaders(substr($responseOutput, 0, $pos)); 296 | 297 | return $this->_responseOutput = substr($responseOutput, $pos + 4); 298 | } 299 | 300 | /** 301 | * Build the response header array. 302 | * 303 | * @param string $headers The response header string 304 | * @return void 305 | */ 306 | protected function buildResponseHeaders($headers) 307 | { 308 | foreach (explode("\r\n", $headers) as $header) { 309 | $semicolon = strpos($header, ':'); 310 | $this->_responseHeaders[strtolower(trim(substr($header, 0, $semicolon)))] 311 | = trim(substr($header, $semicolon + 1)); 312 | } 313 | } 314 | 315 | /** 316 | * Get response content. 317 | * 318 | * @return ?string 319 | */ 320 | public function getResponseContent() 321 | { 322 | return $this->_responseOutput; 323 | } 324 | 325 | /** 326 | * Get response error if any. 327 | * 328 | * @return ?string 329 | */ 330 | public function getResponseError() 331 | { 332 | return $this->_responseError; 333 | } 334 | 335 | /** 336 | * Get response headers. 337 | * 338 | * @return array 339 | */ 340 | public function getResponseHeaders() 341 | { 342 | return $this->_responseHeaders; 343 | } 344 | 345 | /** 346 | * Read data. 347 | * 348 | * @param int $length Length of data to read. 349 | * @return string 350 | */ 351 | protected function read($length) 352 | { 353 | return $this->getClient()->read($length); 354 | } 355 | 356 | /** 357 | * Set client. 358 | * 359 | * @param \Hoa\Socket\Client $client Client. 360 | * @return \Hoa\Socket\Client 361 | */ 362 | public function setClient(Socket\Client $client) 363 | { 364 | $old = $this->_client; 365 | $this->_client = $client; 366 | 367 | return $old; 368 | } 369 | 370 | /** 371 | * Get client. 372 | * 373 | * @return \Hoa\Socket\Client 374 | */ 375 | public function getClient() 376 | { 377 | return $this->_client; 378 | } 379 | } 380 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "hoa/fastcgi", 3 | "description": "The Hoa\\Fastcgi library.", 4 | "type" : "library", 5 | "keywords" : ["library", "fastcgi", "cgi"], 6 | "homepage" : "https://hoa-project.net/", 7 | "license" : "BSD-3-Clause", 8 | "authors" : [ 9 | { 10 | "name" : "Ivan Enderlin", 11 | "email": "ivan.enderlin@hoa-project.net" 12 | }, 13 | { 14 | "name" : "Hoa community", 15 | "homepage": "https://hoa-project.net/" 16 | } 17 | ], 18 | "support": { 19 | "email" : "support@hoa-project.net", 20 | "irc" : "irc://chat.freenode.net/hoaproject", 21 | "forum" : "https://users.hoa-project.net/", 22 | "docs" : "https://central.hoa-project.net/Documentation/Library/Fastcgi", 23 | "source": "https://central.hoa-project.net/Resource/Library/Fastcgi" 24 | }, 25 | "require": { 26 | "hoa/consistency": "~1.0", 27 | "hoa/exception" : "~1.0", 28 | "hoa/socket" : "~1.0" 29 | }, 30 | "require-dev": { 31 | "hoa/test": "~2.0" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "Hoa\\Fastcgi\\": "." 36 | } 37 | }, 38 | "extra": { 39 | "branch-alias": { 40 | "dev-master": "3.x-dev" 41 | } 42 | } 43 | } 44 | --------------------------------------------------------------------------------