Subversion Repositories qbpwcf-lib(archive)

Rev

Rev 915 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
1 liveuser 1
<?php
2
namespace Ratchet\RFC6455\Messaging;
3
 
4
class Frame implements FrameInterface {
5
    const OP_CONTINUE =  0;
6
    const OP_TEXT     =  1;
7
    const OP_BINARY   =  2;
8
    const OP_CLOSE    =  8;
9
    const OP_PING     =  9;
10
    const OP_PONG     = 10;
11
 
12
    const CLOSE_NORMAL      = 1000;
13
    const CLOSE_GOING_AWAY  = 1001;
14
    const CLOSE_PROTOCOL    = 1002;
15
    const CLOSE_BAD_DATA    = 1003;
16
    const CLOSE_NO_STATUS   = 1005;
17
    const CLOSE_ABNORMAL    = 1006;
18
    const CLOSE_BAD_PAYLOAD = 1007;
19
    const CLOSE_POLICY      = 1008;
20
    const CLOSE_TOO_BIG     = 1009;
21
    const CLOSE_MAND_EXT    = 1010;
22
    const CLOSE_SRV_ERR     = 1011;
23
    const CLOSE_TLS         = 1015;
24
 
25
    const MASK_LENGTH = 4;
26
 
27
    /**
28
     * The contents of the frame
29
     * @var string
30
     */
31
    protected $data = '';
32
 
33
    /**
34
     * Number of bytes received from the frame
35
     * @var int
36
     */
37
    public $bytesRecvd = 0;
38
 
39
    /**
40
     * Number of bytes in the payload (as per framing protocol)
41
     * @var int
42
     */
43
    protected $defPayLen = -1;
44
 
45
    /**
46
     * If the frame is coalesced this is true
47
     * This is to prevent doing math every time ::isCoalesced is called
48
     * @var boolean
49
     */
50
    private $isCoalesced = false;
51
 
52
    /**
53
     * The unpacked first byte of the frame
54
     * @var int
55
     */
56
    protected $firstByte = -1;
57
 
58
    /**
59
     * The unpacked second byte of the frame
60
     * @var int
61
     */
62
    protected $secondByte = -1;
63
 
64
    /**
65
     * @var callable
66
     * @returns \UnderflowException
67
     */
68
    private $ufeg;
69
 
70
    /**
71
     * @param string|null $payload
72
     * @param bool        $final
73
     * @param int         $opcode
74
     * @param callable<\UnderflowException> $ufExceptionFactory
75
     */
76
    public function __construct($payload = null, $final = true, $opcode = 1, callable $ufExceptionFactory = null) {
77
        $this->ufeg = $ufExceptionFactory ?: static function($msg = '') {
78
            return new \UnderflowException($msg);
79
        };
80
 
81
        if (null === $payload) {
82
            return;
83
        }
84
 
85
        $this->defPayLen   = strlen($payload);
86
        $this->firstByte   = ($final ? 128 : 0) + $opcode;
87
        $this->secondByte  = $this->defPayLen;
88
        $this->isCoalesced = true;
89
 
90
        $ext = '';
91
        if ($this->defPayLen > 65535) {
92
            $ext = pack('NN', 0, $this->defPayLen);
93
            $this->secondByte = 127;
94
        } elseif ($this->defPayLen > 125) {
95
            $ext = pack('n', $this->defPayLen);
96
            $this->secondByte = 126;
97
        }
98
 
99
        $this->data       = chr($this->firstByte) . chr($this->secondByte) . $ext . $payload;
100
        $this->bytesRecvd = 2 + strlen($ext) + $this->defPayLen;
101
    }
102
 
103
    /**
104
     * {@inheritdoc}
105
     */
106
    public function isCoalesced() {
107
        if (true === $this->isCoalesced) {
108
            return true;
109
        }
110
 
111
        try {
112
            $payload_length = $this->getPayloadLength();
113
            $payload_start  = $this->getPayloadStartingByte();
114
        } catch (\UnderflowException $e) {
115
            return false;
116
        }
117
 
118
        $this->isCoalesced = $this->bytesRecvd >= $payload_length + $payload_start;
119
 
120
        return $this->isCoalesced;
121
    }
122
 
123
    /**
124
     * {@inheritdoc}
125
     */
126
    public function addBuffer($buf) {
127
        $len = strlen($buf);
128
 
129
        $this->data       .= $buf;
130
        $this->bytesRecvd += $len;
131
 
132
        if ($this->firstByte === -1 && $this->bytesRecvd !== 0) {
133
            $this->firstByte = ord($this->data[0]);
134
        }
135
 
136
        if ($this->secondByte === -1 && $this->bytesRecvd >= 2) {
137
            $this->secondByte = ord($this->data[1]);
138
        }
139
    }
140
 
141
    /**
142
     * {@inheritdoc}
143
     */
144
    public function isFinal() {
145
        if (-1 === $this->firstByte) {
146
            throw call_user_func($this->ufeg, 'Not enough bytes received to determine if this is the final frame in message');
147
        }
148
 
149
        return 128 === ($this->firstByte & 128);
150
    }
151
 
152
    public function setRsv1($value = true) {
153
        if (strlen($this->data) == 0) {
154
            throw new \UnderflowException("Cannot set Rsv1 because there is no data.");
155
        }
156
 
157
        $this->firstByte =
158
            ($this->isFinal() ? 128 : 0)
159
            + $this->getOpcode()
160
            + ($value ? 64 : 0)
161
            + ($this->getRsv2() ? 32 : 0)
162
            + ($this->getRsv3() ? 16 : 0)
163
        ;
164
 
165
        $this->data[0] = chr($this->firstByte);
166
        return $this;
167
    }
168
 
169
    /**
170
     * @return boolean
171
     * @throws \UnderflowException
172
     */
173
    public function getRsv1() {
174
        if (-1 === $this->firstByte) {
175
            throw call_user_func($this->ufeg, 'Not enough bytes received to determine reserved bit');
176
        }
177
 
178
        return 64 === ($this->firstByte & 64);
179
    }
180
 
181
    /**
182
     * @return boolean
183
     * @throws \UnderflowException
184
     */
185
    public function getRsv2() {
186
        if (-1 === $this->firstByte) {
187
            throw call_user_func($this->ufeg, 'Not enough bytes received to determine reserved bit');
188
        }
189
 
190
        return 32 === ($this->firstByte & 32);
191
    }
192
 
193
    /**
194
     * @return boolean
195
     * @throws \UnderflowException
196
     */
197
    public function getRsv3() {
198
        if (-1 === $this->firstByte) {
199
            throw call_user_func($this->ufeg, 'Not enough bytes received to determine reserved bit');
200
        }
201
 
202
        return 16 === ($this->firstByte & 16);
203
    }
204
 
205
    /**
206
     * {@inheritdoc}
207
     */
208
    public function isMasked() {
209
        if (-1 === $this->secondByte) {
210
            throw call_user_func($this->ufeg, "Not enough bytes received ({$this->bytesRecvd}) to determine if mask is set");
211
        }
212
 
213
        return 128 === ($this->secondByte & 128);
214
    }
215
 
216
    /**
217
     * {@inheritdoc}
218
     */
219
    public function getMaskingKey() {
220
        if (!$this->isMasked()) {
221
            return '';
222
        }
223
 
224
        $start  = 1 + $this->getNumPayloadBytes();
225
 
226
        if ($this->bytesRecvd < $start + static::MASK_LENGTH) {
227
            throw call_user_func($this->ufeg, 'Not enough data buffered to calculate the masking key');
228
        }
229
 
230
        return substr($this->data, $start, static::MASK_LENGTH);
231
    }
232
 
233
    /**
234
     * Create a 4 byte masking key
235
     * @return string
236
     */
237
    public function generateMaskingKey() {
238
        $mask = '';
239
 
240
        for ($i = 1; $i <= static::MASK_LENGTH; $i++) {
241
            $mask .= chr(rand(32, 126));
242
        }
243
 
244
        return $mask;
245
    }
246
 
247
    /**
248
     * Apply a mask to the payload
249
     * @param string|null If NULL is passed a masking key will be generated
250
     * @throws \OutOfBoundsException
251
     * @throws \InvalidArgumentException If there is an issue with the given masking key
252
     * @return Frame
253
     */
254
    public function maskPayload($maskingKey = null) {
255
        if (null === $maskingKey) {
256
            $maskingKey = $this->generateMaskingKey();
257
        }
258
 
259
        if (static::MASK_LENGTH !== strlen($maskingKey)) {
260
            throw new \InvalidArgumentException("Masking key must be " . static::MASK_LENGTH ." characters");
261
        }
262
 
263
        if (extension_loaded('mbstring') && true !== mb_check_encoding($maskingKey, 'US-ASCII')) {
264
            throw new \OutOfBoundsException("Masking key MUST be ASCII");
265
        }
266
 
267
        $this->unMaskPayload();
268
 
269
        $this->secondByte = $this->secondByte | 128;
270
        $this->data[1]    = chr($this->secondByte);
271
 
272
        $this->data = substr_replace($this->data, $maskingKey, $this->getNumPayloadBytes() + 1, 0);
273
 
274
        $this->bytesRecvd += static::MASK_LENGTH;
275
        $this->data        = substr_replace($this->data, $this->applyMask($maskingKey), $this->getPayloadStartingByte(), $this->getPayloadLength());
276
 
277
        return $this;
278
    }
279
 
280
    /**
281
     * Remove a mask from the payload
282
     * @throws \UnderFlowException If the frame is not coalesced
283
     * @return Frame
284
     */
285
    public function unMaskPayload() {
286
        if (!$this->isCoalesced()) {
287
            throw call_user_func($this->ufeg, 'Frame must be coalesced before applying mask');
288
        }
289
 
290
        if (!$this->isMasked()) {
291
            return $this;
292
        }
293
 
294
        $maskingKey = $this->getMaskingKey();
295
 
296
        $this->secondByte = $this->secondByte & ~128;
297
        $this->data[1] = chr($this->secondByte);
298
 
299
        $this->data = substr_replace($this->data, '', $this->getNumPayloadBytes() + 1, static::MASK_LENGTH);
300
 
301
        $this->bytesRecvd -= static::MASK_LENGTH;
302
        $this->data        = substr_replace($this->data, $this->applyMask($maskingKey), $this->getPayloadStartingByte(), $this->getPayloadLength());
303
 
304
        return $this;
305
    }
306
 
307
    /**
308
     * Apply a mask to a string or the payload of the instance
309
     * @param string $maskingKey   The 4 character masking key to be applied
310
     * @param string|null $payload A string to mask or null to use the payload
311
     * @throws \UnderflowException If using the payload but enough hasn't been buffered
312
     * @return string              The masked string
313
     */
314
    public function applyMask($maskingKey, $payload = null) {
315
        if (null === $payload) {
316
            if (!$this->isCoalesced()) {
317
                throw call_user_func($this->ufeg, 'Frame must be coalesced to apply a mask');
318
            }
319
 
320
            $payload = substr($this->data, $this->getPayloadStartingByte(), $this->getPayloadLength());
321
        }
322
 
323
        $len = strlen($payload);
324
 
325
        if (0 === $len) {
326
            return '';
327
        }
328
 
329
        return $payload ^ str_pad('', $len, $maskingKey, STR_PAD_RIGHT);
330
 
331
        // TODO: Remove this before publish - keeping methods here to compare performance (above is faster but need control against v0.3.3)
332
 
333
        $applied = '';
334
        for ($i = 0, $len = strlen($payload); $i < $len; $i++) {
335
            $applied .= $payload[$i] ^ $maskingKey[$i % static::MASK_LENGTH];
336
        }
337
 
338
        return $applied;
339
    }
340
 
341
    /**
342
     * {@inheritdoc}
343
     */
344
    public function getOpcode() {
345
        if (-1 === $this->firstByte) {
346
            throw call_user_func($this->ufeg, 'Not enough bytes received to determine opcode');
347
        }
348
 
349
        return ($this->firstByte & ~240);
350
    }
351
 
352
    /**
353
     * Gets the decimal value of bits 9 (10th) through 15 inclusive
354
     * @return int
355
     * @throws \UnderflowException If the buffer doesn't have enough data to determine this
356
     */
357
    protected function getFirstPayloadVal() {
358
        if (-1 === $this->secondByte) {
359
            throw call_user_func($this->ufeg, 'Not enough bytes received');
360
        }
361
 
362
        return $this->secondByte & 127;
363
    }
364
 
365
    /**
366
     * @return int (7|23|71) Number of bits defined for the payload length in the fame
367
     * @throws \UnderflowException
368
     */
369
    protected function getNumPayloadBits() {
370
        if (-1 === $this->secondByte) {
371
            throw call_user_func($this->ufeg, 'Not enough bytes received');
372
        }
373
 
374
        // By default 7 bits are used to describe the payload length
375
        // These are bits 9 (10th) through 15 inclusive
376
        $bits = 7;
377
 
378
        // Get the value of those bits
379
        $check = $this->getFirstPayloadVal();
380
 
381
        // If the value is 126 the 7 bits plus the next 16 are used to describe the payload length
382
        if ($check >= 126) {
383
            $bits += 16;
384
        }
385
 
386
        // If the value of the initial payload length are is 127 an additional 48 bits are used to describe length
387
        // Note: The documentation specifies the length is to be 63 bits, but I think that's a typo and is 64 (16+48)
388
        if ($check === 127) {
389
            $bits += 48;
390
        }
391
 
392
        return $bits;
393
    }
394
 
395
    /**
396
     * This just returns the number of bytes used in the frame to describe the payload length (as opposed to # of bits)
397
     * @see getNumPayloadBits
398
     */
399
    protected function getNumPayloadBytes() {
400
        return (1 + $this->getNumPayloadBits()) / 8;
401
    }
402
 
403
    /**
404
     * {@inheritdoc}
405
     */
406
    public function getPayloadLength() {
407
        if ($this->defPayLen !== -1) {
408
            return $this->defPayLen;
409
        }
410
 
411
        $this->defPayLen = $this->getFirstPayloadVal();
412
        if ($this->defPayLen <= 125) {
413
            return $this->getPayloadLength();
414
        }
415
 
416
        $byte_length = $this->getNumPayloadBytes();
417
        if ($this->bytesRecvd < 1 + $byte_length) {
418
            $this->defPayLen = -1;
419
            throw call_user_func($this->ufeg, 'Not enough data buffered to determine payload length');
420
        }
421
 
422
        $len = 0;
423
        for ($i = 2; $i <= $byte_length; $i++) {
424
            $len <<= 8;
425
            $len  += ord($this->data[$i]);
426
        }
427
 
428
        $this->defPayLen = $len;
429
 
430
        return $this->getPayloadLength();
431
    }
432
 
433
    /**
434
     * {@inheritdoc}
435
     */
436
    public function getPayloadStartingByte() {
437
        return 1 + $this->getNumPayloadBytes() + ($this->isMasked() ? static::MASK_LENGTH : 0);
438
    }
439
 
440
    /**
441
     * {@inheritdoc}
442
     * @todo Consider not checking mask, always returning the payload, masked or not
443
     */
444
    public function getPayload() {
445
        if (!$this->isCoalesced()) {
446
            throw call_user_func($this->ufeg, 'Can not return partial message');
447
        }
448
 
449
        return $this->__toString();
450
    }
451
 
452
    /**
453
     * Get the raw contents of the frame
454
     * @todo This is untested, make sure the substr is right - trying to return the frame w/o the overflow
455
     */
456
    public function getContents() {
457
        return substr($this->data, 0, $this->getPayloadStartingByte() + $this->getPayloadLength());
458
    }
459
 
460
    public function __toString() {
461
        $payload = (string)substr($this->data, $this->getPayloadStartingByte(), $this->getPayloadLength());
462
 
463
        if ($this->isMasked()) {
464
            $payload = $this->applyMask($this->getMaskingKey(), $payload);
465
        }
466
 
467
        return $payload;
468
    }
469
 
470
    /**
471
     * Sometimes clients will concatenate more than one frame over the wire
472
     * This method will take the extra bytes off the end and return them
473
     * @return string
474
     */
475
    public function extractOverflow() {
476
        if ($this->isCoalesced()) {
477
            $endPoint  = $this->getPayloadLength();
478
            $endPoint += $this->getPayloadStartingByte();
479
 
480
            if ($this->bytesRecvd > $endPoint) {
481
                $overflow   = substr($this->data, $endPoint);
482
                $this->data = substr($this->data, 0, $endPoint);
483
 
484
                return $overflow;
485
            }
486
        }
487
 
488
        return '';
489
    }
490
}