Connection::httpReadHeaders()   B
last analyzed

Complexity

Conditions 7
Paths 5

Size

Total Lines 21

Duplication

Lines 4
Ratio 19.05 %

Importance

Changes 0
Metric Value
cc 7
dl 4
loc 21
rs 8.6506
c 0
b 0
f 0
nc 5
nop 0
1
<?php
2
namespace PHPDaemon\Servers\HTTP;
3
4
use PHPDaemon\Core\Daemon;
5
use PHPDaemon\Core\Timer;
6
use PHPDaemon\FS\FileSystem;
7
use PHPDaemon\HTTPRequest\Generic;
8
use PHPDaemon\HTTPRequest\Input;
9
use PHPDaemon\Request\IRequestUpstream;
10
11
/**
12
 * @package    NetworkServers
13
 * @subpackage Base
14
 * @author     Vasily Zorin <[email protected]>
15
 */
16
class Connection extends \PHPDaemon\Network\Connection implements IRequestUpstream
17
{
18
    protected $initialLowMark = 1;
19
20
    /**
21
     * @var integer initial value of the maximum amout of bytes in buffer
22
     */
23
    protected $initialHighMark = 8192;
24
25
    protected $timeout = 45;
26
27
    protected $req;
28
29
    protected $keepaliveTimer;
30
31
    protected $freedBeforeProcessing = false;
32
33
    /**
34
     * @TODO DESCR
35
     */
36
    const STATE_FIRSTLINE = 1;
37
    /**
38
     * @TODO DESCR
39
     */
40
    const STATE_HEADERS = 2;
41
    /**
42
     * @TODO DESCR
43
     */
44
    const STATE_CONTENT = 3;
45
    /**
46
     * @TODO DESCR
47
     */
48
    const STATE_PROCESSING = 4;
49
50
    protected $EOL = "\r\n";
51
    protected $currentHeader;
52
53
    protected $policyReqNotFound = false;
54
55
    /**
56
     * Check if Sendfile is supported here.
57
     * @return boolean Succes
58
     */
59
    public function checkSendfileCap()
60
    {
61
        // @todo DISCUSS
62
        return true;
63
    }
64
65
    /**
66
     * Check if Chunked encoding is supported here.
67
     * @return boolean Succes
68
     */
69
    public function checkChunkedEncCap()
70
    {
71
        // @todo DISCUSS
72
        return true;
73
    }
74
75
    /**
76
     * @TODO
77
     * @return integer
78
     */
79
    public function getKeepaliveTimeout()
80
    {
81
        return $this->pool->config->keepalive->value;
82
    }
83
84
    /**
85
     * Read first line of HTTP request
86
     * @return boolean|null Success
87
     */
88
    protected function httpReadFirstline()
89
    {
90
        //D($this->look(2048));
91
        if (($l = $this->readline()) === null) {
92
            return null;
93
        }
94
        $e = explode(' ', $l);
95
        $u = isset($e[1]) ? parse_url($e[1]) : false;
96
        if ($u === false) {
97
            $this->badRequest($this->req);
98
            return false;
99
        }
100
        if (!isset($u['path'])) {
101
            $u['path'] = null;
102
        }
103
        if (isset($u['host'])) {
104
            $this->req->attrs->server['HTTP_HOST'] = $u['host'];
105
        }
106
        $srv = &$this->req->attrs->server;
107
        $srv['REQUEST_METHOD'] = $e[0];
108
        $srv['REQUEST_TIME'] = time();
109
        $srv['REQUEST_TIME_FLOAT'] = microtime(true);
110
        $srv['REQUEST_URI'] = $u['path'] . (isset($u['query']) ? '?' . $u['query'] : '');
111
        $srv['DOCUMENT_URI'] = $u['path'];
112
        $srv['PHP_SELF'] = $u['path'];
113
        $srv['QUERY_STRING'] = isset($u['query']) ? $u['query'] : null;
114
        $srv['SCRIPT_NAME'] = $srv['DOCUMENT_URI'] = isset($u['path']) ? $u['path'] : '/';
115
        $srv['SERVER_PROTOCOL'] = isset($e[2]) ? $e[2] : 'HTTP/1.1';
116
        $srv['REMOTE_ADDR'] = $this->host;
117
        $srv['REMOTE_PORT'] = $this->port;
118
        $srv['HTTPS'] = $this->ssl ? 'on' : 'off';
119
        return true;
120
    }
121
122
    /**
123
     * Read headers line-by-line
124
     * @return boolean|null Success
125
     */
126
    protected function httpReadHeaders()
127
    {
128
        while (($l = $this->readLine()) !== null) {
129
            if ($l === '') {
130
                return true;
131
            }
132
            $e = explode(': ', $l);
133
            if (isset($e[1])) {
134
                $this->currentHeader = 'HTTP_' . strtoupper(strtr($e[0], Generic::$htr));
135
                $this->req->attrs->server[$this->currentHeader] = $e[1];
136 View Code Duplication
            } elseif (($e[0][0] === "\t" || $e[0][0] === "\x20") && $this->currentHeader) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
137
                // multiline header continued
138
                $this->req->attrs->server[$this->currentHeader] .= $e[0];
139
            } else {
140
                // whatever client speaks is not HTTP anymore
141
                $this->badRequest($this->req);
142
                return false;
143
            }
144
        }
145
        return null;
146
    }
147
148
    /**
149
     * Creates new Request object
150
     * @return \stdClass
151
     */
152
    protected function newRequest()
153
    {
154
        $req = new \stdClass;
155
        $req->attrs = new \stdClass();
156
        $req->attrs->request = [];
157
        $req->attrs->get = [];
158
        $req->attrs->post = [];
159
        $req->attrs->cookie = [];
160
        $req->attrs->server = [];
161
        $req->attrs->files = [];
162
        $req->attrs->session = null;
163
        $req->attrs->paramsDone = false;
164
        $req->attrs->inputDone = false;
165
        $req->attrs->input = new Input();
166
        $req->attrs->inputReaded = 0;
167
        $req->attrs->chunked = false;
168
        $req->upstream = $this;
169
        return $req;
170
    }
171
172
    /**
173
     * Process HTTP headers
174
     * @return boolean Success
175
     */
176
    protected function httpProcessHeaders()
177
    {
178
        $this->req->attrs->paramsDone = true;
179
        if (isset($this->req->attrs->server['HTTP_CONNECTION'])
180
            && preg_match('~(?:^|\W)Upgrade(?:\W|$)~i', $this->req->attrs->server['HTTP_CONNECTION'])
181
            && isset($this->req->attrs->server['HTTP_UPGRADE'])
182
            && (strtolower($this->req->attrs->server['HTTP_UPGRADE']) === 'websocket')
183
        ) {
184
            if ($this->pool->WS) {
185
                $this->pool->WS->inheritFromRequest($this->req, $this);
186
            }
187
            return false;
188
        }
189
190
        $this->req = Daemon::$appResolver->getRequest(
191
            $this->req,
192
            $this,
193
            isset($this->pool->config->responder->value) ? $this->pool->config->responder->value : null
194
        );
195
196
        if ($this->req instanceof \stdClass) {
197
            $this->endRequest($this->req, 0, 0);
198
            return false;
199
        } else {
200
            if ($this->pool->config->sendfile->value
201
                && (!$this->pool->config->sendfileonlybycommand->value || isset($this->req->attrs->server['USE_SENDFILE']))
202
                && !isset($this->req->attrs->server['DONT_USE_SENDFILE'])
203
            ) {
204
                $req = $this->req;
205
206
                FileSystem::tempnam(
207
                    $this->pool->config->sendfiledir->value,
208
                    $this->pool->config->sendfileprefix->value,
209
                    function ($fn) use ($req) {
210
                        FileSystem::open($fn, 'wb', function ($file) use ($req) {
211
                            $req->sendfp = $file;
212
                        });
213
                        $req->header('X-Sendfile: ' . $fn);
214
                    }
215
                );
216
            }
217
            $this->req->callInit();
218
        }
219
        return true;
220
    }
221
222
    /* Used for debugging protocol issues */
223
    /*public function readline() {
224
        $s = parent::readline();
225
        Daemon::log(Debug::json($s));
226
        return $s;
227
    }
228
229
    public function write($s) {
230
        Daemon::log(Debug::json($s));
231
        parent::write($s);
232
    }*
233
234
    /**
235
     * Called when new data received.
236
     * @return void
237
     */
238
239
    /**
240
     * onRead
241
     * @return void
242
     */
243
    protected function onRead()
244
    {
245 View Code Duplication
        if (!$this->policyReqNotFound) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
246
            $d = $this->drainIfMatch("<policy-file-request/>\x00");
247
            if ($d === null) { // partially match
248
                return;
249
            }
250
            if ($d) {
251
                $FP = \PHPDaemon\Servers\FlashPolicy\Pool::getInstance(
252
                    $this->pool->config->fpsname->value,
253
                    false
254
                );
255
                if ($FP && $FP->policyData
256
                ) {
257
                    $this->write($FP->policyData . "\x00");
258
                }
259
                $this->finish();
260
                return;
261
            } else {
262
                $this->policyReqNotFound = true;
263
            }
264
        }
265
        start:
266
        if ($this->finished) {
267
            return;
268
        }
269
        if ($this->state === self::STATE_ROOT) {
270
            if ($this->req !== null) { // we have to wait the current request
271
                return;
272
            }
273
            if (!$this->req = $this->newRequest()) {
274
                $this->finish();
275
                return;
276
            }
277
            $this->state = self::STATE_FIRSTLINE;
278
        } else {
279
            if (!$this->req || $this->state === self::STATE_PROCESSING) {
280
                if (isset($this->bev) && ($this->bev->input->length > 0)) {
281
                    Daemon::log('Unexpected input (HTTP request, ' . $this->state . '): ' . json_encode($this->read($this->bev->input->length)));
282
                }
283
                return;
284
            }
285
        }
286
287
        if ($this->state === self::STATE_FIRSTLINE) {
288
            if (!$this->httpReadFirstline()) {
289
                return;
290
            }
291
            Timer::remove($this->keepaliveTimer);
292
            $this->state = self::STATE_HEADERS;
293
        }
294
295 View Code Duplication
        if ($this->state === self::STATE_HEADERS) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
296
            if (!$this->httpReadHeaders()) {
297
                return;
298
            }
299
            if (!$this->httpProcessHeaders()) {
300
                $this->finish();
301
                return;
302
            }
303
            $this->state = self::STATE_CONTENT;
304
        }
305
        if ($this->state === self::STATE_CONTENT) {
306
            if (!isset($this->req->attrs->input) || !$this->req->attrs->input) {
307
                $this->finish();
308
                return;
309
            }
310
            $this->req->attrs->input->readFromBuffer($this->bev->input);
311
            if (!$this->req->attrs->input->isEOF()) {
312
                return;
313
            }
314
            $this->state = self::STATE_PROCESSING;
315
            if ($this->freedBeforeProcessing) {
316
                $this->freeRequest($this->req);
317
                $this->freedBeforeProcessing = false;
318
                goto start;
319
            }
320
            $this->freezeInput();
321
            if ($this->req->attrs->inputDone && $this->req->attrs->paramsDone) {
322
                if ($this->pool->variablesOrder === null) {
323
                    $this->req->attrs->request = $this->req->attrs->get + $this->req->attrs->post + $this->req->attrs->cookie;
324
                } else {
325
                    for ($i = 0, $s = mb_orig_strlen($this->pool->variablesOrder); $i < $s; ++$i) {
326
                        $char = $this->pool->variablesOrder[$i];
327
                        if ($char === 'G') {
328 View Code Duplication
                            if (is_array($this->req->attrs->get)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
329
                                $this->req->attrs->request += $this->req->attrs->get;
330
                            }
331 View Code Duplication
                        } elseif ($char === 'P') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
332
                            if (is_array($this->req->attrs->post)) {
333
                                $this->req->attrs->request += $this->req->attrs->post;
334
                            }
335
                        } elseif ($char === 'C') {
336 View Code Duplication
                            if (is_array($this->req->attrs->cookie)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
337
                                $this->req->attrs->request += $this->req->attrs->cookie;
338
                            }
339
                        }
340
                    }
341
                }
342
                Daemon::$process->timeLastActivity = time();
343
            }
344
        }
345
    }
346
347
    /**
348
     * Handles the output from downstream requests.
349
     * @param  object $req \PHPDaemon\Request\Generic.
350
     * @param  string $s The output.
351
     * @return boolean      Success
352
     */
353
    public function requestOut($req, $s)
354
    {
355
        if ($this->write($s) === false) {
356
            $req->abort();
357
            return false;
358
        }
359
        return true;
360
    }
361
362
    /**
363
     * End request
364
     * @return void
365
     */
366
    public function endRequest($req, $appStatus, $protoStatus)
367
    {
368
        if ($protoStatus === -1) {
369
            $this->close();
370
        } else {
371
            if ($req->attrs->chunked) {
372
                $this->write("0\r\n\r\n");
373
            }
374
375
            if (isset($req->keepalive) && $req->keepalive && $this->pool->config->keepalive->value) {
376
                $this->keepaliveTimer = setTimeout(function ($timer) {
0 ignored issues
show
Unused Code introduced by
The parameter $timer is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
377
                    $this->finish();
378
                }, $this->pool->config->keepalive->value);
379
            } else {
380
                $this->finish();
381
            }
382
        }
383
        $this->freeRequest($req);
384
    }
385
386
    /**
387
     * Frees this request
388
     * @return void
389
     */
390
    public function freeRequest($req)
391
    {
392
        if ($this->state !== self::STATE_PROCESSING) {
393
            $this->freedBeforeProcessing = true;
394
            return;
395
        }
396
        $req->attrs->input = null;
397
        $this->req = null;
398
        $this->state = self::STATE_ROOT;
399
        $this->unfreezeInput();
400
    }
401
402
    /**
403
     * Called when connection is finished
404
     * @return void
405
     */
406
    public function onFinish()
407
    {
408
        Timer::remove($this->keepaliveTimer);
409
        if ($this->req !== null && $this->req instanceof Generic) {
410
            if (!$this->req->isFinished()) {
411
                $this->req->abort();
412
            }
413
        }
414
        $this->req = null;
415
    }
416
417
    /**
418
     * Send Bad request
419
     * @return void
420
     */
421
    public function badRequest($req)
422
    {
423
        $this->state = self::STATE_ROOT;
424
        $this->write("400 Bad Request\r\n\r\n<html><head><title>400 Bad Request</title></head><body bgcolor=\"white\"><center><h1>400 Bad Request</h1></center></body></html>");
425
        $this->finish();
426
    }
427
}
428