S1lentium/IPTools

View on GitHub
src/Network.php

Summary

Maintainability
B
5 hrs
Test Coverage
<?php
namespace IPTools;

use IPTools\Exception\NetworkException;
use ReturnTypeWillChange;

/**
 * @author Safarov Alisher <alisher.safarov@outlook.com>
 * @link https://github.com/S1lentium/IPTools
 */
class Network implements \Iterator, \Countable
{
    use PropertyTrait;

    /**
     * @var IP
     */
    private $ip;
    /**
     * @var IP
     */
    private $netmask;
    /**
     * @var int
     */
    private $position = 0;

    /**
     * @param IP $ip
     * @param IP $netmask
     */
    public function __construct(IP $ip, IP $netmask)
    {
        $this->setIP($ip);
        $this->setNetmask($netmask);
    }

    /**
     *
     * @return string
     */
    public function __toString()
    {
        return $this->getCIDR();
    }

    /**
     * @param string $data
     * @return Network
     */
    public static function parse($data)
    {
        if (preg_match('~^(.+?)/(\d+)$~', $data, $matches)) {
            $ip      = IP::parse($matches[1]);
            $netmask = self::prefix2netmask((int)$matches[2], $ip->getVersion());
        } elseif (strpos($data,' ')) {
            list($ip, $netmask) = explode(' ', $data, 2);
            $ip      = IP::parse($ip);
            $netmask = IP::parse($netmask);
        } else {
            $ip      = IP::parse($data);
            $netmask = self::prefix2netmask($ip->getMaxPrefixLength(), $ip->getVersion());
        }

        return new self($ip, $netmask);
    }

    /**
     * @param int $prefixLength
     * @param string $version
     * @return IP
     * @throws NetworkException
     */
    public static function prefix2netmask($prefixLength, $version)
    {
        if (!in_array($version, array(IP::IP_V4, IP::IP_V6))) {
            throw new NetworkException("Wrong IP version");
        }

        $maxPrefixLength = $version === IP::IP_V4
            ? IP::IP_V4_MAX_PREFIX_LENGTH
            : IP::IP_V6_MAX_PREFIX_LENGTH;

        if (!is_numeric($prefixLength)
            || !($prefixLength >= 0 && $prefixLength <= $maxPrefixLength)
        ) {
            throw new NetworkException('Invalid prefix length');
        }

        $binIP = str_pad(str_pad('', (int)$prefixLength, '1'), $maxPrefixLength, '0');

        return IP::parseBin($binIP);
    }

    /**
     * @param IP ip
     * @return int
     */
    public static function netmask2prefix(IP $ip)
    {
        return strlen(rtrim($ip->toBin(), 0));
    }

    /**
     * @param IP ip
     * @throws NetworkException
     */
    public function setIP(IP $ip)
    {
        if (isset($this->netmask) && $this->netmask->getVersion() !== $ip->getVersion()) {
            throw new NetworkException('IP version is not same as Netmask version');
        }

        $this->ip = $ip;
    }

    /**
     * @param IP ip
     * @throws NetworkException
     */
    public function setNetmask(IP $ip)
    {
        if (!preg_match('/^1*0*$/',$ip->toBin())) {
            throw new NetworkException('Invalid Netmask address format');
        }

        if (isset($this->ip) && $ip->getVersion() !== $this->ip->getVersion()) {
            throw new NetworkException('Netmask version is not same as IP version');
        }

        $this->netmask = $ip;
    }

    /**
     * @param int $prefixLength
     */
    public function setPrefixLength($prefixLength)
    {
        $this->setNetmask(self::prefix2netmask((int)$prefixLength, $this->ip->getVersion()));
    }

    /**
     * @return IP
     */
    public function getIP()
    {
        return $this->ip;
    }

    /**
     * @return IP
     */
    public function getNetmask()
    {
        return $this->netmask;
    }

    /**
     * @return IP
     */
    public function getNetwork()
    {
        return new IP(inet_ntop($this->getIP()->inAddr() & $this->getNetmask()->inAddr()));
    }

    /**
     * @return int
     */
    public function getPrefixLength()
    {
        return self::netmask2prefix($this->getNetmask());
    }

    /**
     * @return string
     */
    public function getCIDR()
    {
        return sprintf('%s/%s', $this->getNetwork(), $this->getPrefixLength());
    }

    /**
     * @return IP
     */
    public function getWildcard()
    {
        return new IP(inet_ntop(~$this->getNetmask()->inAddr()));
    }

    /**
     * @return IP
     */
    public function getBroadcast()
    {
        return new IP(inet_ntop($this->getNetwork()->inAddr() | ~$this->getNetmask()->inAddr()));
    }

    /**
     * @return IP
     */
    public function getFirstIP()
    {
        return $this->getNetwork();
    }

    /**
     * @return IP
     */
    public function getLastIP()
    {
        return $this->getBroadcast();
    }

    /**
     * @return int|string
     */
    public function getBlockSize()
    {
        $maxPrefixLength = $this->ip->getMaxPrefixLength();
        $prefixLength = $this->getPrefixLength();

        if ($this->ip->getVersion() === IP::IP_V6) {
            return bcpow('2', (string)($maxPrefixLength - $prefixLength));
        }

        return pow(2, $maxPrefixLength - $prefixLength);
    }

    /**
     * @return Range
     */
    public function getHosts()
    {
        $firstHost = $this->getNetwork();
        $lastHost = $this->getBroadcast();

        if ($this->ip->getVersion() === IP::IP_V4) {
            if ($this->getBlockSize() > 2) {
                $firstHost = IP::parseBin(substr($firstHost->toBin(), 0, $firstHost->getMaxPrefixLength() - 1) . '1');
                $lastHost  = IP::parseBin(substr($lastHost->toBin(), 0, $lastHost->getMaxPrefixLength() - 1) . '0');
            }
        }

        return new Range($firstHost, $lastHost);
    }

    /**
     * @param IP|Network $exclude
     * @return Network[]
     * @throws NetworkException
     */
    public function exclude($exclude)
    {
        $exclude = self::parse($exclude);

        if (strcmp($exclude->getFirstIP()->inAddr() , $this->getLastIP()->inAddr()) > 0
            || strcmp($exclude->getLastIP()->inAddr() , $this->getFirstIP()->inAddr()) < 0
        ) {
            throw new NetworkException('Exclude subnet not within target network');
        }

        $networks = array();

        $newPrefixLength = $this->getPrefixLength() + 1;
        if ($newPrefixLength > $this->ip->getMaxPrefixLength()) {
            return $networks;
        }

        $lower = clone $this;
        $lower->setPrefixLength($newPrefixLength);

        $upper = clone $lower;
        $upper->setIP($lower->getLastIP()->next());

        while ($newPrefixLength <= $exclude->getPrefixLength()) {
            $range = new Range($lower->getFirstIP(), $lower->getLastIP());
            if ($range->contains($exclude)) {
                $matched   = $lower;
                $unmatched = $upper;
            } else {
                $matched   = $upper;
                $unmatched = $lower;
            }

            $networks[] = clone $unmatched;

            if (++$newPrefixLength > $this->getNetwork()->getMaxPrefixLength()) break;

            $matched->setPrefixLength($newPrefixLength);
            $unmatched->setPrefixLength($newPrefixLength);
            $unmatched->setIP($matched->getLastIP()->next());
        }

        sort($networks);

        return $networks;
    }

    /**
     * @param int $prefixLength
     * @return Network[]
     * @throws NetworkException
     */
    public function moveTo($prefixLength)
    {
        $maxPrefixLength = $this->ip->getMaxPrefixLength();

        if ($prefixLength <= $this->getPrefixLength() || $prefixLength > $maxPrefixLength) {
            throw new NetworkException('Invalid prefix length ');
        }

        $netmask = self::prefix2netmask($prefixLength, $this->ip->getVersion());
        $networks = array();

        $subnet = clone $this;
        $subnet->setPrefixLength($prefixLength);

        while ($subnet->ip->inAddr() <= $this->getLastIP()->inAddr()) {
            $networks[] = $subnet;
            $subnet = new self($subnet->getLastIP()->next(), $netmask);
        }

        return $networks;
    }

    /**
    * @return IP
    */
    #[ReturnTypeWillChange]
    public function current()
    {
        return $this->getFirstIP()->next($this->position);
    }

    /**
    * @return int
    */
    #[ReturnTypeWillChange]
    public function key()
    {
        return $this->position;
    }

    /**
     * @return void
     */
    #[ReturnTypeWillChange]
    public function next()
    {
        ++$this->position;
    }

    /**
     * @return void
     */
    #[ReturnTypeWillChange]
    public function rewind()
    {
        $this->position = 0;
    }

    /**
    * @return bool
    */
    #[ReturnTypeWillChange]
    public function valid()
    {
        return strcmp($this->getFirstIP()->next($this->position)->inAddr(), $this->getLastIP()->inAddr()) <= 0;
    }

    /**
    * @return int
    */
    #[ReturnTypeWillChange]
    public function count()
    {
        return (integer)$this->getBlockSize();
    }

}