|
<?php |
|
|
|
declare(strict_types=1); |
|
|
|
final class QuicInitialPacketParser |
|
{ |
|
private string $buffer = ''; |
|
private int $offset = 0; |
|
|
|
public function parse(string $packet): array |
|
{ |
|
$this->buffer = $packet; |
|
$this->offset = 0; |
|
|
|
$firstByte = $this->readUint8(); |
|
$headerForm = ($firstByte >> 7) & 0x01; |
|
$fixedBit = ($firstByte >> 6) & 0x01; |
|
$longPacketType = ($firstByte >> 4) & 0x03; |
|
$packetNumberLength = ($firstByte & 0x03) + 1; |
|
|
|
if ($headerForm !== 1) { |
|
throw new InvalidArgumentException('Not a QUIC long header packet (Header Form must be 1).'); |
|
} |
|
if ($fixedBit !== 1) { |
|
throw new InvalidArgumentException('Invalid QUIC packet (Fixed Bit must be 1).'); |
|
} |
|
if ($longPacketType !== 0) { |
|
throw new InvalidArgumentException('Long header packet is not Initial (Long Packet Type must be 0).'); |
|
} |
|
|
|
$version = $this->readUint32(); |
|
|
|
$dcidLength = $this->readUint8(); |
|
if ($dcidLength > 20) { |
|
throw new InvalidArgumentException('Destination Connection ID length exceeds 20 bytes.'); |
|
} |
|
$dcid = $this->readBytes($dcidLength); |
|
|
|
$scidLength = $this->readUint8(); |
|
if ($scidLength > 20) { |
|
throw new InvalidArgumentException('Source Connection ID length exceeds 20 bytes.'); |
|
} |
|
$scid = $this->readBytes($scidLength); |
|
|
|
$tokenLength = $this->readVarInt(); |
|
$token = $this->readBytes($tokenLength); |
|
|
|
// RFC 9000: Length includes Packet Number field + payload. |
|
$length = $this->readVarInt(); |
|
if ($length < $packetNumberLength) { |
|
throw new InvalidArgumentException('Invalid Length field: smaller than Packet Number length.'); |
|
} |
|
|
|
$packetNumberRaw = $this->readBytes($packetNumberLength); |
|
$packetNumber = $this->bytesToInt($packetNumberRaw); |
|
|
|
$remainingPayloadLength = strlen($this->buffer) - $this->offset; |
|
$declaredPayloadLength = $length - $packetNumberLength; |
|
|
|
return [ |
|
'type' => 'initial', |
|
'header_form' => $headerForm, |
|
'fixed_bit' => $fixedBit, |
|
'long_packet_type' => $longPacketType, |
|
'version' => $version, |
|
'dcid_length' => $dcidLength, |
|
'dcid' => bin2hex($dcid), |
|
'scid_length' => $scidLength, |
|
'scid' => bin2hex($scid), |
|
'token_length' => $tokenLength, |
|
'token' => bin2hex($token), |
|
'length' => $length, |
|
'packet_number' => $packetNumber, |
|
'payload_length' => $declaredPayloadLength, |
|
'remaining_payload_length' => $remainingPayloadLength, |
|
]; |
|
} |
|
|
|
private function readUint8(): int |
|
{ |
|
$this->ensureAvailable(1); |
|
return ord($this->buffer[$this->offset++]); |
|
} |
|
|
|
private function readUint32(): int |
|
{ |
|
$bytes = $this->readBytes(4); |
|
$parts = unpack('Nvalue', $bytes); |
|
return (int) $parts['value']; |
|
} |
|
|
|
private function readBytes(int $n): string |
|
{ |
|
if ($n < 0) { |
|
throw new InvalidArgumentException('Cannot read a negative number of bytes.'); |
|
} |
|
$this->ensureAvailable($n); |
|
$chunk = substr($this->buffer, $this->offset, $n); |
|
$this->offset += $n; |
|
return $chunk; |
|
} |
|
|
|
private function readVarInt(): int |
|
{ |
|
$first = $this->readUint8(); |
|
$prefix = $first >> 6; |
|
$length = 1 << $prefix; |
|
|
|
$value = $first & 0x3f; |
|
for ($i = 1; $i < $length; $i++) { |
|
$value = ($value << 8) | $this->readUint8(); |
|
} |
|
|
|
return $value; |
|
} |
|
|
|
private function bytesToInt(string $bytes): int |
|
{ |
|
$value = 0; |
|
$len = strlen($bytes); |
|
for ($i = 0; $i < $len; $i++) { |
|
$value = ($value << 8) | ord($bytes[$i]); |
|
} |
|
return $value; |
|
} |
|
|
|
private function ensureAvailable(int $needed): void |
|
{ |
|
$remaining = strlen($this->buffer) - $this->offset; |
|
if ($remaining < $needed) { |
|
throw new InvalidArgumentException( |
|
sprintf('Packet too short: needed %d byte(s), but only %d remain at offset %d.', $needed, $remaining, $this->offset) |
|
); |
|
} |
|
} |
|
} |