First version of the library, with unit tests.
+ Add a parser of Forwarded header. + Add classes for the Forwarded header in itself, and its components like a single Forward and its ForwardNodes. + Unit tests of some of the usable methods.
This commit is contained in:
commit
6810b25f58
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
# Composer
|
||||
/vendor/
|
||||
|
||||
# IDEA
|
||||
*.iml
|
31
composer.json
Normal file
31
composer.json
Normal file
@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "madeorsk/forwarded",
|
||||
"description": "Forwarded header (RFC 7239) parsing library for PHP.",
|
||||
"type": "library",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Madeorsk",
|
||||
"email": "madeorsk@protonmail.com"
|
||||
}
|
||||
],
|
||||
"scripts": {
|
||||
"test": "phpunit tests/"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Madeorsk\\Forwarded\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Madeorsk\\Forwarded\\Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^10.1"
|
||||
}
|
||||
}
|
1628
composer.lock
generated
Normal file
1628
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
14
src/Exceptions/EmptyNodeNameException.php
Normal file
14
src/Exceptions/EmptyNodeNameException.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Madeorsk\Forwarded\Exceptions;
|
||||
|
||||
/**
|
||||
* Exception thrown when trying to guess the type on an empty node name.
|
||||
*/
|
||||
class EmptyNodeNameException extends \Exception
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct("Empty node name while guessing type of the ForwardNode.");
|
||||
}
|
||||
}
|
17
src/Exceptions/NotIpAddressException.php
Normal file
17
src/Exceptions/NotIpAddressException.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Madeorsk\Forwarded\Exceptions;
|
||||
|
||||
/**
|
||||
* Exception thrown when the forward node interface is not an IP address and we are trying to get an IP address.
|
||||
*/
|
||||
class NotIpAddressException extends \Exception
|
||||
{
|
||||
/**
|
||||
* @param string $nodeName The node name of the forward node.
|
||||
*/
|
||||
public function __construct(public string $nodeName)
|
||||
{
|
||||
parent::__construct("This forward node with the node name \"$this->nodeName\" is not an IP address.");
|
||||
}
|
||||
}
|
139
src/Forward.php
Normal file
139
src/Forward.php
Normal file
@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
namespace Madeorsk\Forwarded;
|
||||
|
||||
use Madeorsk\Forwarded\Exceptions\EmptyNodeNameException;
|
||||
|
||||
/**
|
||||
* Class of an element in the forwarded elements list as defined in RFC7239.
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc7239#section-5
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc7239#section-7.1
|
||||
*/
|
||||
class Forward
|
||||
{
|
||||
/**
|
||||
* Identifies the user-agent facing interface of the proxy.
|
||||
* @var ForwardNode|null
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc7239#section-5.1
|
||||
*/
|
||||
protected ?ForwardNode $by = null;
|
||||
|
||||
/**
|
||||
* Identifies the node making the request to the proxy.
|
||||
* @var ForwardNode|null
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc7239#section-5.2
|
||||
*/
|
||||
protected ?ForwardNode $for = null;
|
||||
|
||||
/**
|
||||
* The host request header field as received by the proxy.
|
||||
* @var string|null
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc7239#section-5.3
|
||||
*/
|
||||
protected ?string $host = null;
|
||||
|
||||
/**
|
||||
* Indicates what protocol was used to make the request.
|
||||
* @var string|null
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc7239#section-5.4
|
||||
*/
|
||||
protected ?string $protocol = null;
|
||||
|
||||
/**
|
||||
* Create a new Forward.
|
||||
* @param array<string, string> $assoc - The associative array of token-value pairs in the forward.
|
||||
* @throws EmptyNodeNameException
|
||||
*/
|
||||
public function __construct(array $assoc = [])
|
||||
{
|
||||
// Load the given associative array.
|
||||
$this->loadAssoc($assoc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the forward data from its token-value pairs.
|
||||
* @param array<string, string> $assoc - The associative array of token-value pairs in the forward.
|
||||
* @return void
|
||||
* @throws EmptyNodeNameException
|
||||
*/
|
||||
public function loadAssoc(array $assoc = []): void
|
||||
{
|
||||
// Read the interfaces, if they are specified.
|
||||
if (!empty($assoc["by"]))
|
||||
$this->by = new ForwardNode($assoc["by"]);
|
||||
if (!empty($assoc["for"]))
|
||||
$this->for = new ForwardNode($assoc["for"]);
|
||||
|
||||
// Store the host, if it is specified.
|
||||
if (!empty($assoc["host"]))
|
||||
$this->host = $assoc["host"];
|
||||
// Store the protocol, if it is specified.
|
||||
if (!empty($assoc["proto"]))
|
||||
$this->protocol = $assoc["proto"];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The "by" parameter is used to disclose the interface where the
|
||||
* request came in to the proxy server.
|
||||
* This is primarily added by reverse proxies that wish to forward this
|
||||
* information to the backend server. It can also be interesting in a
|
||||
* multihomed environment to signal to backend servers from which the
|
||||
* request came.
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc7239#section-5.1
|
||||
* @return ForwardNode|null - The node that identifies the interface, if there is one.
|
||||
*/
|
||||
public function by(): ?ForwardNode
|
||||
{
|
||||
return $this->by;
|
||||
}
|
||||
|
||||
/**
|
||||
* The "for" parameter is used to disclose information about the client
|
||||
* that initiated the request and subsequent proxies in a chain of
|
||||
* proxies.
|
||||
* In a chain of proxy servers where this is fully utilized, the first
|
||||
* "for" parameter will disclose the client where the request was first
|
||||
* made, followed by any subsequent proxy identifiers. The last proxy
|
||||
* in the chain is not part of the list of "for" parameters. The last
|
||||
* proxy's IP address, and optionally a port number, are, however,
|
||||
* readily available as the remote IP address at the transport layer.
|
||||
* It can, however, be more relevant to read information about the last
|
||||
* proxy from preceding "Forwarded" header field's "by" parameter, if
|
||||
* present.
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc7239#section-5.2
|
||||
* @return ForwardNode|null - The node that identifies the interface, if there is one.
|
||||
*/
|
||||
public function for(): ?ForwardNode
|
||||
{
|
||||
return $this->for;
|
||||
}
|
||||
|
||||
/**
|
||||
* The "host" parameter is used to forward the original value of the
|
||||
* "Host" header field. This can be used, for example, by the origin
|
||||
* server if a reverse proxy is rewriting the "Host" header field to
|
||||
* some internal host name.
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc7239#section-5.3
|
||||
* @return string|null - The original host, if there is one.
|
||||
*/
|
||||
public function host(): ?string
|
||||
{
|
||||
return $this->host;
|
||||
}
|
||||
|
||||
/**
|
||||
* The "proto" parameter has the value of the used protocol type.
|
||||
* For example, in an environment where a reverse proxy is also used as
|
||||
* a crypto offloader, this allows the origin server to rewrite URLs in
|
||||
* a document to match the type of connection as the user agent
|
||||
* requested, even though all connections to the origin server are
|
||||
* unencrypted HTTP.
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc7239#section-5.4
|
||||
* @return string|null - The original protocol, if there is one.
|
||||
*/
|
||||
public function protocol(): ?string
|
||||
{
|
||||
return $this->protocol;
|
||||
}
|
||||
}
|
181
src/ForwardNode.php
Normal file
181
src/ForwardNode.php
Normal file
@ -0,0 +1,181 @@
|
||||
<?php
|
||||
|
||||
namespace Madeorsk\Forwarded;
|
||||
|
||||
use Madeorsk\Forwarded\Exceptions\EmptyNodeNameException;
|
||||
use Madeorsk\Forwarded\Exceptions\NotIpAddressException;
|
||||
|
||||
/**
|
||||
* The class of an interface that emitted or received the request (by / for).
|
||||
* It is defined in section 6 of RFC 7239.
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc7239#section-6
|
||||
*/
|
||||
class ForwardNode
|
||||
{
|
||||
/**
|
||||
* The raw node name.
|
||||
* @var string
|
||||
*/
|
||||
protected string $nodeName;
|
||||
|
||||
/**
|
||||
* The type of the node.
|
||||
* @var ForwardNodeType
|
||||
*/
|
||||
protected ForwardNodeType $nodeType;
|
||||
|
||||
/**
|
||||
* Create a new forward interface class from a raw node name.
|
||||
* @param string $nodeName - The raw node name.
|
||||
* @throws EmptyNodeNameException
|
||||
*/
|
||||
public function __construct(string $nodeName)
|
||||
{
|
||||
$this->setNodeName($nodeName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the node name.
|
||||
* @param string $nodeName - The new node name.
|
||||
* @return void
|
||||
* @throws EmptyNodeNameException
|
||||
*/
|
||||
public function setNodeName(string $nodeName): void
|
||||
{
|
||||
$this->nodeName = $nodeName;
|
||||
// After setting the node name, we should update the current node type.
|
||||
$this->guessType();
|
||||
}
|
||||
|
||||
/**
|
||||
* Guess the node type based on the current node name.
|
||||
* @return ForwardNodeType - The updated node type.
|
||||
* @throws EmptyNodeNameException
|
||||
*/
|
||||
protected function guessType(): ForwardNodeType
|
||||
{
|
||||
if (empty($this->nodeName)) throw new EmptyNodeNameException();
|
||||
|
||||
if ($this->nodeName[0] == "_")
|
||||
// If the node name starts with '_', it is an obfuscated identifier: https://datatracker.ietf.org/doc/html/rfc7239#section-6.3.
|
||||
$this->nodeType = ForwardNodeType::IDENTIFIER;
|
||||
elseif ($this->nodeName == "unknown")
|
||||
// If the node name is "unknown", it is a special node from an unknown interface: https://datatracker.ietf.org/doc/html/rfc7239#section-6.2.
|
||||
$this->nodeType = ForwardNodeType::UNKNOWN;
|
||||
elseif ($this->nodeName[0] == "[")
|
||||
// If the node starts with '[', it is an IPV6: https://datatracker.ietf.org/doc/html/rfc7239#section-6.1.
|
||||
$this->nodeType = ForwardNodeType::IPV6;
|
||||
else
|
||||
// If nothing was true, it can only be an IPV4.
|
||||
$this->nodeType = ForwardNodeType::IPV4;
|
||||
|
||||
return $this->nodeType; // Returning the updated node type.
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the current node interface is an IP address.
|
||||
* @return bool - True if it is an IP address, false otherwise.
|
||||
*/
|
||||
public function isIP(): bool
|
||||
{
|
||||
return in_array($this->nodeType, [ForwardNodeType::IPV4, ForwardNodeType::IPV6]);
|
||||
}
|
||||
/**
|
||||
* Determine if the current node interface is an IPV4 address.
|
||||
* @return bool - True if it is an IPV4 address, false otherwise.
|
||||
*/
|
||||
public function isV4(): bool
|
||||
{
|
||||
return $this->nodeType == ForwardNodeType::IPV4;
|
||||
}
|
||||
/**
|
||||
* Determine if the current node interface is an IPV6 address.
|
||||
* @return bool - True if it is an IPV6 address, false otherwise.
|
||||
*/
|
||||
public function isV6(): bool
|
||||
{
|
||||
return $this->nodeType == ForwardNodeType::IPV6;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the IP address from the node name.
|
||||
* @return string - The IP address.
|
||||
* @throws NotIpAddressException
|
||||
*/
|
||||
public function getIp(): string
|
||||
{
|
||||
if ($this->isV4())
|
||||
return $this->getIpv4();
|
||||
if ($this->isV6())
|
||||
return $this->getIpv6();
|
||||
|
||||
throw new NotIpAddressException($this->nodeName);
|
||||
}
|
||||
/**
|
||||
* Get the IPV4 address from the node name.
|
||||
* @return string - The IPV4 address.
|
||||
*/
|
||||
public function getIpv4(): string
|
||||
{
|
||||
// We try to find the port separator character.
|
||||
$portSeparator = strrpos($this->nodeName, ":");
|
||||
|
||||
return $portSeparator !== false
|
||||
// If there is a port separator character, we cut the string to keep only the IP address.
|
||||
? substr($this->nodeName, 0, $portSeparator)
|
||||
// If there is no port separator character, we can return the whole node name that should contain only the IP address.
|
||||
: $this->nodeName;
|
||||
}
|
||||
/**
|
||||
* Get the IPV6 address from the node name.
|
||||
* @return string - The IPV6 address.
|
||||
*/
|
||||
public function getIpv6(): string
|
||||
{
|
||||
return substr($this->nodeName, 1,
|
||||
// Finding the end of the IPV6 address. It is always enclosed in square brackets, according RFC 7239: https://datatracker.ietf.org/doc/html/rfc7239#section-6.1.
|
||||
strrpos($this->nodeName, "]") - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the used port from the node name.
|
||||
* @return int|null - The used port, if there is one specified.
|
||||
*/
|
||||
public function getPort(): ?int
|
||||
{
|
||||
// We try to find the port separator character.
|
||||
$portSeparator = strrpos($this->nodeName, ":",
|
||||
// In an IPV6 address, the port separator can only be found after the end of the IP address (always enclosed in square brackets, according RFC 7239: https://datatracker.ietf.org/doc/html/rfc7239#section-6.1).
|
||||
($ipv6EndPos = strrpos($this->nodeName, "]")) !== false ? $ipv6EndPos : 0);
|
||||
|
||||
// If we found a port separator, there is one and we can return it.
|
||||
return $portSeparator !== false ? intval(substr($this->nodeName, $portSeparator + 1)) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the current node interface is unknown.
|
||||
* @return bool - True if it is unknown, false otherwise.
|
||||
*/
|
||||
public function isUnknown(): bool
|
||||
{
|
||||
return $this->nodeType == ForwardNodeType::UNKNOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the current node interface is an obfuscated identifier.
|
||||
* @return bool - True if it is an obfuscated identifier, false otherwise.
|
||||
*/
|
||||
public function isIdentifier(): bool
|
||||
{
|
||||
return $this->nodeType == ForwardNodeType::IDENTIFIER;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current obfuscated identifier without the leading '_'.
|
||||
* @return string - The identifier, without the leading '_'.
|
||||
*/
|
||||
public function getIdentifier(): string
|
||||
{
|
||||
return substr($this->nodeName, 1);
|
||||
}
|
||||
}
|
31
src/ForwardNodeType.php
Normal file
31
src/ForwardNodeType.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Madeorsk\Forwarded;
|
||||
|
||||
/**
|
||||
* An enumeration of the possible types of nodes as defined in section 6 of RFC7239.
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc7239#section-6
|
||||
*/
|
||||
enum ForwardNodeType: string
|
||||
{
|
||||
/**
|
||||
* In the case of a node that is an IPV4, as defined in section 6.1.
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc7239#section-6.1
|
||||
*/
|
||||
case IPV4 = "ipv4";
|
||||
/**
|
||||
* In the case of a node that is an IPV6, as defined in section 6.1.
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc7239#section-6.1
|
||||
*/
|
||||
case IPV6 = "ipv6";
|
||||
/**
|
||||
* In the case of a node that is "unknown", as defined in section 6.2.
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc7239#section-6.2
|
||||
*/
|
||||
case UNKNOWN = "unknown";
|
||||
/**
|
||||
* In the case of a node that is an obfuscated identifier, as defined in section 6.3.
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc7239#section-6.3
|
||||
*/
|
||||
case IDENTIFIER = "identifier";
|
||||
}
|
71
src/Forwarded.php
Normal file
71
src/Forwarded.php
Normal file
@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace Madeorsk\Forwarded;
|
||||
|
||||
use Madeorsk\Forwarded\Exceptions\EmptyNodeNameException;
|
||||
|
||||
/**
|
||||
* The parsed Forwarded header.
|
||||
*/
|
||||
class Forwarded
|
||||
{
|
||||
/**
|
||||
* @var Forward[]
|
||||
*/
|
||||
protected array $forwards;
|
||||
|
||||
/**
|
||||
* Create a new Forwarded parsed header.
|
||||
* @param array<(array<string, string>|Forward)> $forwards - An array of forwards.
|
||||
* @throws EmptyNodeNameException
|
||||
*/
|
||||
public function __construct(array $forwards = [])
|
||||
{
|
||||
$this->loadForwards($forwards);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the given forwards array.
|
||||
* If the forwards array is an array of associative arrays, converting them as Forward objects.
|
||||
* @param array<(array<string, string>|Forward)> $forwards - An array of forwards.
|
||||
* @return void
|
||||
* @throws EmptyNodeNameException
|
||||
*/
|
||||
public function loadForwards(array $forwards): void
|
||||
{
|
||||
if (!empty($forwards))
|
||||
{ // If some forwards have been given, we read them and save them.
|
||||
$forwards = array_values($forwards); // Getting a simple forwards array with no index.
|
||||
if ($forwards[0] instanceof Forward)
|
||||
// If we have an array of Forward objects, we can simply save it.
|
||||
$this->forwards = $forwards;
|
||||
else
|
||||
// If we have an array of associative arrays for each forward,
|
||||
// we convert each forward associative array to a Forward object before to save the array.
|
||||
$this->forwards = array_map(function (array $assoc) {
|
||||
// Converting the current associative array as a Forward object.
|
||||
return new Forward($assoc);
|
||||
}, $forwards);
|
||||
}
|
||||
// If there are no forwards, reset to an empty array.
|
||||
else $this->forwards = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first forward, if there is one.
|
||||
* @return Forward|null - If NULL, there are no forward.
|
||||
*/
|
||||
public function first(): ?Forward
|
||||
{
|
||||
return !empty($this->forwards) ? $this->forwards[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the forwards list.
|
||||
* @return Forward[] - The forwards list.
|
||||
*/
|
||||
public function getForwards(): array
|
||||
{
|
||||
return $this->forwards;
|
||||
}
|
||||
}
|
223
src/Parser.php
Normal file
223
src/Parser.php
Normal file
@ -0,0 +1,223 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
86
tests/ParseTest.php
Normal file
86
tests/ParseTest.php
Normal file
@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace Madeorsk\Forwarded\Tests;
|
||||
|
||||
use Madeorsk\Forwarded\Parser;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ParseTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* The first example of Section 7.5 of RFC 7239.
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc7239#section-7.5
|
||||
* @return void
|
||||
*/
|
||||
public function testSimpleForward(): void
|
||||
{
|
||||
$forwarded = (new Parser())->parse("for=192.0.2.43");
|
||||
|
||||
$this->assertSame(1, count($forwarded->getForwards()));
|
||||
$this->assertSame("192.0.2.43", $forwarded->first()->for()->getIp());
|
||||
$this->assertNull($forwarded->first()->by());
|
||||
$this->assertNull($forwarded->first()->host());
|
||||
$this->assertNull($forwarded->first()->protocol());
|
||||
}
|
||||
|
||||
/**
|
||||
* The example of Section 7.1 of RFC 7239.
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc7239#section-7.1
|
||||
* @return void
|
||||
*/
|
||||
public function testExample71(): void
|
||||
{
|
||||
$forwarded = (new Parser())->parse("for=192.0.2.43,for=\"[2001:db8:cafe::17]\",for=unknown");
|
||||
|
||||
$this->assertSame(3, count($forwarded->getForwards()));
|
||||
$this->assertSame("192.0.2.43", $forwarded->first()->for()->getIp());
|
||||
$this->assertSame("2001:db8:cafe::17", $forwarded->getForwards()[1]->for()->getIp());
|
||||
$this->assertTrue($forwarded->getForwards()[2]->for()->isUnknown());
|
||||
}
|
||||
|
||||
public function testEverything(): void
|
||||
{
|
||||
$forwarded = (new Parser())->parse("for=192.0.2.43:55423;proto=http;host=test.dev;by=unknown,for=_something; by=unknown, for=\"[2001:db8:cafe::17]:22\";host=another.test;by=172.55.10.10,for=unknown");
|
||||
|
||||
// Testing that we read enough forwards.
|
||||
$this->assertSame(4, count($forwarded->getForwards()));
|
||||
|
||||
// Testing the first forward.
|
||||
$forward = $forwarded->getForwards()[0];
|
||||
$this->assertTrue($forward->for()->isIP());
|
||||
$this->assertTrue($forward->for()->isV4());
|
||||
$this->assertSame("192.0.2.43", $forward->for()->getIp());
|
||||
$this->assertSame(55423, $forward->for()->getPort());
|
||||
$this->assertTrue($forward->by()->isUnknown());
|
||||
$this->assertSame("test.dev", $forward->host());
|
||||
$this->assertSame("http", $forward->protocol());
|
||||
|
||||
// Testing the second forward.
|
||||
$forward = $forwarded->getForwards()[1];
|
||||
$this->assertTrue($forward->for()->isIdentifier());
|
||||
$this->assertSame("something", $forward->for()->getIdentifier());
|
||||
$this->assertTrue($forward->by()->isUnknown());
|
||||
$this->assertNull($forward->host());
|
||||
$this->assertNull($forward->protocol());
|
||||
|
||||
// Testing the third forward.
|
||||
$forward = $forwarded->getForwards()[2];
|
||||
$this->assertTrue($forward->for()->isIP());
|
||||
$this->assertTrue($forward->for()->isV6());
|
||||
$this->assertSame("2001:db8:cafe::17", $forward->for()->getIp());
|
||||
$this->assertSame(22, $forward->for()->getPort());
|
||||
$this->assertTrue($forward->by()->isIP());
|
||||
$this->assertTrue($forward->by()->isV4());
|
||||
$this->assertSame("172.55.10.10", $forward->by()->getIp());
|
||||
$this->assertNull($forward->by()->getPort());
|
||||
$this->assertSame("another.test", $forward->host());
|
||||
$this->assertNull($forward->protocol());
|
||||
|
||||
// Testing the fourth forward.
|
||||
$forward = $forwarded->getForwards()[3];
|
||||
$this->assertTrue($forward->for()->isUnknown());
|
||||
$this->assertNull($forward->by());
|
||||
$this->assertNull($forward->host());
|
||||
$this->assertNull($forward->protocol());
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user