Completed
Push — master ( 4e6a16...ebf796 )
by Никита
10:49
created

check_timeouts_on_connection_request_received()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 5
c 0
b 0
f 0
ccs 4
cts 4
cp 1
rs 9.4285
cc 3
eloc 4
nc 3
nop 1
crap 3
1
<?php
2
3
    namespace NokitaKaze\TestHTTPServer;
4
5
    /**
6
     * Class Server
7
     * @package NokitaKaze\TestHTTPServer
8
     *
9
     * @doc https://tools.ietf.org/html/rfc7230
10
     * @doc http://httpwg.org/specs/rfc7230.html
11
     */
12
    class Server {
13
        const CHUNK_SIZE = 4096;
14
15
        /**
16
         * @var ServerSettings
17
         */
18
        protected $_settings = null;
19
20
        /**
21
         * @var resource
22
         */
23
        protected $_server_socket = null;
24
25
        /**
26
         * @var resource
27
         */
28
        protected $_stream_context = null;
29
30
        /**
31
         * @var ClientDatum[]
32
         */
33
        protected $_client_connects = [];
34
35
        /**
36
         * Server constructor
37
         *
38
         * @param ServerSettings|object|array $settings
39
         *
40
         * @throws Exception
41
         */
42 24
        function __construct($settings) {
43 24
            $settings = (object) $settings;
44 24
            self::fix_incoming_settings($settings);
45 24
            $this->_settings = $settings;
46 24
            $this->init_ssl_settings();
47 24
        }
48
49 24
        function __destruct() {
50 24
            $this->shutdown();
51 24
        }
52
53 24
        function shutdown() {
54 24
            foreach ($this->_client_connects as &$connect) {
55
                $this->close_connection($connect);
56 10
            }
57 24
            $this->_client_connects = [];
58 24
            $this->close_connection_socket($this->_server_socket);
59 24
            $this->_server_socket = null;
60 24
        }
61
62
        /**
63
         * @param resource $connection
64
         */
65 24
        function close_connection_socket($connection) {
66 24
            if (is_resource($connection)) {
67 24
                fclose($connection);
68 10
            }
69 24
        }
70
71
        /**
72
         * @param object|ClientDatum $connection
73
         */
74 24
        function close_connection(&$connection) {
75 24
            if (is_null($connection)) {
76
                return;
77
            }
78 24
            $this->close_connection_socket($connection->client);
79 24
            $connection->client = null;
80 24
            $connection = null;
81 24
        }
82
83
        /**
84
         * @return array
85
         */
86 24
        static function get_default_settings() {
87
            return [
88 24
                'interface' => '127.0.0.1',
89 24
                'port' => 58080,
90 24
                'server_sleep_if_no_connect' => 1,
91 10
                'is_ssl' => false,
92
                'filterIncomingConnect' => function () { return true; },
93 10
                'server_maximum_chunk' => 300 * 1024,
94
95 24
                'time_wait_until_first_byte' => 60,
96 10
            ];
97
        }
98
99 24
        function init_ssl_settings() {
100 24
            $this->_stream_context = stream_context_create();
101 24
            if (isset($this->_settings->ssl_server_certificate_file)) {
1 ignored issue
show
Bug introduced by
Accessing ssl_server_certificate_file on the interface NokitaKaze\TestHTTPServer\ServerSettings suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
102
                // @hint Здесь специально не задаётся _settings->is_ssl
103
                $this->stream_set_ssl_option('local_cert', $this->_settings->ssl_server_certificate_file);
1 ignored issue
show
Bug introduced by
Accessing ssl_server_certificate_file on the interface NokitaKaze\TestHTTPServer\ServerSettings suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
104
                $this->stream_set_ssl_option('allow_self_signed', true);
105
                $this->stream_set_ssl_option('verify_peer', false);
106
                if (isset($this->_settings->ssl_server_key_file)) {
1 ignored issue
show
Bug introduced by
Accessing ssl_server_key_file on the interface NokitaKaze\TestHTTPServer\ServerSettings suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
107
                    $this->stream_set_ssl_option('local_pk', $this->_settings->ssl_server_key_file);
1 ignored issue
show
Bug introduced by
Accessing ssl_server_key_file on the interface NokitaKaze\TestHTTPServer\ServerSettings suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
108
                }
109
                $this->stream_set_ssl_option('passphrase',
110
                    isset($this->_settings->ssl_server_key_password) ? $this->_settings->ssl_server_key_password : '');
1 ignored issue
show
Bug introduced by
Accessing ssl_server_key_password on the interface NokitaKaze\TestHTTPServer\ServerSettings suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
111
112
                if (isset($this->_settings->ssl_client_certificate_file)) {
1 ignored issue
show
Bug introduced by
Accessing ssl_client_certificate_file on the interface NokitaKaze\TestHTTPServer\ServerSettings suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
113
                    $this->stream_set_ssl_option('verify_peer', true);
114
                    $this->stream_set_ssl_option('capture_peer_cert', true);
115
                    $this->stream_set_ssl_option('capture_peer_cert_chain', true);
116
                    $this->stream_set_ssl_option('cafile', $this->_settings->ssl_client_certificate_file);
1 ignored issue
show
Bug introduced by
Accessing ssl_client_certificate_file on the interface NokitaKaze\TestHTTPServer\ServerSettings suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
117
                }
118 10
            } elseif (isset($this->_settings->ssl_server_key_file)) {
1 ignored issue
show
Bug introduced by
Accessing ssl_server_key_file on the interface NokitaKaze\TestHTTPServer\ServerSettings suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
119
                throw new Exception('ssl_server_key_file is set, but ssl_server_certificate_file is missing');
120 10
            } elseif (isset($this->_settings->ssl_server_key_password)) {
1 ignored issue
show
Bug introduced by
Accessing ssl_server_key_password on the interface NokitaKaze\TestHTTPServer\ServerSettings suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
121
                throw new Exception('ssl_server_key_password is set, but ssl_server_certificate_file is missing');
122 10
            } elseif (isset($this->_settings->ssl_client_certificate_file)) {
1 ignored issue
show
Bug introduced by
Accessing ssl_client_certificate_file on the interface NokitaKaze\TestHTTPServer\ServerSettings suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
123
                throw new Exception('ssl_client_certificate_file is set, but ssl_server_certificate_file is missing');
124
            }
125 24
        }
126
127
        /**
128
         * Server constructor.
129
         *
130
         * @param ServerSettings|object $settings
131
         */
132 24
        protected static function fix_incoming_settings($settings) {
133 24
            $default_settings = self::get_default_settings();
134 24
            foreach ($default_settings as $key => $value) {
135 24
                if (!isset($settings->{$key})) {
136 24
                    $settings->{$key} = $value;
137 10
                }
138 10
            }
139 24
            foreach (['ListenStart', 'Connect', 'Request', 'Disconnect', 'ListenStop', 'AnyIncomingData', 'HeadIncomingData',
140 10
                      'BodyIncomingData', 'HeadReceived', 'HeadInvalidReceived'] as $event) {
141 24
                if (!isset($settings->{'on'.$event})) {
142 24
                    $settings->{'on'.$event} = null;
143 10
                }
144 10
            }
145 24
        }
146
147
        /**
148
         * @param callable|double $param
149
         * @param callable|null   $tick
150
         */
151 24
        function listen($param, $tick = null) {
152 24
            $this->init_listening();
153 24
            if (is_callable($param)) {
154 24
                $closure = $param;
155 10
            } else {
156
                $closure = function () use ($param) { return (microtime(true) < $param); };
157
            }
158
            /** @noinspection PhpMethodParametersCountMismatchInspection */
159 24
            while ($closure($this)) {
160 24
                $this->listen_tick();
161 24
                if (!is_null($tick)) {
162
                    $tick($this);
163
                }
164 10
            }
165 24
        }
166
167 24
        function init_listening() {
168 24
            if (!is_null($this->_server_socket)) {
169 24
                return;
170
            }
171 24
            $this->_server_socket = stream_socket_server(
172 24
                sprintf('%s://%s:%d',
173 24
                    $this->_settings->is_ssl ? 'ssl' : 'tcp',
174 24
                    is_null($this->_settings->interface) ? '0.0.0.0' : $this->_settings->interface,
175 24
                    $this->_settings->port
176 10
                ),
177 24
                $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN,
178 24
                $this->_stream_context);
179 24
            if (!isset($this->_server_socket)) {
180
                // @codeCoverageIgnoreStart
181
                throw new Exception('Can not create socket ['.$errstr.']', 100 + $errno);
182
                // @codeCoverageIgnoreEnd
183
            }
184
185 24
            $this->event_raise('ListenStart');
186 24
        }
187
188 24
        function listen_tick() {
189 24
            $read = [$this->_server_socket];
190 24
            $write = null;
191 24
            $except = null;
192 24
            if (stream_select($read, $write, $except, 0) === false) {
193
                // @codeCoverageIgnoreStart
194
                throw new Exception('Error on stream select');
195
                // @codeCoverageIgnoreEnd
196
            }
197 24
            if (empty($read)) {
198 24
                $ts_before = microtime(true);
199 24
                $this->process_all_connected_clients();
200 24
                $sleep = $this->_settings->server_sleep_if_no_connect - microtime(true) + $ts_before;
201 24
                if ($sleep > 0) {
202 24
                    usleep($sleep * 1000000);
203 10
                }
204
205 24
                return;
206
            }
207
208 24
            $client = @stream_socket_accept($this->_server_socket);
209 24
            if ($client === false) {
210 9
                $this->event_raise('InvalidConnection');
211
212
                // Если я не запущу process_all_connected_clients, то может быть DDoS через неправильные коннекты
213 9
                $this->process_all_connected_clients();
214
215 9
                return;
216
            }
217
218 24
            if (stream_set_blocking($client, 0) === false) {
219
                // @codeCoverageIgnoreStart
220
                throw new Exception('Can not set socket as non blocking');
221
                // @codeCoverageIgnoreEnd
222
            }
223 24
            $connect_time = microtime(true);
224
            /**
225
             * @var ClientDatum $datum
226
             */
227 24
            $datum = new \stdClass();
228 24
            $datum->status = 0;
229 24
            $datum->client = $client;
230 24
            $datum->connection_time = $connect_time;
231 24
            $datum->blob_request = '';
232 24
            $datum->request_head_params = [];
233 24
            $datum->server = $this;
234 24
            $datum->accepted_hosts = null;
235 24
            $this->event_raise('Connect', $datum);
236 24
            $this->_client_connects[] = $datum;
237
238
            //
239 24
            $this->process_all_connected_clients();
240 24
        }
241
242 9
        function stream_set_ssl_option($key, $value) {
243 9
            stream_context_set_option($this->_stream_context, 'ssl', $key, $value);
244 9
        }
245
246 9
        function set_option($key, $value) {
247 9
            $this->_settings->{$key} = $value;
248 9
        }
249
250
        function get_option($key) {
251
            return $this->_settings->{$key};
252
        }
253
254
        /**
255
         * Первый байт не пришёл
256
         *
257
         * @param object|ClientDatum $connection
258
         *
259
         * @return boolean
260
         */
261 24
        protected function check_timeouts_on_connection_first_byte($connection) {
262 24
            return isset($this->_settings->time_wait_until_first_byte) and
263 24
                   !isset($connection->first_byte_received_time) and
264 24
                   ($connection->connection_time < microtime(true) - $this->_settings->time_wait_until_first_byte);
265
        }
266
267
        /**
268
         * Голова не пришла
269
         *
270
         * @param object|ClientDatum $connection
271
         *
272
         * @return boolean
273
         */
274 24
        protected function check_timeouts_on_connection_head_received($connection) {
275 24
            return isset($this->_settings->time_wait_until_head_received) and
276 17
                   !isset($connection->head_received_time) and
277 24
                   ($connection->connection_time < microtime(true) - $this->_settings->time_wait_until_head_received);
278
        }
279
280
        /**
281
         * Запрос не пришёл
282
         *
283
         * @param object|ClientDatum $connection
284
         *
285
         * @return boolean
286
         */
287 24
        protected function check_timeouts_on_connection_request_received($connection) {
288 24
            return isset($this->_settings->time_wait_until_request_received) and
289 17
                   !isset($connection->full_request_received_time) and
290 24
                   ($connection->connection_time < microtime(true) - $this->_settings->time_wait_until_request_received);
291
        }
292
293
        /**
294
         * Тело не пришло (голова пришла)
295
         *
296
         * @param object|ClientDatum $connection
297
         *
298
         * @return boolean
299
         */
300 24
        protected function check_timeouts_on_connection_body_received_without_head($connection) {
301 24
            return isset($this->_settings->time_between_head_and_body_received, $connection->head_received_time) and
302 17
                   !isset($connection->full_request_received_time) and
303 24
                   ($connection->head_received_time < microtime(true) - $this->_settings->time_between_head_and_body_received);
304
        }
305
306
        /**
307
         * Проверяем слишком старые подключения и убиваем их
308
         *
309
         * @param object|ClientDatum $connection
310
         *
311
         * @return bool
312
         */
313 24
        protected function check_timeouts_on_connection(&$connection) {
314 24
            if ($this->check_timeouts_on_connection_first_byte($connection) or
315 24
                $this->check_timeouts_on_connection_head_received($connection) or
316 24
                $this->check_timeouts_on_connection_request_received($connection) or
317 24
                $this->check_timeouts_on_connection_body_received_without_head($connection)
318 10
            ) {
319
                $this->close_connection($connection);
320
321
                return false;
322
            }
323
324
            // @hint No Slowloris ( https://en.wikipedia.org/wiki/Slowloris_%28computer_security%29 ) test.
325
            // This is not a real server
326
327 24
            return true;
328
        }
329
330
        /**
331
         * Processing all connected clients
332
         */
333 24
        protected function process_all_connected_clients() {
334 24
            if (empty($this->_client_connects)) {
335 24
                return;
336
            }
337
338
            /**
339
             * @var resource[] $read
340
             * @var resource[] $write
341
             * @var resource[] $except
342
             */
343 24
            $read = [];
344 24
            foreach ($this->_client_connects as $connected) {
345 24
                if (is_null($connected)) {
346 24
                    continue;
347
                }
348 24
                if (is_null($connected->client)) {
349
                    $connected = null;
350
                    continue;
351
                }
352
353 24
                if (!is_resource($connected->client)) {
354
                    // @codeCoverageIgnoreStart
355
                    throw new Exception(sprintf('Connection became non resource: %s', (string) $connected->client));
356
                    // @codeCoverageIgnoreEnd
357
                }
358
359
                // Проверяем слишком старые подключения и убиваем их
360 24
                if (!$this->check_timeouts_on_connection($connected)) {
361
                    continue;
362
                }
363
364 24
                $read[] = $connected->client;
365 10
            }
366 24
            if (empty($read)) {
367 24
                $this->_client_connects = [];
368
369 24
                return;
370
            }
371 24
            $write = null;
372 24
            $except = null;
373 24
            if (stream_select($read, $write, $except, 0) === false) {
374
                // @codeCoverageIgnoreStart
375
                throw new Exception('Error on stream select');
376
                // @codeCoverageIgnoreEnd
377
            }
378 24
            unset($connected);
379
380 24
            foreach ($read as &$socket_resource) {
381 24
                foreach ($this->_client_connects as &$connected) {
382 24
                    if (!is_null($connected) and ($connected->client == $socket_resource)) {
383 24
                        $this->process_connected_client($connected);
384 24
                        break;
385
                    }
386 10
                }
387 10
            }
388 24
        }
389
390
        /**
391
         * @param ClientDatum $connect
392
         * @param double      $time
393
         * @param string      $buf
394
         *
395
         * @return boolean
396
         */
397 24
        protected function receive_data_from_connected_client($connect, $time, &$buf) {
398 24
            $buf = @fread($connect->client, self::CHUNK_SIZE);
399 24
            if (empty($buf)) {
400 16
                $this->close_connection($connect);
401
402 16
                return false;
403
            }
404 24
            $connect->last_byte_received_time = $time;
405 24
            if (!isset($connect->first_byte_received_time)) {
406 24
                $connect->first_byte_received_time = $time;
407 10
            }
408 24
            if (strlen($buf) >= self::CHUNK_SIZE) {
409
                do {
410
                    $sub_buf = @fread($connect->client, self::CHUNK_SIZE);
411
                    $buf .= $sub_buf;
412
                    if (isset($this->_settings->server_maximum_chunk) and
413
                        (strlen($buf) >= $this->_settings->server_maximum_chunk)
414
                    ) {
415
                        // Слишком много пришло за один раз
416
                        break;
417
                    }
418
                } while (strlen($sub_buf) >= self::CHUNK_SIZE);
419
            }
420
421 24
            return true;
422
        }
423
424
        /**
425
         * Processing all connected clients
426
         *
427
         * @param ClientDatum $connect
428
         */
429 24
        protected function process_connected_client(&$connect) {
430 24
            $time = microtime(true);
431 24
            $connect->context_options = stream_context_get_options($connect->client);
432 24
            $connect->context_params = stream_context_get_params($connect->client);
433 24
            if (!$this->receive_data_from_connected_client($connect, $time, $buf)) {
434 16
                $this->close_connection($connect);
435
436 16
                return;
437
            }
438 24
            $this->event_raise('AnyIncomingData', $connect, $buf);
439 24
            if ($connect->status == 0) {
440 24
                $this->event_raise('HeadIncomingData', $connect, $buf);
441 10
            } elseif ($connect->status == 1) {
442
                $this->event_raise('BodyIncomingData', $connect, $buf);
443
            }
444 24
            $connect->blob_request .= $buf;
445 24
            if (strpos($connect->blob_request, "\r\n\r\n") === false) {
446
                // Head не дошёл
447
                return;
448
            }
449
450
            // Проверяем на голову
451
            // Голова только-только дошла
452 24
            if (($connect->status == 0) and !$this->process_connected_client_head($connect, $time)) {
453
                return;
454
            }
455
456
            // Проверяем на body
457 24
            if ($connect->status == 1) {
458
                $this->process_connected_client_body($connect, $buf, $time);
459
            }
460 24
            if (is_null($connect) or ($connect->status != 2)) {
461
                return;
462
            }
463
464
            // Проверяем, что Host в списке обрабатываемых
465 24
            if (!$this->check_requested_host_in_accepted_list($connect)) {
466
                return;
467
            }
468
469
            // filterIncomingConnect
470 24
            $closure = $this->_settings->filterIncomingConnect;
471 24
            if (!$closure($this, $connect)) {
472
                $this->close_connection($connect);
473
474
                return;
475
            }
476 24
            unset($closure, $buf);
477
478
            // Request
479 24
            $this->event_raise('Request', $connect);
480
481 24
            if ($connect->status == 3) {
482 24
                $this->close_connection($connect);
483 10
            }
484 24
        }
485
486 24
        function check_requested_host_in_accepted_list($connect) {
487 24
            if (!isset($this->_settings->accepted_hosts)) {
488 24
                return true;
489
            }
490
491
            list($host) = explode(':', $connect->request_head_params['Host']);
492
            if (!in_array(strtolower($host), $this->_settings->accepted_hosts)) {
493
                if ($this->event_raise('HostNotFound', $connect) === false) {
494
                    $this->close_connection($connect);
495
496
                    return false;
497
                }
498
499
                $this->answer($connect, 404, 'Not Found', 'Host not found');
500
                $this->close_connection($connect);
501
502
                return false;
503
            }
504
505
            return true;
506
        }
507
508
        /**
509
         * If returns "false" connection closed
510
         *
511
         * @param ClientDatum $connect
512
         * @param double      $time
513
         *
514
         * @return boolean
515
         */
516 24
        protected function process_connected_client_head(&$connect, $time) {
517 24
            $connect->head_received_time = $time;
518 24
            list($connect->blob_head) = explode("\r\n\r\n", $connect->blob_request, 2);
519 24
            $a = explode("\r\n", $connect->blob_head, 2);
520 24
            list($first_line, $other_lines) = (count($a) == 2) ? $a : [$a[0], ''];
521 24
            if (!preg_match('_^([A-Z]+)\\s+(.+)\\s+HTTP/([0-9.]+)$_', $first_line, $a)) {
522
                if ($this->event_raise('HeadInvalidReceived', $connect, 0) === false) {
523
                    // Подключение сдохло
524
                    $this->close_connection($connect);
525
526
                    return false;
527
                }
528
529
                $this->answer($connect, 400, 'Bad Request', "This is not a HTTP request");
530
                $this->close_connection($connect);
531
532
                return false;
533
            }
534
535 24
            $connect->request_type = $a[1];
536 24
            $connect->request_url = $a[2];
537 24
            $connect->request_http_version = $a[3];
538 24
            if ($connect->request_type == 'POST') {
539
                $connect->status = 1;
540 24
            } elseif ($connect->request_type == 'GET') {
541 24
                $connect->status = 2;
542 24
                $connect->full_request_received_time = $time;
543 10
            } else {
544
                if ($this->event_raise('HeadInvalidReceived', $connect, 1) === false) {
545
                    // Подключение сдохло
546
                    $this->close_connection($connect);
547
548
                    return false;
549
                }
550
551
                $this->answer($connect, 400, 'Bad Request', "Can not process this request type");
552
                $this->close_connection($connect);
553
554
                return false;
555
            }
556
557 24
            $connect->request_head_params = [];
558 24
            foreach (explode("\r\n", $other_lines) as $other_line) {
559 24
                if (empty($other_line) or !preg_match('_^([A-Za-z0-9-]+):\\s*(.*)$_', $other_line, $a)) {
560
                    if ($this->event_raise('HeadInvalidReceived', $connect, 2) === false) {
561
                        // Подключение сдохло
562
                        $this->close_connection($connect);
563
564
                        return false;
565
                    }
566
567
                    $this->answer($connect, 400, 'Bad Request', "Malformed head");
568
                    $this->close_connection($connect);
569
570
                    return false;
571
                }
572
573 24
                $connect->request_head_params[$a[1]] = $a[2];
574 10
            }
575
576 24
            if (!isset($connect->request_head_params['Host'])) {
577
                $this->answer($connect, 400, 'Bad Request', "Field 'host' missed");
578
                $this->close_connection($connect);
579
580
                return false;
581
            }
582
583
            // event
584 24
            $this->event_raise('HeadReceived', $connect);
585
586 24
            return true;
587
        }
588
589
        /**
590
         * If returns "false" connection closed
591
         *
592
         * @param ClientDatum $connect
593
         * @param string      $buf
594
         * @param double      $time
595
         */
596
        protected function process_connected_client_body(&$connect, $buf, $time) {
597
            $head_end = strpos($connect->blob_request, "\r\n\r\n");
598
            if (!isset($connect->request_head_params['Content-Length'])) {
599
                if ($this->event_raise('HeadInvalidReceived', $connect, 3) === false) {
600
                    $this->close_connection($connect);
601
602
                    return;
603
                }
604
605
                $this->answer($connect, 400, 'malformed request', 'Malformed request');
606
                $this->close_connection($connect);
607
608
                return;
609
            }
610
            $requested_body_length = (int) $connect->request_head_params['Content-Length'];
611
            if (strlen($connect->blob_request) >= $head_end + 4 + $requested_body_length) {
612
                $connect->blob_body = substr($connect->blob_request, $head_end + 4, $requested_body_length);
613
                $connect->status = 2;
614
                $connect->body_received_time = $time;
615
                $connect->full_request_received_time = $time;
616
                // @hint Request raised in process_connected_client
617
            }
618
619
            $this->event_raise('BodyIncomingData', $connect, $buf);
620
        }
621
622
        /**
623
         * @param string $event
624
         *
625
         * @return mixed|null
626
         */
627 24
        function event_raise($event) {
628 24
            $method_name = 'on'.$event;
629 24
            if (!isset($this->_settings->{$method_name}) or !is_callable($this->_settings->{$method_name})) {
630 24
                return null;
631
            }
632
633 24
            $args = func_get_args();
634 24
            $args[0] = $this;
635
636 24
            return call_user_func_array($this->_settings->{$method_name}, $args);
637
        }
638
639
        /**
640
         * @param ClientDatum $connect
641
         * @param integer     $code
642
         * @param string      $code_text
643
         * @param string      $body
644
         * @param array       $headers
645
         */
646 24
        function answer($connect, $code, $code_text, $body, array $headers = []) {
647 24
            $buf = sprintf("HTTP/1.0 %d %s\r\n", $code, $code_text);
648 24
            $headers['Content-Length'] = strlen($body);
649 24
            foreach ($headers as $key => &$value) {
650 24
                $buf .= sprintf("%s: %s\r\n", $key, $value);
651 10
            }
652 24
            fwrite($connect->client, "{$buf}\r\n{$body}");
653 24
        }
654
    }
655
656
?>