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

sirn-se / websocket-php / 5610959835

pending completion
5610959835

push

github

Sören Jensen
Middleware support

14 of 14 new or added lines in 4 files covered. (100.0%)

283 of 676 relevant lines covered (41.86%)

1.66 hits per line

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

75.31
/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
    private $pushMasked;
32
    private $pullMaskedRequired;
33

34
    public function __construct(SocketStream $stream, bool $pushMasked, bool $pullMaskedRequired)
35
    {
36
        $this->stream = $stream;
12✔
37
        $this->pushMasked = $pushMasked;
12✔
38
        $this->pullMaskedRequired = $pullMaskedRequired;
12✔
39
        $this->setLogger(new NullLogger());
12✔
40
    }
41

42
    public function setLogger(LoggerInterface $logger): void
43
    {
44
        $this->logger = $logger;
12✔
45
    }
46

47
    // Pull frame from stream
48
    public function pull(): Frame
49
    {
50
        // Read the frame "header" first, two bytes.
51
        $data = $this->read(2);
2✔
52

53
        list ($byte_1, $byte_2) = array_values(unpack('C*', $data));
1✔
54
        $final = (bool)($byte_1 & 0b10000000); // Final fragment marker.
1✔
55
        $rsv = $byte_1 & 0b01110000; // Unused bits, ignore
1✔
56

57
        // Parse opcode
58
        $opcode_int = $byte_1 & 0b00001111;
1✔
59
        $opcode_ints = array_flip(self::$opcodes);
1✔
60
        $opcode = array_key_exists($opcode_int, $opcode_ints) ? $opcode_ints[$opcode_int] : strval($opcode_int);
1✔
61

62
        // Masking bit
63
        $masked = (bool)($byte_2 & 0b10000000);
1✔
64

65
        $payload = '';
1✔
66

67
        // Payload length
68
        $payload_length = $byte_2 & 0b01111111;
1✔
69

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

80
        // Get masking key.
81
        if ($masked) {
1✔
82
            $masking_key = $this->stream->read(4);
×
83
        }
84

85
        // @todo Throw exception if !masked && pullMaskedRequired
86

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

108
    // Push frame to stream
109
    public function push(Frame $frame, bool $masked = null): int
110
    {
111
        $final = $frame->isFinal();
9✔
112
        $payload = $frame->getPayload();
9✔
113
        $opcode = $frame->getOpcode();
9✔
114
        $payload_length = $frame->getPayloadLength();
9✔
115

116
        $data = '';
9✔
117
        $byte_1 = $final ? 0b10000000 : 0b00000000; // Final fragment marker.
9✔
118
        $byte_1 |= self::$opcodes[$opcode]; // Set opcode.
9✔
119
        $data .= pack('C', $byte_1);
9✔
120

121
        $byte_2 = $this->pushMasked ? 0b10000000 : 0b00000000; // Masking bit marker.
9✔
122

123
        // 7 bits of payload length
124
        if ($payload_length > 65535) {
9✔
125
            $data .= pack('C', $byte_2 | 0b01111111);
×
126
            $data .= pack('J', $payload_length);
×
127
        } elseif ($payload_length > 125) {
9✔
128
            $data .= pack('C', $byte_2 | 0b01111110);
×
129
            $data .= pack('n', $payload_length);
×
130
        } else {
131
            $data .= pack('C', $byte_2 | $payload_length);
9✔
132
        }
133

134
        // Handle masking.
135
        if ($this->pushMasked) {
9✔
136
            // Generate a random mask.
137
            $mask = '';
×
138
            for ($i = 0; $i < 4; $i++) {
×
139
                $mask .= chr(rand(0, 255));
×
140
            }
141
            $data .= $mask;
×
142

143
            // Append masked payload to frame.
144
            for ($i = 0; $i < $payload_length; $i++) {
×
145
                $data .= $payload[$i] ^ $mask[$i % 4];
×
146
            }
147
        } else {
148
            // Append payload as-is to frame.
149
            $data .= $payload;
9✔
150
        }
151

152
        // Write to stream.
153
        $written = $this->write($data);
9✔
154

155
        $this->logger->debug("[frame-handler] Pushed '{opcode}' frame", [
1✔
156
            'opcode' => $frame->getOpcode(),
1✔
157
            'final' => $frame->isFinal(),
1✔
158
            'content-length' => $frame->getPayloadLength(),
1✔
159
        ]);
1✔
160
        return $written;
1✔
161
    }
162

163
    // Secured read op
164
    private function read(int $length): string
165
    {
166
        $data = '';
2✔
167
        $read = 0;
2✔
168
        while ($read < $length) {
2✔
169
            $got = $this->stream->read($length - $read);
2✔
170
            if (empty($got)) {
1✔
171
                throw new RuntimeException('Empty read; connection dead?');
×
172
            }
173
            $data .= $got;
1✔
174
            $read = strlen($data);
1✔
175
        }
176
        return $data;
1✔
177
    }
178

179
    // Secured write op
180
    private function write(string $data): int
181
    {
182
        $length = strlen($data);
9✔
183
        $written = $this->stream->write($data);
9✔
184
        if ($written < $length) {
1✔
185
            throw new RuntimeException("Could only write {$written} out of {$length} bytes.");
×
186
        }
187
        return $written;
1✔
188
    }
189
}
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