• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

sirn-se / websocket-php / 5608975860

pending completion
5608975860

push

github

Sören Jensen
Middleware support

90 of 90 new or added lines in 8 files covered. (100.0%)

245 of 671 relevant lines covered (36.51%)

1.27 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

74.68
/src/Frame/FrameHandler.php
1
<?php
2

3
/**
4
 * Copyright (C) 2014-2023 Textalk and contributors.
5
 *
6
 * This file is part of Websocket PHP and is free software under the ISC License.
7
 * License text: https://raw.githubusercontent.com/sirn-se/websocket-php/master/COPYING.md
8
 */
9

10
namespace WebSocket\Frame;
11

12
use Phrity\Net\SocketStream;
13
use Psr\Log\{
14
    LoggerInterface,
15
    LoggerAwareInterface,
16
    NullLogger
17
};
18
use RuntimeException;
19
use WebSocket\OpcodeTrait;
20

21
/**
22
 * WebSocket\Frame\FrameHandler class.
23
 * Reads and writes Frames on stream.
24
 */
25
class FrameHandler implements LoggerAwareInterface
26
{
27
    use OpcodeTrait;
28

29
    private $stream;
30
    private $logger;
31

32
    public function __construct(SocketStream $stream)
33
    {
34
        $this->stream = $stream;
5✔
35
        $this->setLogger(new NullLogger());
5✔
36
    }
37

38
    public function setLogger(LoggerInterface $logger): void
39
    {
40
        $this->logger = $logger;
5✔
41
    }
42

43
    // Pull frame from stream
44
    public function pull(): Frame
45
    {
46
        // Read the frame "header" first, two bytes.
47
        $data = $this->read(2);
4✔
48

49
        list ($byte_1, $byte_2) = array_values(unpack('C*', $data));
4✔
50
        $final = (bool)($byte_1 & 0b10000000); // Final fragment marker.
4✔
51
        $rsv = $byte_1 & 0b01110000; // Unused bits, ignore
4✔
52

53
        // Parse opcode
54
        $opcode_int = $byte_1 & 0b00001111;
4✔
55
        $opcode_ints = array_flip(self::$opcodes);
4✔
56
        $opcode = array_key_exists($opcode_int, $opcode_ints) ? $opcode_ints[$opcode_int] : strval($opcode_int);
4✔
57

58
        // Masking bit
59
        $masked = (bool)($byte_2 & 0b10000000);
4✔
60

61
        $payload = '';
4✔
62

63
        // Payload length
64
        $payload_length = $byte_2 & 0b01111111;
4✔
65

66
        if ($payload_length > 125) {
4✔
67
            if ($payload_length === 126) {
×
68
                $data = $this->read(2); // 126: Payload length is a 16-bit unsigned int
×
69
                $payload_length = current(unpack('n', $data));
×
70
            } else {
71
                $data = $this->read(8); // 127: Payload length is a 64-bit unsigned int
×
72
                $payload_length = current(unpack('J', $data));
×
73
            }
74
        }
75

76
        // Get masking key.
77
        if ($masked) {
4✔
78
            $masking_key = $this->stream->read(4);
×
79
        }
80

81
        // Get the actual payload, if any (might not be for e.g. close frames).
82
        if ($payload_length > 0) {
4✔
83
            $data = $this->read($payload_length);
3✔
84
            if ($masked) {
3✔
85
                // Unmask payload.
86
                for ($i = 0; $i < $payload_length; $i++) {
×
87
                    $payload .= ($data[$i] ^ $masking_key[$i % 4]);
×
88
                }
89
            } else {
90
                $payload = $data;
3✔
91
            }
92
        }
93
        $frame = new Frame($opcode, $payload, $final);
4✔
94
        $this->logger->debug("[frame-handler] Pulled '{opcode}' frame", [
4✔
95
            'opcode' => $frame->getOpcode(),
4✔
96
            'final' => $frame->isFinal(),
4✔
97
            'content-length' => $frame->getPayloadLength(),
4✔
98
        ]);
4✔
99
        return $frame;
4✔
100
    }
101

102
    // Push frame to stream
103
    public function push(Frame $frame, bool $masked): int
104
    {
105
        $final = $frame->isFinal();
4✔
106
        $payload = $frame->getPayload();
4✔
107
        $opcode = $frame->getOpcode();
4✔
108
        $payload_length = $frame->getPayloadLength();
4✔
109

110
        $data = '';
4✔
111
        $byte_1 = $final ? 0b10000000 : 0b00000000; // Final fragment marker.
4✔
112
        $byte_1 |= self::$opcodes[$opcode]; // Set opcode.
4✔
113
        $data .= pack('C', $byte_1);
4✔
114

115
        $byte_2 = $masked ? 0b10000000 : 0b00000000; // Masking bit marker.
4✔
116

117
        // 7 bits of payload length
118
        if ($payload_length > 65535) {
4✔
119
            $data .= pack('C', $byte_2 | 0b01111111);
×
120
            $data .= pack('J', $payload_length);
×
121
        } elseif ($payload_length > 125) {
4✔
122
            $data .= pack('C', $byte_2 | 0b01111110);
×
123
            $data .= pack('n', $payload_length);
×
124
        } else {
125
            $data .= pack('C', $byte_2 | $payload_length);
4✔
126
        }
127

128
        // Handle masking.
129
        if ($masked) {
4✔
130
            // Generate a random mask.
131
            $mask = '';
×
132
            for ($i = 0; $i < 4; $i++) {
×
133
                $mask .= chr(rand(0, 255));
×
134
            }
135
            $data .= $mask;
×
136

137
            // Append masked payload to frame.
138
            for ($i = 0; $i < $payload_length; $i++) {
×
139
                $data .= $payload[$i] ^ $mask[$i % 4];
×
140
            }
141
        } else {
142
            // Append payload as-is to frame.
143
            $data .= $payload;
4✔
144
        }
145

146
        // Write to stream.
147
        $written = $this->write($data);
4✔
148

149
        $this->logger->debug("[frame-handler] Pushed '{opcode}' frame", [
4✔
150
            'opcode' => $frame->getOpcode(),
4✔
151
            'final' => $frame->isFinal(),
4✔
152
            'content-length' => $frame->getPayloadLength(),
4✔
153
        ]);
4✔
154
        return $written;
4✔
155
    }
156

157
    // Secured read op
158
    private function read(int $length): string
159
    {
160
        $data = '';
4✔
161
        $read = 0;
4✔
162
        while ($read < $length) {
4✔
163
            $got = $this->stream->read($length - $read);
4✔
164
            if (empty($got)) {
4✔
165
                throw new RuntimeException('Empty read; connection dead?');
×
166
            }
167
            $data .= $got;
4✔
168
            $read = strlen($data);
4✔
169
        }
170
        return $data;
4✔
171
    }
172

173
    // Secured write op
174
    private function write(string $data): int
175
    {
176
        $length = strlen($data);
4✔
177
        $written = $this->stream->write($data);
4✔
178
        if ($written < $length) {
4✔
179
            throw new RuntimeException("Could only write {$written} out of {$length} bytes.");
×
180
        }
181
        return $written;
4✔
182
    }
183
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc