Frame   A
last analyzed

Complexity

Total Complexity 28

Size/Duplication

Total Lines 290
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 3

Test Coverage

Coverage 84.62%

Importance

Changes 0
Metric Value
wmc 28
lcom 2
cbo 3
dl 0
loc 290
ccs 66
cts 78
cp 0.8462
rs 10
c 0
b 0
f 0

15 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 19 6
A parseFrame() 0 9 1
B parseFrameHeader() 0 27 3
A getLength() 0 4 1
A setLength() 0 4 1
B serialize() 0 24 3
A parseFlags() 0 10 3
serializeBody() 0 1 ?
parseBody() 0 1 ?
A getFlags() 0 4 1
A setFlags() 0 10 3
A getBodyLen() 0 4 1
A setStreamId() 0 6 1
A getStreamId() 0 4 1
A __debugInfo() 0 20 3
1
<?php
2
/**
3
 * Defines framing logic for HTTP/2. Provides both classes to represent framed
4
 * data and logic for aiding the connection when it comes to reading from the
5
 * socket.
6
 *
7
 * @package Hyphper
8
 */
9
declare(strict_types=1);
10
11
namespace Hyphper;
12
13
use Hyphper\Frame\Flags;
14
15
use Hyphper\Frame\Exception\InvalidFrameException;
16
use Hyphper\Frame\Exception\UnknownFrameException;
17
18
/**
19
 * The base class for all HTTP/2 frames.
20
 *
21
 * @package Hyphper
22
 */
23
abstract class Frame
24
{
25
    const FRAMES = [
26
        0x0 => 'DataFrame',
27
        0x01 => 'HeadersFrame',
28
        0x02 => 'PriorityFrame',
29
        0x03 => 'RstStreamFrame',
30
        0x04 => 'SettingsFrame',
31
        0x05 => 'PushPromiseFrame',
32
        0x06 => 'PingFrame',
33
        0x07 => 'GoAwayFrame',
34
        0x08 => 'WindowUpdateFrame',
35
        0x09 => 'ContinuationFrame',
36
        0xA => 'AltSvcFrame',
37
    ];
38
39
    const HAS_STREAM = 1;
40
    const NO_STREAM = 2;
41
    const EITHER_STREAM = 4;
42
43
    /**
44
     * @var array The flags defined on this type of frame.
45
     */
46
    protected $defined_flags = [];
47
48
    /**
49
     * @var Flags
50
     */
51
    protected $flags;
52
53
    /**
54
     * @var int The byte used to define the type of the frame.
55
     */
56
    protected $type;
57
58
    /**
59
     * @var int The length of the frame
60
     */
61
    protected $length;
62
63
    /**
64
     * If true, the frame's stream_id must be non-zero. If false,
65
     * it must be zero. If null, stream_id is ignored.
66
     *
67
     * @var int
68
     */
69
    protected $stream_association;
70
71
    /**
72
     * @var int
73
     */
74
    protected $stream_id = 0;
75
76
    /**
77
     * @var string
78
     */
79
    protected $body;
80
81
    /**
82
     * @var int
83
     */
84
    protected $body_len = 0;
85
86
    /**
87
     * Frame constructor.
88
     *
89
     * @param array $options
90
     */
91 71
    public function __construct(array $options = [])
92
    {
93 71
        $this->stream_id = $options['stream_id'] ?? 0;
94 71
        $this->length = $options['length'] ?? 0;
95
96 71
        $this->flags = new Flags(... $this->defined_flags);
97
98 71
        foreach ($options['flags'] ?? [] as $flag) {
99 2
            $this->flags->add($flag);
100
        }
101
102 71
        if ($this->stream_association === self::HAS_STREAM && !$this->stream_id) {
103 3
            throw new InvalidFrameException();
104
        }
105
106 68
        if ($this->stream_association === self::NO_STREAM && $this->stream_id) {
107 3
            throw new InvalidFrameException();
108
        }
109 65
    }
110
111
    /**
112
     * @param string $data
113
     *
114
     * @return Frame
115
     * @throws InvalidFrameException
116
     * @throws UnknownFrameException
117
     */
118
    public static function parseFrame(string $data): Frame
119
    {
120
        $frame = static::parseFrameHeader(substr($data, 0, 9));
121
        $length = $frame->getLength();
122
123
        $frame->parseBody(strlen($data, 9, $length));
124
125
        return $frame;
126
    }
127
128
    /**
129
     * Takes a 9-byte frame header and returns a tuple of the appropriate
130
     * Frame object and the length that needs to be read from the socket.
131
     * This populates the flags field, and determines how long the body is.
132
     *
133
     * @param string $header
134
     * @throws UnknownFrameException If a frame of unknown type is received.
135
     */
136 54
    public static function parseFrameHeader(string $header): Frame
137
    {
138
        /*
139
         * H = unsigned 16bit int = n
140
         * B = unsigned 8bit string = C
141
         * L = unsigned 32bit int = N
142
         */
143
144 54
        if (!$fields = @unpack('nlength8/Clength16/Ctype/Cflags/Nstream_id', $header)) {
145 12
            throw new InvalidFrameException("Invalid Frame Header");
146
        }
147
148 42
        $length = ($fields['length8'] << 8) + $fields['length16'];
149 42
        $type = $fields['type'];
150 42
        $flags = $fields['flags'];
151 42
        $stream_id = $fields['stream_id'];
152
153 42
        if (!array_key_exists($type, static::FRAMES)) {
154 14
            throw new UnknownFrameException($type, $length);
155
        }
156
157 28
        $frame = '\Hyphper\Frame\\' . static::FRAMES[$type];
158 28
        $frame = new $frame(['stream_id' => $stream_id, 'length' => $length]);
159 28
        $frame->parseFlags($flags);
160
161 28
        return $frame;
162
    }
163
164
    /**
165
     * @return int
166
     */
167 28
    public function getLength()
168
    {
169 28
        return $this->length;
170
    }
171
172
    /**
173
     * @param int $length
174
     */
175
    public function setLength($length)
176
    {
177
        $this->length = $length;
178
    }
179
180
    /**
181
     * Convert a frame into a bytestring, representing the serialized form of
182
     * the frame.
183
     *
184
     * @return string
185
     */
186 22
    public function serialize():string
187
    {
188 22
        $body = $this->serializeBody();
189 21
        $this->body_len = strlen($body);
190
191 21
        $flags = 0;
192
193 21
        foreach ($this->defined_flags as $flag) {
194 13
            if ($this->flags->hasFlag($flag)) {
195 13
                $flags |= $flag;
196
            }
197
        }
198
199 21
        $header = pack(
200 21
            'nCCCN',
201 21
            ($this->body_len & 0xFFFF00) >> 8,    // Length spread over top 24 bits
202 21
            $this->body_len & 0x0000FF,
203 21
            $this->type,
204
            $flags,
205 21
            $this->stream_id & 0x7FFFFFFF   // Stream ID is 32 bits.
206
        );
207
208 21
        return $header . $body;
209
    }
210
211
    /**
212
     * @param $flag_byte
213
     *
214
     * @return Flags
215
     * @throws Frame\Exception\InvalidFlagException
216
     */
217 42
    public function parseFlags(int $flag_byte): Flags
218
    {
219 42
        foreach ($this->defined_flags as $flag) {
220 28
            if ($flag_byte & $flag) {
221 28
                $this->flags->add($flag);
222
            }
223
        }
224
225 42
        return $this->flags;
226
    }
227
228
    abstract public function serializeBody(): string;
229
230
    /**
231
     * Given the body of a frame, parses it into frame data. This populates
232
     * the non-header parts of the frame: that is, it does not populate the
233
     * stream ID or flags.
234
     *
235
     *
236
     * @param string $data
237
     * @return void
238
     */
239
    abstract public function parseBody(string $data);
240
241
    /**
242
     * @return Flags
243
     */
244 14
    public function getFlags()
245
    {
246 14
        return $this->flags;
247
    }
248
249
    /**
250
     * @param array $flags
251
     * @return $this
252
     */
253 3
    public function setFlags(array $flags)
254
    {
255 3
        foreach ($flags as $flag) {
256 3
            if (in_array($flag, $this->defined_flags)) {
257 3
                $this->flags->add($flag);
258
            }
259
        }
260
261 3
        return $this;
262
    }
263
264
    /**
265
     * @return int
266
     */
267 14
    public function getBodyLen()
268
    {
269 14
        return $this->body_len;
270
    }
271
272
    /**
273
     * @param int $stream_id
274
     *
275
     * @return Frame
276
     */
277
    public function setStreamId($stream_id)
278
    {
279
        $this->stream_id = $stream_id;
280
281
        return $this;
282
    }
283
284
    /**
285
     * @return int
286
     */
287 1
    public function getStreamId()
288
    {
289 1
        return $this->stream_id;
290
    }
291
292 1
    public function __debugInfo()
293
    {
294 1
        $flags = "None";
295 1
        if ($f = $this->flags->getIterator()) {
296
            $flags = implode(", ", $f);
297
        }
298
299 1
        $body = bin2hex($this->serializeBody());
300 1
        if (strlen($body) > 20) {
301 1
            $body = substr($body, 0, 20) . '...';
302
        }
303
304 1
        return [sprintf(
305 1
            "%s(Stream: %s; Flags: %s): %s",
306 1
            substr(strrchr(static::class, '\\'), 1),
307 1
            $this->stream_id ?? "None",
308
            $flags,
309
            $body
310
        )];
311
    }
312
}
313