224 lines
5.9 KiB
PHP
224 lines
5.9 KiB
PHP
<?php
|
|
|
|
namespace Madeorsk\Forwarded;
|
|
|
|
use Madeorsk\Forwarded\Exceptions\EmptyNodeNameException;
|
|
|
|
/**
|
|
* The Forwarded header parser, following the section 4 of RFC 7239.
|
|
* @see https://datatracker.ietf.org/doc/html/rfc7239#section-4
|
|
*/
|
|
class Parser
|
|
{
|
|
/**
|
|
* Parse the HTTP header found in `$_SERVER["HTTP_FORWARDED"]`.
|
|
* @return Forwarded - The parsed Forwarded header.
|
|
* @throws EmptyNodeNameException
|
|
*/
|
|
public function parseHttpHeader(): Forwarded
|
|
{
|
|
return $this->parse($_SERVER["HTTP_FORWARDED"]);
|
|
}
|
|
|
|
/**
|
|
* The currently reading forwards list.
|
|
* @var array
|
|
*/
|
|
protected array $forwards = [];
|
|
/**
|
|
* An associative array (token => value) of the currently reading forward.
|
|
* @var array<string, string>
|
|
*/
|
|
protected array $pairs = [];
|
|
/**
|
|
* The currently reading token.
|
|
* @var string
|
|
*/
|
|
protected string $token = "";
|
|
/**
|
|
* The currently reading value.
|
|
* @var string
|
|
*/
|
|
protected string $value = "";
|
|
/**
|
|
* The currently reading quoted string.
|
|
* @var string
|
|
*/
|
|
protected string $quotedString = "";
|
|
/**
|
|
* The name of the currently reading value, which determine the variable to alter and the state function to use.
|
|
* @var string
|
|
*/
|
|
protected string $currentState = self::TOKEN_STATE;
|
|
|
|
/**
|
|
* Parse the header content in an array of associative arrays with token => value.
|
|
* @see https://datatracker.ietf.org/doc/html/rfc7239#section-4 for token / value meaning.
|
|
* @param string $headerContent - The header content string.
|
|
* @return array<array<string, string>> - The parsed array of forwards [token => value] arrays.
|
|
*/
|
|
public function parseAssoc(string $headerContent): array
|
|
{
|
|
// Full reinitialization.
|
|
$this->forwards = [];
|
|
$this->pairs = [];
|
|
$this->reinitPairParsing();
|
|
|
|
foreach (mb_str_split($headerContent) as $char)
|
|
{ // For each character, we first execute the state function.
|
|
// The state function can change the state variable to the next state, so we store the current state in a local variable.
|
|
$currentState = $this->currentState;
|
|
if ($this->{"state".ucfirst($currentState)}($char))
|
|
// If the state function returned true, then we add the current char to the current state.
|
|
$this->{$currentState} .= $char;
|
|
}
|
|
|
|
if (!empty($this->token))
|
|
// If the token is not empty, we save the current pair.
|
|
$this->savePair();
|
|
if (!empty($this->pairs))
|
|
// If there are pairs, we save the current forward.
|
|
$this->saveForward();
|
|
|
|
return $this->forwards; // The parsing is finished, we can return the parsed forwards.
|
|
}
|
|
|
|
/**
|
|
* Parse the header content in a Forwarded header object.
|
|
* @param string $headerContent - The header content string.
|
|
* @return Forwarded - The parsed Forwarded header object.
|
|
* @throws EmptyNodeNameException
|
|
*/
|
|
public function parse(string $headerContent): Forwarded
|
|
{
|
|
return new Forwarded($this->parseAssoc($headerContent));
|
|
}
|
|
|
|
/**
|
|
* Reinitialize the parsing of a pair.
|
|
* @return void
|
|
*/
|
|
protected function reinitPairParsing(): void
|
|
{
|
|
$this->token = "";
|
|
$this->value = "";
|
|
$this->quotedString = "";
|
|
$this->currentState = self::TOKEN_STATE;
|
|
}
|
|
|
|
/**
|
|
* Save the currently parsing pair and reinitialize parsing to read another one.
|
|
* @return void
|
|
*/
|
|
protected function savePair(): void
|
|
{
|
|
// There should be at least one filled value between `value` and `quotedString`.
|
|
$this->pairs[trim($this->token)] = trim($this->value.$this->quotedString);
|
|
$this->reinitPairParsing();
|
|
}
|
|
/**
|
|
* Save the currently parsing forward and reinitialize parsing to read another one.
|
|
* @return void
|
|
*/
|
|
protected function saveForward(): void
|
|
{
|
|
if (!empty($this->token))
|
|
// If the token is not empty, we save the current pair.
|
|
$this->savePair();
|
|
|
|
$this->forwards[] = $this->pairs;
|
|
$this->pairs = [];
|
|
$this->reinitPairParsing();
|
|
}
|
|
|
|
|
|
/*
|
|
*
|
|
* =*= STATES CONSTANTS AND FUNCTIONS =*=
|
|
*
|
|
*/
|
|
|
|
|
|
const TOKEN_STATE = "token";
|
|
const VALUE_STATE = "value";
|
|
const QUOTED_STRING_STATE = "quotedString";
|
|
const QUOTED_STRING_ESCAPING_STATE = "quotedStringEscaping";
|
|
|
|
/**
|
|
* The state function of token parsing.
|
|
* @param string $char - The currently reading character.
|
|
* @return bool - True if the current character should be added to the token, false otherwise.
|
|
*/
|
|
protected function stateToken(string $char): bool
|
|
{
|
|
switch ($char)
|
|
{
|
|
case "=":
|
|
$this->currentState = self::VALUE_STATE;
|
|
return false;
|
|
|
|
default:
|
|
return true;
|
|
}
|
|
}
|
|
/**
|
|
* The state function of value parsing.
|
|
* @param string $char - The currently reading character.
|
|
* @return bool - True if the current character should be added to the value, false otherwise.
|
|
*/
|
|
protected function stateValue(string $char): bool
|
|
{
|
|
if (empty($this->value))
|
|
{ // If there is an empty value, we check if this value can be a quoted string.
|
|
if ($char == "\"")
|
|
{ // The value starts with a quote, it is a quoted string.
|
|
$this->currentState = self::QUOTED_STRING_STATE;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
switch ($char)
|
|
{
|
|
case ";":
|
|
$this->savePair();
|
|
return false;
|
|
case ",":
|
|
$this->saveForward();
|
|
return false;
|
|
|
|
default:
|
|
return true;
|
|
}
|
|
}
|
|
/**
|
|
* The state function of quoted string parsing.
|
|
* @param string $char - The currently reading character.
|
|
* @return bool - True if the current character should be added to the value, false otherwise.
|
|
*/
|
|
protected function stateQuotedString(string $char): bool
|
|
{
|
|
switch ($char)
|
|
{
|
|
case "\"":
|
|
$this->currentState = self::VALUE_STATE;
|
|
return false;
|
|
case "\\":
|
|
$this->currentState = self::QUOTED_STRING_ESCAPING_STATE;
|
|
return false;
|
|
|
|
default:
|
|
return true;
|
|
}
|
|
}
|
|
/**
|
|
* The state function of "quoted string while escaping" parsing.
|
|
* @param string $char - The currently reading character.
|
|
* @return bool - True if the current character should be added to the value, false otherwise.
|
|
*/
|
|
protected function stateQuotedStringEscaping(string $char): bool
|
|
{
|
|
$this->currentState = self::QUOTED_STRING_STATE;
|
|
return true;
|
|
}
|
|
}
|