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

sirn-se / websocket-php / 8346487915

19 Mar 2024 04:15PM UTC coverage: 22.584% (-77.4%) from 100.0%
8346487915

push

github

sirn-se
Temp test verification

2 of 2 new or added lines in 1 file covered. (100.0%)

742 existing lines in 32 files now uncovered.

222 of 983 relevant lines covered (22.58%)

0.23 hits per line

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

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

3
/**
4
 * Copyright (C) 2014-2024 Textalk and contributors.
5
 * This file is part of Websocket PHP and is free software under the ISC License.
6
 */
7

8
namespace WebSocket\Frame;
9

10
use Phrity\Net\SocketStream;
11
use Psr\Log\{
12
    LoggerInterface,
13
    LoggerAwareInterface,
14
    NullLogger
15
};
16
use RuntimeException;
17
use Stringable;
18
use WebSocket\Exception\CloseException;
19
use WebSocket\Trait\{
20
    OpcodeTrait,
21
    StringableTrait
22
};
23

24
/**
25
 * WebSocket\Frame\FrameHandler class.
26
 * Reads and writes Frames on stream.
27
 */
28
class FrameHandler implements LoggerAwareInterface, Stringable
29
{
30
    use OpcodeTrait;
31
    use StringableTrait;
32

33
    private $stream;
34
    private $logger;
35
    private $pushMasked;
36
    private $pullMaskedRequired;
37

38
    public function __construct(SocketStream $stream, bool $pushMasked, bool $pullMaskedRequired)
39
    {
40
        $this->stream = $stream;
1✔
41
        $this->pushMasked = $pushMasked;
1✔
42
        $this->pullMaskedRequired = $pullMaskedRequired;
1✔
43
        $this->setLogger(new NullLogger());
1✔
44
    }
45

46
    public function setLogger(LoggerInterface $logger): void
47
    {
48
        $this->logger = $logger;
1✔
49
    }
50

51
    // Pull frame from stream
52
    public function pull(): Frame
53
    {
54
        // Read the frame "header" first, two bytes.
UNCOV
55
        $data = $this->read(2);
×
UNCOV
56
        list ($byte_1, $byte_2) = array_values(unpack('C*', $data));
×
UNCOV
57
        $final = (bool)($byte_1 & 0b10000000); // Final fragment marker.
×
UNCOV
58
        $rsv = $byte_1 & 0b01110000; // Unused bits, ignore
×
59

60
        // Parse opcode
UNCOV
61
        $opcode_int = $byte_1 & 0b00001111;
×
UNCOV
62
        $opcode_ints = array_flip(self::$opcodes);
×
UNCOV
63
        $opcode = array_key_exists($opcode_int, $opcode_ints) ? $opcode_ints[$opcode_int] : strval($opcode_int);
×
64

65
        // Masking bit
UNCOV
66
        $masked = (bool)($byte_2 & 0b10000000);
×
67

UNCOV
68
        $payload = '';
×
69

70
        // Payload length
UNCOV
71
        $payload_length = $byte_2 & 0b01111111;
×
72

UNCOV
73
        if ($payload_length > 125) {
×
UNCOV
74
            if ($payload_length === 126) {
×
UNCOV
75
                $data = $this->read(2); // 126: Payload length is a 16-bit unsigned int
×
UNCOV
76
                $payload_length = current(unpack('n', $data));
×
77
            } else {
UNCOV
78
                $data = $this->read(8); // 127: Payload length is a 64-bit unsigned int
×
UNCOV
79
                $payload_length = current(unpack('J', $data));
×
80
            }
81
        }
82

83
        // Get masking key.
UNCOV
84
        if ($masked) {
×
UNCOV
85
            $masking_key = $this->stream->read(4);
×
86
        }
87

88
        // Get the actual payload, if any (might not be for e.g. close frames).
UNCOV
89
        if ($payload_length > 0) {
×
UNCOV
90
            $data = $this->read($payload_length);
×
UNCOV
91
            if ($masked) {
×
92
                // Unmask payload.
UNCOV
93
                for ($i = 0; $i < $payload_length; $i++) {
×
UNCOV
94
                    $payload .= ($data[$i] ^ $masking_key[$i % 4]);
×
95
                }
96
            } else {
UNCOV
97
                $payload = $data;
×
98
            }
99
        }
100

UNCOV
101
        $frame = new Frame($opcode, $payload, $final);
×
UNCOV
102
        $this->logger->debug("[frame-handler] Pulled '{$opcode}' frame", [
×
UNCOV
103
            'opcode' => $frame->getOpcode(),
×
UNCOV
104
            'final' => $frame->isFinal(),
×
UNCOV
105
            'content-length' => $frame->getPayloadLength(),
×
UNCOV
106
        ]);
×
107

UNCOV
108
        if ($this->pullMaskedRequired && !$masked) {
×
UNCOV
109
            $this->logger->error("[frame-handler] Masking required, but frame was unmasked");
×
UNCOV
110
            throw new CloseException(1002, 'Masking required');
×
111
        }
112

UNCOV
113
        return $frame;
×
114
    }
115

116
    // Push frame to stream
117
    public function push(Frame $frame, bool|null $masked = null): int
118
    {
UNCOV
119
        $final = $frame->isFinal();
×
UNCOV
120
        $payload = $frame->getPayload();
×
UNCOV
121
        $opcode = $frame->getOpcode();
×
UNCOV
122
        $payload_length = $frame->getPayloadLength();
×
123

UNCOV
124
        $data = '';
×
UNCOV
125
        $byte_1 = $final ? 0b10000000 : 0b00000000; // Final fragment marker.
×
UNCOV
126
        $byte_1 |= self::$opcodes[$opcode]; // Set opcode.
×
UNCOV
127
        $data .= pack('C', $byte_1);
×
128

UNCOV
129
        $byte_2 = $this->pushMasked ? 0b10000000 : 0b00000000; // Masking bit marker.
×
130

131
        // 7 bits of payload length
UNCOV
132
        if ($payload_length > 65535) {
×
UNCOV
133
            $data .= pack('C', $byte_2 | 0b01111111);
×
UNCOV
134
            $data .= pack('J', $payload_length);
×
UNCOV
135
        } elseif ($payload_length > 125) {
×
UNCOV
136
            $data .= pack('C', $byte_2 | 0b01111110);
×
UNCOV
137
            $data .= pack('n', $payload_length);
×
138
        } else {
UNCOV
139
            $data .= pack('C', $byte_2 | $payload_length);
×
140
        }
141

142
        // Handle masking.
UNCOV
143
        if ($this->pushMasked) {
×
144
            // Generate a random mask.
UNCOV
145
            $mask = '';
×
UNCOV
146
            for ($i = 0; $i < 4; $i++) {
×
UNCOV
147
                $mask .= chr(rand(0, 255));
×
148
            }
UNCOV
149
            $data .= $mask;
×
150

151
            // Append masked payload to frame.
UNCOV
152
            for ($i = 0; $i < $payload_length; $i++) {
×
UNCOV
153
                $data .= $payload[$i] ^ $mask[$i % 4];
×
154
            }
155
        } else {
156
            // Append payload as-is to frame.
UNCOV
157
            $data .= $payload;
×
158
        }
159

160
        // Write to stream.
UNCOV
161
        $written = $this->write($data);
×
162

UNCOV
163
        $this->logger->debug("[frame-handler] Pushed '{opcode}' frame", [
×
UNCOV
164
            'opcode' => $frame->getOpcode(),
×
UNCOV
165
            'final' => $frame->isFinal(),
×
UNCOV
166
            'content-length' => $frame->getPayloadLength(),
×
UNCOV
167
        ]);
×
UNCOV
168
        return $written;
×
169
    }
170

171
    // Secured read op
172
    private function read(int $length): string
173
    {
UNCOV
174
        $data = '';
×
UNCOV
175
        $read = 0;
×
UNCOV
176
        while ($read < $length) {
×
UNCOV
177
            $got = $this->stream->read($length - $read);
×
UNCOV
178
            if (empty($got)) {
×
UNCOV
179
                throw new RuntimeException('Empty read; connection dead?');
×
180
            }
UNCOV
181
            $data .= $got;
×
UNCOV
182
            $read = strlen($data);
×
183
        }
UNCOV
184
        return $data;
×
185
    }
186

187
    // Secured write op
188
    private function write(string $data): int
189
    {
UNCOV
190
        $length = strlen($data);
×
UNCOV
191
        $written = $this->stream->write($data);
×
UNCOV
192
        if ($written < $length) {
×
UNCOV
193
            throw new RuntimeException("Could only write {$written} out of {$length} bytes.");
×
194
        }
UNCOV
195
        return $written;
×
196
    }
197
}
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