Dienstag, 18. Juni 2013

IP-Berechnung in PHP

Nun ist es wieder so weit, ich habe mir in PHP ein kleine Klasse gebastelt mit welcher ich alles Rund um IPv4 berechnen kann. Natürlich gibt es da noch das eine oder andere was man hinzufügen könnte aber mir reichen diese Functions:

Zuerst einmal ein Entity um die IP-Definition zu speichern:

class NetworkAddress {

    /**
     * @var string
     */
    private $ip;

    /**
     * @var integer
     */
    private $asInteger;

    function __construct($ip = null, $asInteger = null) {
        $this->setIp($ip);
        $this->setAsInteger($asInteger);
    }

    /**
     * Get id
     * @return integer
     */
    public function getId() {
        return $this->id;
    }

    /**
     * Set ip
     * @param string $ip
     * @return NetworkAddress
     */
    public function setIp($ip) {
        $this->ip = $ip;
        return $this;
    }

    /**
     * Get ip
     * @return string
     */
    public function getIp() {
        return $this->ip;
    }

    /**
     * Set asInteger
     * @param integer $asInteger
     * @return NetworkAddress
     */
    public function setAsInteger($asInteger) {
        $this->asInteger = floor((float) $asInteger);
        return $this;
    }

    /**
     * Get asInteger
     * @return integer
     */
    public function getAsInteger() {
        return (floor) $this->asInteger;
    }

}
Und hier die eigentlich Logik:
class Network {

    const MAX_IP_AS_INT = 4294967295; //255.255.255.255

    /**
     * korrigiert die inegabe von CIDR
     * @param int $cidr
     * @return int
     */

    static protected function fixCIDR($cidr) {
        return max(0, min(32, (int) $cidr)); //cidr zwischen 0 - 32
    }

    /**
     * @param integer $asInt
     * @return \NetworkAddress
     * @throws \Excpetion
     */
    static public function generateNetworkAddressByInteger($asInt) {
        $asInt = floor((float) $asInt);
        if ($asInt >= 0 && $asInt <= self::MAX_IP_AS_INT) {
            return new NetworkAddress(long2ip($asInt), $asInt);
        } else {
            throw new \Exception('Invalid integer range: ' . $asInt);
        }
    }

    /**
     * @param string $asString
     * @return \NetworkAddress
     * @throws \Excpetion
     */
    static public function generateNetworkAddressByString($asString) {
        if (filter_var($asString, FILTER_VALIDATE_IP)) {
            return new NetworkAddress($asString, ip2long($asString));
        } else {
            throw new \Excpetion('Invalid IP: ' . $asString);
        }
    }

    /**
     * gibt die erste adresse eines netzwerkes zurück.
     * @param \Network $network
     * @return \NetworkAddress
     */
    static public function getFirstHost(\SkyBurner\StoreBundle\Entity\Network $network) {
        return self::generateNetworkAddressByInteger($network->getNetaddressNetworkAddress()->getAsInteger() + 1);
    }

    /**
     *
     * @param int $cidr
     * @return \NetworkAddress
     */
    public function subnetMask($cidr) {
        $asInt = bindec(str_pad(str_repeat(1, self::fixCIDR($cidr)), 32, 0, STR_PAD_RIGHT));
        return self::generateNetworkAddressByInteger($asInt);
    }

    /**
     *
     * @param \NetworkAddress $netmask
     * @return int
     */
    public function subnetMaskToCidr(NetworkAddress $netmask) {
        return 32 - log(($netmask->getAsInteger() ^ self::MAX_IP_AS_INT) + 1, 2);
    }

    /**
     *
     * @param \NetworkAddress $ip
     * @param \NetworkAddress $netmask
     * @return \NetworkAddress
     */
    public function netAddress(NetworkAddress $ip, NetworkAddress $netmask) {
        $asInt = $ip->getAsInteger() & $netmask->getAsInteger();
        return self::generateNetworkAddressByInteger($asInt);
    }

    /**
     *
     * @param \NetworkAddress $netAddress
     * @param \NetworkAddress $netmask
     * @return \NetworkAddress
     */
    public function broadcast(NetworkAddress $netAddress, NetworkAddress $netmask) {
//        $ipAsLong = $netAddress->getAsInteger() | (~ $netmask->getAsInteger());
        $ipAsLong = $netAddress->getAsInteger() | $this->flipBin($netmask->getAsInteger());
        return self::generateNetworkAddressByInteger($asInt);
    }
    protected function flipBin($number) {
        $bin = str_pad(base_convert($number,10,2), 32, 0, STR_PAD_LEFT);
        for($i = 0; $i < 32; $i++) {
            $bin{$i} = $bin{$i} === '0' ? '1' : '0';
        }
        return bindec($bin);
    }
    /**
     *
     * @param int $cidr
     * @param \NetworkAddress $ip
     * @return \NetworkAddress
     */
    public function minIp($cidr, NetworkAddress $ip) {
        $netmask = $this->subnetMask($cidr);
        $netAddress = $this->netAddress($ip, $netmask);
        return self::generateNetworkAddressByInteger($netAddress->getAsInteger() + 1);
    }

    /**
     *
     * @param int $cidr
     * @param \NetworkAddress $ip
     * @return \NetworkAddress
     */
    public function maxIp($cidr, NetworkAddress $ip) {
        $netmask = $this->subnetMask($cidr);
        $netAddress = $this->netAddress($ip, $netmask);
        $broadcast = $this->broadcast($netAddress, $netmask);
        return self::generateNetworkAddressByInteger($broadcast->getAsInteger() - 1);
    }

    /**
     * @param int $cidr
     * @return int
     */
    public function countByCidr($cidr) {
        return max(0, pow(2, 32 - self::fixCIDR($cidr)) - 2);
    }

}

Zu beachten ist hier die interne verwendung von "FLOAT", da es bei "INT" probleme geben kann dass wir ein Überlauf erzeugen. Also solltet ihr diese Daten in die Datenbank speichern wollen achtet darauf ein "UNSIGNED INT" zu benutzen! Aus gleichem Grund habe ich eine Reimplementierung vom "Bitweisem NOT (~)" erstellt -> "flipBin".
Ich denke das ganze noch in ein passenden Namespace packen und gut is. Wer hierzu noch ein paar vorschläge hat, immer her damit!