rtckit/php-pcap-ffi

View on GitHub
src/PcapFFI.php

Summary

Maintainability
A
35 mins
Test Coverage
<?php

declare(strict_types = 1);

namespace RTCKit\Pcap\Stream\FFI;

use FFI;
use FFI\CData;
use FFI\Exception as FFIException;

/**
 * FFI/libpcap class
 */
class PcapFFI
{
    public const LIBPCAP_NAME = 'libpcap.so.1';

    static private FFI $ffi;

    private ?string $error = null;

    public function __construct()
    {
        if (isset(self::$ffi)) {
            return;
        }

        $lib = getenv('LIBPCAP_NAME');
        if ($lib) {
            // old way
            $code = file_get_contents(__DIR__ . '/pcap.h');

            if ($code === false) {
                throw new FFIException('Cannot load pcap C definitions');
            }
            self::$ffi = FFI::cdef($code, $lib);
        } else {
            try {
                // Try preload
                self::$ffi = FFI::scope("_RTCKIT_PCAP_FFI_");
            } catch (FFIException $e) {
                // Try direct load
                self::$ffi = FFI::load(__DIR__ . '/pcap.h');
            }
        }
    }

    public function lib_version(): string
    {
        return self::$ffi->pcap_lib_version();
    }

    /**
     * Returns an array of sniffable network interfaces. The output might differ
     * from PHP's net_get_interfaces() as it's reflecting libpcap's perspective.
     *
     * @return ?array<mixed>
     */
    public function findalldevs(): ?array
    {
        $this->resetLastError();

        $devs = self::$ffi->new('pcap_if_t *');
        $dev = self::$ffi->new('pcap_if_t *');
        $err = self::$ffi->new('char[257]');

        if (self::$ffi->pcap_findalldevs(FFI::addr($devs), $err) < 0) {
            $this->setLastError(FFI::string($err));

            return null;
        }

        $ret = [];
        $dev = $devs;

        while (!is_null($dev)) {
            $ret[] = [
                'name' => FFI::string($dev->name),
            ];

            $dev = $dev->next;
        }

        self::$ffi->pcap_freealldevs($devs);

        return $ret;
    }

    public function create(string $dev): ?CData
    {
        $this->resetLastError();

        $err = self::$ffi->new('char[257]');
        $ret = self::$ffi->pcap_create($dev, $err);

        if (is_null($ret)) {
            $this->setLastError(FFI::string($err));

            return null;
        }

        return $ret;
    }

    public function set_snaplen(CData $pcap, int $snaplen): int
    {
        $this->resetLastError();

        $ret = self::$ffi->pcap_set_snaplen($pcap, $snaplen);

        if ($ret < 0) {
            $this->setLastPcapError($pcap);
        }

        return $ret;
    }

    public function set_promisc(CData $pcap, int $promisc): int
    {
        $this->resetLastError();

        $ret = self::$ffi->pcap_set_promisc($pcap, $promisc);

        if ($ret < 0) {
            $this->setLastPcapError($pcap);
        }

        return $ret;
    }

    public function set_immediate_mode(CData $pcap, int $immediate_mode): int
    {
        $this->resetLastError();

        $ret = self::$ffi->pcap_set_immediate_mode($pcap, $immediate_mode);

        if ($ret < 0) {
            $this->setLastPcapError($pcap);
        }

        return $ret;
    }

    public function set_timeout(CData $pcap, int $to_ms): int
    {
        $this->resetLastError();

        $ret = self::$ffi->pcap_set_timeout($pcap, $to_ms);

        if ($ret < 0) {
            $this->setLastPcapError($pcap);
        }

        return $ret;
    }

    public function setnonblock(CData $pcap, int $nonblock): int
    {
        $this->resetLastError();

        $err = self::$ffi->new('char[257]');
        $ret = self::$ffi->pcap_setnonblock($pcap, $nonblock, $err);

        if ($ret < 0) {
            $this->setLastError(FFI::string($err));
        }

        return $ret;
    }

    public function activate(CData $pcap): int
    {
        $this->resetLastError();

        $ret = self::$ffi->pcap_activate($pcap);

        if ($ret < 0) {
            $this->setLastPcapError($pcap);
        }

        return $ret;
    }

    public function get_selectable_fd(CData $pcap): int
    {
        return self::$ffi->pcap_get_selectable_fd($pcap);
    }

    public function inject(CData $pcap, string $data): int
    {
        return self::$ffi->pcap_inject($pcap, $data, strlen($data));
    }

    public function next_ex(CData $pcap): string
    {
        $this->resetLastError();

        $header = self::$ffi->new('struct pcap_pkthdr *');
        $data = self::$ffi->new('const u_char *');
        $next = self::$ffi->pcap_next_ex($pcap, FFI::addr($header), FFI::addr($data));

        if ($next < 0) {
            $this->setLastPcapError($pcap);
            return '';
        } else if (!$next) {
            return '';
        }

        $ret = pack('LLLL', $header[0]->ts->tv_sec, $header[0]->ts->tv_usec, $header[0]->caplen, $header[0]->len);
        $ret .= FFI::string($data, $header[0]->caplen);

        return $ret;
    }

    public function compile_setfilter(CData $pcap, string $filter): int
    {
        $this->resetLastError();

        $fp = self::$ffi->new('struct bpf_program');
        $ret = self::$ffi->pcap_compile($pcap, FFI::addr($fp), $filter, 0, 0);

        if ($ret < 0) {
            $this->setLastPcapError($pcap);
        } else {
            $ret = self::$ffi->pcap_setfilter($pcap, FFI::addr($fp));

            if ($ret < 0) {
                $this->setLastPcapError($pcap);
            }
        }

        return $ret;
    }

    public function close(CData $pcap): void
    {
        self::$ffi->pcap_close($pcap);
    }

    public function getLastError(): string
    {
        return is_null($this->error) ? '<unknown error>' : $this->error;
    }

    private function setLastError(string $error): void
    {
        $this->error = $error;
    }

    private function setLastPcapError(CData $pcap): void
    {
        $err = self::$ffi->pcap_geterr($pcap);

        if (!is_null($err)) {
            $this->setLastError(FFI::string($err));
        }
    }

    private function resetLastError(): void
    {
        $this->error = null;
    }
}