HEX
Server: LiteSpeed
System: Linux s1049.use1.mysecurecloudhost.com 4.18.0-477.27.2.lve.el8.x86_64 #1 SMP Wed Oct 11 12:32:56 UTC 2023 x86_64
User: xedaptot (3356)
PHP: 8.3.31
Disabled: NONE
Upload Files
File: /home/xedaptot/be.naniguide.com/vendor/louis/spfcheck/src/SPFCheck.php
<?php
/**
 *
 * @author Mikael Peigney
 */

namespace Mika56\SPFCheck;


use Mika56\SPFCheck\Exception\DNSLookupException;
use Mika56\SPFCheck\Exception\DNSLookupLimitReachedException;
use Symfony\Component\HttpFoundation\IpUtils;

class SPFCheck
{
    const RESULT_PASS = '+';
    const RESULT_FAIL = '-';
    const RESULT_SOFTFAIL = '~';
    const RESULT_NEUTRAL = '?';
    const RESULT_NONE = 'NO';
    const RESULT_PERMERROR = 'PE';
    const RESULT_TEMPERROR = 'TE';
    const RESULT_DEFINITIVE_PERMERROR = 'DPE'; // Special result for recursion limit, that cannot be ignored and is transformed to PERMERROR

    /**
     * Stores any "redirect" modifier value for later redirection
     * @var string|null
     */
    private $redirect;
    private $voidLookup = 0;

    protected static function getValidResults()
    {
        return [self::RESULT_PASS, self::RESULT_FAIL, self::RESULT_SOFTFAIL, self::RESULT_NEUTRAL];
    }

    const MECHANISM_ALL = 'all';
    const MECHANISM_IP4 = 'ip4';
    const MECHANISM_IP6 = 'ip6';
    const MECHANISM_A = 'a';
    const MECHANISM_MX = 'mx';
    const MECHANISM_PTR = 'ptr';
    const MECHANISM_EXISTS = 'exists';
    const MECHANISM_INCLUDE = 'include';
    const MODIFIER_REDIRECT = 'redirect';
    const MODIFIER_EXP = 'exp';

    /** @var  DNSRecordGetterInterface */
    protected $DNSRecordGetter;

    /**
     * SPFCheck constructor.
     * @param DNSRecordGetterInterface $DNSRecordGetter
     */
    public function __construct(DNSRecordGetterInterface $DNSRecordGetter)
    {
        $this->DNSRecordGetter = $DNSRecordGetter;
    }

    /**
     * @param string $ipAddress The IP address to be tested
     * @param string $domain The domain to test the IP address against
     * @return string
     */
    public function isIPAllowed($ipAddress, $domain)
    {
        return $this->doIsIPAllowed($ipAddress, $domain, true);
    }

    protected function doIsIPAllowed($ipAddress, $domain, $resetRequestCount)
    {
        if (!$domain) {
            return self::RESULT_NONE;
        }

        $this->redirect = null;
        if ($resetRequestCount) {
            $this->voidLookup = 0;
            $this->DNSRecordGetter->resetRequestCounts();
        }

        // Handle IPv4 address in IPv6 format
        if (preg_match('/^(:|0000:0000:0000:0000:0000):FFFF:/i', $ipAddress)) {
            $ipAddress = strrev(explode(':', strrev($ipAddress), '2')[0]);
        }

        $result = $this->doCheck($ipAddress, $domain);
        if ($result == self::RESULT_DEFINITIVE_PERMERROR) {
            $result = self::RESULT_PERMERROR;
        }

        return $result;
    }

    /**
     * @param $ipAddress
     * @param $domain
     * @return bool|string
     * @throws DNSLookupException
     */
    private function doCheck($ipAddress, $domain)
    {
        try {
            $spfRecords = $this->DNSRecordGetter->getSPFRecordForDomain($domain);
        } catch (DNSLookupException $e) {
            return self::RESULT_TEMPERROR;
        }

        if (count($spfRecords) == 0) {
            return self::RESULT_NONE;
        }
        if (count($spfRecords) > 1) {
            return self::RESULT_PERMERROR;
        }
        $spfRecord = $spfRecords[0];
        if (!self::isSPFValid($spfRecord)) {
            return self::RESULT_PERMERROR;
        }

        $recordParts = explode(' ', $spfRecord);
        array_shift($recordParts); // Remove first part (v=spf1)
        if (count($recordParts) == 0) {
            $recordParts = array('?all');
        }

        if (in_array("include:{$ipAddress}", $recordParts)) {
            return self::RESULT_PASS;
        }

        $result = false;

        foreach ($recordParts as $recordPart) {
            try {
                if (false !== ($result = $this->ipMatchesPart($ipAddress, $recordPart, $domain))) {
                    return $result;
                }
            } catch (DNSLookupLimitReachedException $e) {
                return self::RESULT_DEFINITIVE_PERMERROR;
            }
        }

        if ($result === false && $this->redirect) {
            $result = $this->doIsIPAllowed($ipAddress, $this->redirect, false);
            // In a redirect, if no SPF is found or if domain does not exist, it should return PermError (RFC4408 6.1/4)
            if ($result == self::RESULT_NONE) {
                $result = self::RESULT_PERMERROR;
            }

            return $result;
        }

        return self::RESULT_NEUTRAL;
    }

    /**
     * @param $ipAddress
     * @param $part
     * @param $matchingDomain
     * @return bool
     * @throws DNSLookupLimitReachedException
     * @throws DNSLookupException
     */
    protected function ipMatchesPart($ipAddress, $part, $matchingDomain)
    {
        $qualifier = substr($part, 0, 1);
        if (!in_array($qualifier, self::getValidResults())) {
            $qualifier = self::RESULT_PASS;
            $condition = $part;
        } else {
            $condition = substr($part, 1);
        }

        $operandOption = $operand = null;
        if (1 == preg_match('`:|=`', $condition)) {
            list($mechanism, $operand) = preg_split('`:|=`', $condition, 2);
        } elseif (false !== stripos($condition, '/')) {
            list($mechanism, $operandOption) = explode('/', $condition, 2);
        } else {
            $mechanism = $condition;
        }

        switch ($mechanism) {
            case self::MECHANISM_ALL:
                return $qualifier;
                break;

            /** @noinspection PhpMissingBreakStatementInspection */
            case self::MECHANISM_IP4:
                if (false === stripos($operand, '/')) {
                    $operand .= '/32';
                }
            case self::MECHANISM_IP6:
                if (false === stripos($operand, '/')) {
                    $operand .= '/128';
                }

                // CIDR 0 matches any IP Address
                list(, $cidr) = explode('/', $operand, 2);
                if (isset($cidr) && $cidr == 0) {
                    return $qualifier;
                }
                if (IpUtils::checkIp($ipAddress, $operand)) {
                    return $qualifier;
                }
                break;

            case self::MECHANISM_A:
                $domain = $operand ? $operand : $matchingDomain;
                if (false !== stripos($domain, '/')) {
                    list($domain, $cidr) = explode('/', $domain);
                }
                if (!is_null($operandOption)) {
                    $cidr = $operandOption;
                }
                if (isset($cidr) && !is_numeric($cidr)) {
                    // If cidr is not numeric, then it's not a cidr
                    // RFC 4408 allow any character, including /, in a domain name
                    $domain .= '/'.$cidr;
                    unset($cidr);
                }
                $this->DNSRecordGetter->countRequest();
                $validIpAddresses = $this->DNSRecordGetter->resolveA($domain);
                if (count($validIpAddresses) == 0) {
                    try {
                        $this->countVoidLookup();
                    } catch (DNSLookupException $e) {
                        return self::RESULT_PERMERROR;
                    }
                }
                if (isset($cidr)) {
                    foreach ($validIpAddresses as &$validIpAddress) {
                        $validIpAddress .= '/'.$cidr;
                    }
                }

                // CIDR 0 matches any IP Address
                if (isset($cidr) && $cidr == 0 && count($validIpAddresses) > 0) {
                    return $qualifier;
                }

                if (IpUtils::checkIp($ipAddress, $validIpAddresses)) {
                    return $qualifier;
                }
                break;

            case self::MECHANISM_MX:
                $domain = $operand ? $operand : $matchingDomain;
                if (false !== stripos($domain, '/')) {
                    list($domain, $cidr) = explode('/', $domain);
                }
                if (!is_null($operandOption)) {
                    $cidr = $operandOption;
                }
                if (isset($cidr) && !is_numeric($cidr)) {
                    // If cidr is not numeric, then it's not a cidr
                    // RFC 4408 allow any character, including /, in a domain name
                    $domain .= '/'.$cidr;
                    unset($cidr);
                }

                $validIpAddresses = [];
                $this->DNSRecordGetter->countRequest();
                $mxServers = $this->DNSRecordGetter->resolveMx($domain);
                foreach ($mxServers as $mxServer) {
                    $this->DNSRecordGetter->countMxRequest();
                    if (false !== filter_var($mxServer, FILTER_VALIDATE_IP)) {
                        $validIpAddresses[] = $mxServer;
                    } else {
                        foreach ($this->DNSRecordGetter->resolveA($mxServer) as $mxIpAddress) {
                            $validIpAddresses[] = $mxIpAddress;
                        }
                    }
                }
                if (isset($cidr)) {
                    foreach ($validIpAddresses as &$validIpAddress) {
                        $validIpAddress .= '/'.$cidr;
                    }
                }

                // CIDR 0 matches any IP Address
                if (isset($cidr) && $cidr == 0 && count($validIpAddresses) > 0) {
                    return $qualifier;
                }

                if (IpUtils::checkIp($ipAddress, $validIpAddresses)) {
                    return $qualifier;
                }
                break;

            case self::MECHANISM_PTR:
                $domain = $operand ? $operand : $matchingDomain;

                $this->DNSRecordGetter->countRequest();
                $ptrRecords                  = $this->DNSRecordGetter->resolvePtr($ipAddress);
                $validatedSendingDomainNames = array();
                foreach ($ptrRecords as $ptrRecord) {
                    $this->DNSRecordGetter->countPtrRequest();
                    $ptrRecord   = strtolower($ptrRecord);
                    $ipAddresses = $this->DNSRecordGetter->resolveA($ptrRecord);
                    if (in_array($ipAddress, $ipAddresses)) {
                        $validatedSendingDomainNames[] = $ptrRecord;
                    }
                }

                foreach ($validatedSendingDomainNames as $name) {
                    if ($name == $domain || substr($name, -strlen($domain)) == $domain) {
                        return $qualifier;
                    }
                }
                break;

            case self::MECHANISM_EXISTS:
                try {
                    if ($this->DNSRecordGetter->exists($operand)) {
                        return $qualifier;
                    }
                } catch (DNSLookupException $e) {
                    return self::RESULT_TEMPERROR;
                }
                break;

            case self::MECHANISM_INCLUDE:
                $this->DNSRecordGetter->countRequest();
                $includeResult = $this->doCheck($ipAddress, $operand);
                if (in_array($includeResult, array(self::RESULT_PASS, self::RESULT_DEFINITIVE_PERMERROR, self::RESULT_PERMERROR, self::RESULT_TEMPERROR))) {
                    return $includeResult;
                }
                if ($includeResult == self::RESULT_NONE) {
                    return self::RESULT_PERMERROR;
                }
                break;
            case self::MODIFIER_REDIRECT:
                $this->DNSRecordGetter->countRequest();
                $this->redirect = $operand;

                return false;
                break;
            case '':
                // If a SPF record contains multiple spaces between parts, this should not be considered as an unknown mechanism
                break;
            default:
                // This can't be a mechanism as we checked before if SPF syntax was correct (and mechanisms valid)
                // We are certain this is a modifier. Any string is allowed as a modifier, but that doesn't make it match
                return false;
                break;
        }

        return false;
    }

    private static function isSPFValid($spfRecord)
    {
        if (preg_match('/^v=spf1( +([-+?~]?(all|include:(%\{[CDHILOPR-Tcdhilopr-t]([1-9][0-9]?|10[0-9]|11[0-9]|12[0-8])?r?[+-\/=_]*\}|%%|%_|%-|[!-$&-~])*(\.([A-Za-z]|[A-Za-z]([-0-9A-Za-z]?)*[0-9A-Za-z])|%\{[CDHILOPR-Tcdhilopr-t]([1-9][0-9]?|10[0-9]|11[0-9]|12[0-8])?r?[+-\/=_]*\})|a(:(%\{[CDHILOPR-Tcdhilopr-t]([1-9][0-9]?|10[0-9]|11[0-9]|12[0-8])?r?[+-\/=_]*\}|%%|%_|%-|[!-$&-~])*(\.([A-Za-z]|[A-Za-z]([-0-9A-Za-z]?)*[0-9A-Za-z])|%\{[CDHILOPR-Tcdhilopr-t]([1-9][0-9]?|10[0-9]|11[0-9]|12[0-8])?r?[+-\/=_]*\}))?((\/(\d|1\d|2\d|3[0-2]))?(\/\/([1-9][0-9]?|10[0-9]|11[0-9]|12[0-8]))?)?|mx(:(%\{[CDHILOPR-Tcdhilopr-t]([1-9][0-9]?|10[0-9]|11[0-9]|12[0-8])?r?[+-\/=_]*\}|%%|%_|%-|[!-$&-~])*(\.([A-Za-z]|[A-Za-z]([-0-9A-Za-z]?)*[0-9A-Za-z])|%\{[CDHILOPR-Tcdhilopr-t]([1-9][0-9]?|10[0-9]|11[0-9]|12[0-8])?r?[+-\/=_]*\}))?((\/(\d|1\d|2\d|3[0-2]))?(\/\/([1-9][0-9]?|10[0-9]|11[0-9]|12[0-8]))?)?|ptr(:(%\{[CDHILOPR-Tcdhilopr-t]([1-9][0-9]?|10[0-9]|11[0-9]|12[0-8])?r?[+-\/=_]*\}|%%|%_|%-|[!-$&-~])*(\.([A-Za-z]|[A-Za-z]([-0-9A-Za-z]?)*[0-9A-Za-z])|%\{[CDHILOPR-Tcdhilopr-t]([1-9][0-9]?|10[0-9]|11[0-9]|12[0-8])?r?[+-\/=_]*\}))?|ip4:([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|1[0-9]|2[0-9]|3[0-2]))?|ip6:(::|([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}|([0-9A-Fa-f]{1,4}:){1,8}:|([0-9A-Fa-f]{1,4}:){7}:[0-9A-Fa-f]{1,4}|([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}){1,2}|([0-9A-Fa-f]{1,4}:){5}(:[0-9A-Fa-f]{1,4}){1,3}|([0-9A-Fa-f]{1,4}:){4}(:[0-9A-Fa-f]{1,4}){1,4}|([0-9A-Fa-f]{1,4}:){3}(:[0-9A-Fa-f]{1,4}){1,5}|([0-9A-Fa-f]{1,4}:){2}(:[0-9A-Fa-f]{1,4}){1,6}|[0-9A-Fa-f]{1,4}:(:[0-9A-Fa-f]{1,4}){1,7}|:(:[0-9A-Fa-f]{1,4}){1,8}|([0-9A-Fa-f]{1,4}:){6}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])|([0-9A-Fa-f]{1,4}:){6}:([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])|([0-9A-Fa-f]{1,4}:){5}:([0-9A-Fa-f]{1,4}:)?([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])|([0-9A-Fa-f]{1,4}:){4}:([0-9A-Fa-f]{1,4}:){0,2}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])|([0-9A-Fa-f]{1,4}:){3}:([0-9A-Fa-f]{1,4}:){0,3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])|([0-9A-Fa-f]{1,4}:){2}:([0-9A-Fa-f]{1,4}:){0,4}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])|[0-9A-Fa-f]{1,4}::([0-9A-Fa-f]{1,4}:){0,5}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])|::([0-9A-Fa-f]{1,4}:){0,6}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))(\/(\d{1,2}|10[0-9]|11[0-9]|12[0-8]))?|exists:(%\{[CDHILOPR-Tcdhilopr-t]([1-9][0-9]?|10[0-9]|11[0-9]|12[0-8])?r?[+-\/=_]*\}|%%|%_|%-|[!-$&-~])*(\.([A-Za-z]|[A-Za-z]([-0-9A-Za-z]?)*[0-9A-Za-z])|%\{[CDHILOPR-Tcdhilopr-t]([1-9][0-9]?|10[0-9]|11[0-9]|12[0-8])?r?[+-\/=_]*\}))|redirect=(%\{[CDHILOPR-Tcdhilopr-t]([1-9][0-9]?|10[0-9]|11[0-9]|12[0-8])?r?[+-\/=_]*\}|%%|%_|%-|[!-$&-~])*(\.([A-Za-z]|[A-Za-z]([-0-9A-Za-z]?)*[0-9A-Za-z])|%\{[CDHILOPR-Tcdhilopr-t]([1-9][0-9]?|10[0-9]|11[0-9]|12[0-8])?r?[+-\/=_]*\})|exp=(%\{[CDHILOPR-Tcdhilopr-t]([1-9][0-9]?|10[0-9]|11[0-9]|12[0-8])?r?[+-\/=_]*\}|%%|%_|%-|[!-$&-~])*(\.([A-Za-z]|[A-Za-z]([-0-9A-Za-z]?)*[0-9A-Za-z])|%\{[CDHILOPR-Tcdhilopr-t]([1-9][0-9]?|10[0-9]|11[0-9]|12[0-8])?r?[+-\/=_]*\})|[A-Za-z][-.0-9A-Z_a-z]*=(%\{[CDHILOPR-Tcdhilopr-t]([1-9][0-9]?|10[0-9]|11[0-9]|12[0-8])?r?[+-\/=_]*\}|%%|%_|%-|[!-$&-~])*))* *$/i',
                $spfRecord) == 1
        ) {

            $recordParts = explode(' ', $spfRecord);
            array_shift($recordParts); // Remove first part (v=spf1)

            // RFC4408 6/2: each modifier can only appear once
            $redirectCount = 0;
            $expCount      = 0;
            foreach ($recordParts as $recordPart) {
                if (false !== strpos($recordPart, '=')) {
                    list($modifier, $domain) = explode('=', $recordPart, 2);
                    $expOrRedirect = false;
                    if ($modifier == self::MODIFIER_REDIRECT || substr($modifier, 1) == self::MODIFIER_REDIRECT) {
                        $redirectCount++;
                        $expOrRedirect = true;
                    }
                    if ($modifier == self::MODIFIER_EXP || substr($modifier, 1) == self::MODIFIER_EXP) {
                        $expCount++;
                        $expOrRedirect = true;
                    }
                    if ($expOrRedirect) {
                        if (empty($domain)) {
                            return false;
                        } else {
                            if (preg_match('/^[+-?~](all|a|mx|ptr|ip4|ip6|exists):?.*$/', $domain)) {
                                return false;
                            }
                            if (!preg_match('/^(((?!-))(xn--)?[a-z0-9-_]{0,61}[a-z0-9]{1,1}\.)*(xn--)?([a-z0-9\-]{1,61}|[a-z0-9-]{1,30}\.[a-z]{2,})$/i', $domain)) {
                                return false;
                            }
                        }
                    }
                }
            }
            if ($redirectCount > 1 || $expCount > 1) {
                return false;
            }

            return true;
        }

        return false;
    }

    protected function countVoidLookup()
    {
        if (++$this->voidLookup > 2) {
            throw new DNSLookupException();
        }
    }
}