Subversion Repositories php-qbpwcf

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
3 liveuser 1
<?php
2
namespace Ratchet\RFC6455\Messaging;
3
 
4
use Ratchet\RFC6455\Handshake\PermessageDeflateOptions;
5
 
6
class MessageBuffer {
7
    /**
8
     * @var \Ratchet\RFC6455\Messaging\CloseFrameChecker
9
     */
10
    private $closeFrameChecker;
11
 
12
    /**
13
     * @var callable
14
     */
15
    private $exceptionFactory;
16
 
17
    /**
18
     * @var \Ratchet\RFC6455\Messaging\Message
19
     */
20
    private $messageBuffer;
21
 
22
    /**
23
     * @var \Ratchet\RFC6455\Messaging\Frame
24
     */
25
    private $frameBuffer;
26
 
27
    /**
28
     * @var callable
29
     */
30
    private $onMessage;
31
 
32
    /**
33
     * @var callable
34
     */
35
    private $onControl;
36
 
37
    /**
38
     * @var bool
39
     */
40
    private $checkForMask;
41
 
42
    /**
43
     * @var callable
44
     */
45
    private $sender;
46
 
47
    /**
48
     * @var string
49
     */
50
    private $leftovers;
51
 
52
    /**
53
     * @var int
54
     */
55
    private $streamingMessageOpCode = -1;
56
 
57
    /**
58
     * @var PermessageDeflateOptions
59
     */
60
    private $permessageDeflateOptions;
61
 
62
    /**
63
     * @var bool
64
     */
65
    private $deflateEnabled = false;
66
 
67
    /**
68
     * @var int
69
     */
70
    private $maxMessagePayloadSize;
71
 
72
    /**
73
     * @var int
74
     */
75
    private $maxFramePayloadSize;
76
 
77
    /**
78
     * @var bool
79
     */
80
    private $compressedMessage;
81
 
82
    function __construct(
83
        CloseFrameChecker $frameChecker,
84
        callable $onMessage,
85
        callable $onControl = null,
86
        $expectMask = true,
87
        $exceptionFactory = null,
88
        $maxMessagePayloadSize = null, // null for default - zero for no limit
89
        $maxFramePayloadSize = null,   // null for default - zero for no limit
90
        callable $sender = null,
91
        PermessageDeflateOptions $permessageDeflateOptions = null
92
    ) {
93
        $this->closeFrameChecker = $frameChecker;
94
        $this->checkForMask = (bool)$expectMask;
95
 
96
        $this->exceptionFactory ?: $exceptionFactory = function($msg) {
97
            return new \UnderflowException($msg);
98
        };
99
 
100
        $this->onMessage = $onMessage;
101
        $this->onControl = $onControl ?: function() {};
102
 
103
        $this->sender = $sender;
104
 
105
        $this->permessageDeflateOptions = $permessageDeflateOptions ?: PermessageDeflateOptions::createDisabled();
106
 
107
        $this->deflateEnabled = $this->permessageDeflateOptions->isEnabled();
108
 
109
        if ($this->deflateEnabled && !is_callable($this->sender)) {
110
            throw new \InvalidArgumentException('sender must be set when deflate is enabled');
111
        }
112
 
113
        $this->compressedMessage = false;
114
 
115
        $this->leftovers = '';
116
 
117
        $memory_limit_bytes = static::getMemoryLimit();
118
 
119
        if ($maxMessagePayloadSize === null) {
120
            $maxMessagePayloadSize = $memory_limit_bytes / 4;
121
        }
122
        if ($maxFramePayloadSize === null) {
123
            $maxFramePayloadSize = $memory_limit_bytes / 4;
124
        }
125
 
126
        if (!is_int($maxFramePayloadSize) || $maxFramePayloadSize > 0x7FFFFFFFFFFFFFFF || $maxFramePayloadSize < 0) { // this should be interesting on non-64 bit systems
127
            throw new \InvalidArgumentException($maxFramePayloadSize . ' is not a valid maxFramePayloadSize');
128
        }
129
        $this->maxFramePayloadSize = $maxFramePayloadSize;
130
 
131
        if (!is_int($maxMessagePayloadSize) || $maxMessagePayloadSize > 0x7FFFFFFFFFFFFFFF || $maxMessagePayloadSize < 0) {
132
            throw new \InvalidArgumentException($maxMessagePayloadSize . 'is not a valid maxMessagePayloadSize');
133
        }
134
        $this->maxMessagePayloadSize = $maxMessagePayloadSize;
135
    }
136
 
137
    public function onData($data) {
138
        $data = $this->leftovers . $data;
139
        $dataLen = strlen($data);
140
 
141
        if ($dataLen < 2) {
142
            $this->leftovers = $data;
143
 
144
            return;
145
        }
146
 
147
        $frameStart = 0;
148
        while ($frameStart + 2 <= $dataLen) {
149
            $headerSize     = 2;
150
            $payload_length = unpack('C', $data[$frameStart + 1] & "\x7f")[1];
151
            $isMasked       = ($data[$frameStart + 1] & "\x80") === "\x80";
152
            $headerSize     += $isMasked ? 4 : 0;
153
            if ($payload_length > 125 && ($dataLen - $frameStart < $headerSize + 125)) {
154
                // no point of checking - this frame is going to be bigger than the buffer is right now
155
                break;
156
            }
157
            if ($payload_length > 125) {
158
                $payloadLenBytes = $payload_length === 126 ? 2 : 8;
159
                $headerSize      += $payloadLenBytes;
160
                $bytesToUpack    = substr($data, $frameStart + 2, $payloadLenBytes);
161
                $payload_length  = $payload_length === 126
162
                    ? unpack('n', $bytesToUpack)[1]
163
                    : unpack('J', $bytesToUpack)[1];
164
            }
165
 
166
            $closeFrame = null;
167
 
168
            if ($payload_length < 0) {
169
                // this can happen when unpacking in php
170
                $closeFrame = $this->newCloseFrame(Frame::CLOSE_PROTOCOL, 'Invalid frame length');
171
            }
172
 
173
            if (!$closeFrame && $this->maxFramePayloadSize > 1 && $payload_length > $this->maxFramePayloadSize) {
174
                $closeFrame = $this->newCloseFrame(Frame::CLOSE_TOO_BIG, 'Maximum frame size exceeded');
175
            }
176
 
177
            if (!$closeFrame && $this->maxMessagePayloadSize > 0
178
                && $payload_length + ($this->messageBuffer ? $this->messageBuffer->getPayloadLength() : 0) > $this->maxMessagePayloadSize) {
179
                $closeFrame = $this->newCloseFrame(Frame::CLOSE_TOO_BIG, 'Maximum message size exceeded');
180
            }
181
 
182
            if ($closeFrame !== null) {
183
                $onControl = $this->onControl;
184
                $onControl($closeFrame);
185
                $this->leftovers = '';
186
 
187
                return;
188
            }
189
 
190
            $isCoalesced = $dataLen - $frameStart >= $payload_length + $headerSize;
191
            if (!$isCoalesced) {
192
                break;
193
            }
194
            $this->processData(substr($data, $frameStart, $payload_length + $headerSize));
195
            $frameStart = $frameStart + $payload_length + $headerSize;
196
        }
197
 
198
        $this->leftovers = substr($data, $frameStart);
199
    }
200
 
201
    /**
202
     * @param string $data
203
     * @return null
204
     */
205
    private function processData($data) {
206
        $this->messageBuffer ?: $this->messageBuffer = $this->newMessage();
207
        $this->frameBuffer   ?: $this->frameBuffer   = $this->newFrame();
208
 
209
        $this->frameBuffer->addBuffer($data);
210
 
211
        $onMessage = $this->onMessage;
212
        $onControl = $this->onControl;
213
 
214
        $this->frameBuffer = $this->frameCheck($this->frameBuffer);
215
 
216
        $this->frameBuffer->unMaskPayload();
217
 
218
        $opcode = $this->frameBuffer->getOpcode();
219
 
220
        if ($opcode > 2) {
221
            $onControl($this->frameBuffer, $this);
222
 
223
            if (Frame::OP_CLOSE === $opcode) {
224
                return '';
225
            }
226
        } else {
227
            if ($this->messageBuffer->count() === 0 && $this->frameBuffer->getRsv1()) {
228
                $this->compressedMessage = true;
229
            }
230
            if ($this->compressedMessage) {
231
                $this->frameBuffer = $this->inflateFrame($this->frameBuffer);
232
            }
233
 
234
            $this->messageBuffer->addFrame($this->frameBuffer);
235
        }
236
 
237
        $this->frameBuffer = null;
238
 
239
        if ($this->messageBuffer->isCoalesced()) {
240
            $msgCheck = $this->checkMessage($this->messageBuffer);
241
 
242
            $msgBuffer = $this->messageBuffer;
243
            $this->messageBuffer = null;
244
 
245
            if (true !== $msgCheck) {
246
                $onControl($this->newCloseFrame($msgCheck, 'Ratchet detected an invalid UTF-8 payload'), $this);
247
            } else {
248
                $onMessage($msgBuffer, $this);
249
            }
250
 
251
            $this->messageBuffer = null;
252
            $this->compressedMessage = false;
253
 
254
            if ($this->permessageDeflateOptions->getServerNoContextTakeover()) {
255
                $this->inflator = null;
256
            }
257
        }
258
    }
259
 
260
    /**
261
     * Check a frame to be added to the current message buffer
262
     * @param \Ratchet\RFC6455\Messaging\FrameInterface|FrameInterface $frame
263
     * @return \Ratchet\RFC6455\Messaging\FrameInterface|FrameInterface
264
     */
265
    public function frameCheck(FrameInterface $frame) {
266
        if ((false !== $frame->getRsv1() && !$this->deflateEnabled) ||
267
            false !== $frame->getRsv2() ||
268
            false !== $frame->getRsv3()
269
        ) {
270
            return $this->newCloseFrame(Frame::CLOSE_PROTOCOL, 'Ratchet detected an invalid reserve code');
271
        }
272
 
273
        if ($this->checkForMask && !$frame->isMasked()) {
274
            return $this->newCloseFrame(Frame::CLOSE_PROTOCOL, 'Ratchet detected an incorrect frame mask');
275
        }
276
 
277
        $opcode = $frame->getOpcode();
278
 
279
        if ($opcode > 2) {
280
            if ($frame->getPayloadLength() > 125 || !$frame->isFinal()) {
281
                return $this->newCloseFrame(Frame::CLOSE_PROTOCOL, 'Ratchet detected a mismatch between final bit and indicated payload length');
282
            }
283
 
284
            switch ($opcode) {
285
                case Frame::OP_CLOSE:
286
                    $closeCode = 0;
287
 
288
                    $bin = $frame->getPayload();
289
 
290
                    if (empty($bin)) {
291
                        return $this->newCloseFrame(Frame::CLOSE_NORMAL);
292
                    }
293
 
294
                    if (strlen($bin) === 1) {
295
                        return $this->newCloseFrame(Frame::CLOSE_PROTOCOL, 'Ratchet detected an invalid close code');
296
                    }
297
 
298
                    if (strlen($bin) >= 2) {
299
                        list($closeCode) = array_merge(unpack('n*', substr($bin, 0, 2)));
300
                    }
301
 
302
                    $checker = $this->closeFrameChecker;
303
                    if (!$checker($closeCode)) {
304
                        return $this->newCloseFrame(Frame::CLOSE_PROTOCOL, 'Ratchet detected an invalid close code');
305
                    }
306
 
307
                    if (!$this->checkUtf8(substr($bin, 2))) {
308
                        return $this->newCloseFrame(Frame::CLOSE_BAD_PAYLOAD, 'Ratchet detected an invalid UTF-8 payload in the close reason');
309
                    }
310
 
311
                    return $frame;
312
                    break;
313
                case Frame::OP_PING:
314
                case Frame::OP_PONG:
315
                    break;
316
                default:
317
                    return $this->newCloseFrame(Frame::CLOSE_PROTOCOL, 'Ratchet detected an invalid OP code');
318
                    break;
319
            }
320
 
321
            return $frame;
322
        }
323
 
324
        if (Frame::OP_CONTINUE === $frame->getOpcode() && 0 === count($this->messageBuffer)) {
325
            return $this->newCloseFrame(Frame::CLOSE_PROTOCOL, 'Ratchet detected the first frame of a message was a continue');
326
        }
327
 
328
        if (count($this->messageBuffer) > 0 && Frame::OP_CONTINUE !== $frame->getOpcode()) {
329
            return $this->newCloseFrame(Frame::CLOSE_PROTOCOL, 'Ratchet detected invalid OP code when expecting continue frame');
330
        }
331
 
332
        return $frame;
333
    }
334
 
335
    /**
336
     * Determine if a message is valid
337
     * @param \Ratchet\RFC6455\Messaging\MessageInterface
338
     * @return bool|int true if valid - false if incomplete - int of recommended close code
339
     */
340
    public function checkMessage(MessageInterface $message) {
341
        if (!$message->isBinary()) {
342
            if (!$this->checkUtf8($message->getPayload())) {
343
                return Frame::CLOSE_BAD_PAYLOAD;
344
            }
345
        }
346
 
347
        return true;
348
    }
349
 
350
    private function checkUtf8($string) {
351
        if (extension_loaded('mbstring')) {
352
            return mb_check_encoding($string, 'UTF-8');
353
        }
354
 
355
        return preg_match('//u', $string);
356
    }
357
 
358
    /**
359
     * @return \Ratchet\RFC6455\Messaging\MessageInterface
360
     */
361
    public function newMessage() {
362
        return new Message;
363
    }
364
 
365
    /**
366
     * @param string|null $payload
367
     * @param bool|null   $final
368
     * @param int|null    $opcode
369
     * @return \Ratchet\RFC6455\Messaging\FrameInterface
370
     */
371
    public function newFrame($payload = null, $final = null, $opcode = null) {
372
        return new Frame($payload, $final, $opcode, $this->exceptionFactory);
373
    }
374
 
375
    public function newCloseFrame($code, $reason = '') {
376
        return $this->newFrame(pack('n', $code) . $reason, true, Frame::OP_CLOSE);
377
    }
378
 
379
    public function sendFrame(Frame $frame) {
380
        if ($this->sender === null) {
381
            throw new \Exception('To send frames using the MessageBuffer, sender must be set.');
382
        }
383
 
384
        if ($this->deflateEnabled &&
385
            ($frame->getOpcode() === Frame::OP_TEXT || $frame->getOpcode() === Frame::OP_BINARY)) {
386
            $frame = $this->deflateFrame($frame);
387
        }
388
 
389
        if (!$this->checkForMask) {
390
            $frame->maskPayload();
391
        }
392
 
393
        $sender = $this->sender;
394
        $sender($frame->getContents());
395
    }
396
 
397
    public function sendMessage($messagePayload, $final = true, $isBinary = false) {
398
        $opCode = $isBinary ? Frame::OP_BINARY : Frame::OP_TEXT;
399
        if ($this->streamingMessageOpCode === -1) {
400
            $this->streamingMessageOpCode = $opCode;
401
        }
402
 
403
        if ($this->streamingMessageOpCode !== $opCode) {
404
            throw new \Exception('Binary and text message parts cannot be streamed together.');
405
        }
406
 
407
        $frame = $this->newFrame($messagePayload, $final, $opCode);
408
 
409
        $this->sendFrame($frame);
410
 
411
        if ($final) {
412
            // reset deflator if client doesn't remember contexts
413
            if ($this->getDeflateNoContextTakeover()) {
414
                $this->deflator = null;
415
            }
416
            $this->streamingMessageOpCode = -1;
417
        }
418
    }
419
 
420
    private $inflator;
421
 
422
    private function getDeflateNoContextTakeover() {
423
        return $this->checkForMask ?
424
            $this->permessageDeflateOptions->getServerNoContextTakeover() :
425
            $this->permessageDeflateOptions->getClientNoContextTakeover();
426
    }
427
 
428
    private function getDeflateWindowBits() {
429
        return $this->checkForMask ? $this->permessageDeflateOptions->getServerMaxWindowBits() : $this->permessageDeflateOptions->getClientMaxWindowBits();
430
    }
431
 
432
    private function getInflateNoContextTakeover() {
433
        return $this->checkForMask ?
434
            $this->permessageDeflateOptions->getClientNoContextTakeover() :
435
            $this->permessageDeflateOptions->getServerNoContextTakeover();
436
    }
437
 
438
    private function getInflateWindowBits() {
439
        return $this->checkForMask ? $this->permessageDeflateOptions->getClientMaxWindowBits() : $this->permessageDeflateOptions->getServerMaxWindowBits();
440
    }
441
 
442
    private function inflateFrame(Frame $frame) {
443
        if ($this->inflator === null) {
444
            $this->inflator = inflate_init(
445
                ZLIB_ENCODING_RAW,
446
                [
447
                    'level'    => -1,
448
                    'memory'   => 8,
449
                    'window'   => $this->getInflateWindowBits(),
450
                    'strategy' => ZLIB_DEFAULT_STRATEGY
451
                ]
452
            );
453
        }
454
 
455
        $terminator = '';
456
        if ($frame->isFinal()) {
457
            $terminator = "\x00\x00\xff\xff";
458
        }
459
 
460
        gc_collect_cycles(); // memory runs away if we don't collect ??
461
 
462
        return new Frame(
463
            inflate_add($this->inflator, $frame->getPayload() . $terminator),
464
            $frame->isFinal(),
465
            $frame->getOpcode()
466
        );
467
    }
468
 
469
    private $deflator;
470
 
471
    private function deflateFrame(Frame $frame)
472
    {
473
        if ($frame->getRsv1()) {
474
            return $frame; // frame is already deflated
475
        }
476
 
477
        if ($this->deflator === null) {
478
            $bits = (int)$this->getDeflateWindowBits();
479
            if ($bits === 8) {
480
                $bits = 9;
481
            }
482
            $this->deflator = deflate_init(
483
                ZLIB_ENCODING_RAW,
484
                [
485
                    'level'    => -1,
486
                    'memory'   => 8,
487
                    'window'   => $bits,
488
                    'strategy' => ZLIB_DEFAULT_STRATEGY
489
                ]
490
            );
491
        }
492
 
493
        // there is an issue in the zlib extension for php where
494
        // deflate_add does not check avail_out to see if the buffer filled
495
        // this only seems to be an issue for payloads between 16 and 64 bytes
496
        // This if statement is a hack fix to break the output up allowing us
497
        // to call deflate_add twice which should clear the buffer issue
498
//        if ($frame->getPayloadLength() >= 16 && $frame->getPayloadLength() <= 64) {
499
//            // try processing in 8 byte chunks
500
//            // https://bugs.php.net/bug.php?id=73373
501
//            $payload = "";
502
//            $orig = $frame->getPayload();
503
//            $partSize = 8;
504
//            while (strlen($orig) > 0) {
505
//                $part = substr($orig, 0, $partSize);
506
//                $orig = substr($orig, strlen($part));
507
//                $flags = strlen($orig) > 0 ? ZLIB_PARTIAL_FLUSH : ZLIB_SYNC_FLUSH;
508
//                $payload .= deflate_add($this->deflator, $part, $flags);
509
//            }
510
//        } else {
511
        $payload = deflate_add(
512
            $this->deflator,
513
            $frame->getPayload(),
514
            ZLIB_SYNC_FLUSH
515
        );
516
//        }
517
 
518
        $deflatedFrame = new Frame(
519
            substr($payload, 0, $frame->isFinal() ? -4 : strlen($payload)),
520
            $frame->isFinal(),
521
            $frame->getOpcode()
522
        );
523
 
524
        if ($frame->isFinal()) {
525
            $deflatedFrame->setRsv1();
526
        }
527
 
528
        return $deflatedFrame;
529
    }
530
 
531
    /**
532
     * This is a separate function for testing purposes
533
     * $memory_limit is only used for testing
534
     *
535
     * @param null|string $memory_limit
536
     * @return int
537
     */
538
    private static function getMemoryLimit($memory_limit = null) {
539
        $memory_limit = $memory_limit === null ? \trim(\ini_get('memory_limit')) : $memory_limit;
540
        $memory_limit_bytes = 0;
541
        if ($memory_limit !== '') {
542
            $shifty = ['k' => 0, 'm' => 10, 'g' => 20];
543
            $multiplier = strlen($memory_limit) > 1 ? substr(strtolower($memory_limit), -1) : '';
544
            $memory_limit = (int)$memory_limit;
545
            $memory_limit_bytes = in_array($multiplier, array_keys($shifty), true) ? $memory_limit * 1024 << $shifty[$multiplier] : $memory_limit;
546
        }
547
 
548
        return $memory_limit_bytes < 0 ? 0 : $memory_limit_bytes;
549
    }
550
}