<?php

namespace App\Classes\Protocol\DNS;

use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;

final class Query
{
	private const LOGKEY = 'PDQ';

	private string $buf;
	private int $class;
	private string $dns;
	private int $id;
	private int $type;

	private int $arcount;
	private int $qdcount;

	private RR $additional;

	private Collection $labels;

	// https://github.com/guyinatuxedo/dns-fuzzer/blob/master/dns.md
	private const header = [				// Struct of a DNS query
		'id'		=> [0x00,'n',1],		// ID
		'header'	=> [0x01,'n',1],		// Header
		'qdcount'	=> [0x02,'n',1],		// Entries in the question
		'ancount'	=> [0x03,'n',1],		// Resource Records in the answer
		'nscount'	=> [0x04,'n',1],		// Server Resource Records in the answer
		'arcount'	=> [0x05,'n',1],		// Resource Records in the addition records section
	];

	public function __construct(string $buf)
	{
		$this->buf = $buf;
		$rx_ptr = 0;

		// DNS Query header
		$header = unpack(self::unpackheader(self::header),$buf);
		$rx_ptr += $this->header_len();

		$this->id = $header['id'];
		$this->qdcount = $header['qdcount'];
		$this->arcount = $header['arcount'];
		$this->header = $header['header'];

		// Get the domain elements
		$this->labels = collect();

		while (($len=ord(substr($this->buf,$rx_ptr++,1))) !== 0x00) {
			$this->labels->push(strtolower(substr($this->buf,$rx_ptr,$len)));
			$rx_ptr += $len;
		}

		// Get the query type/class
		try {
			$result = unpack('ntype/nclass',substr($this->buf,$rx_ptr,4));

		} catch (\Exception $e) {
			Log::error(sprintf('%s:! Unpack failed: Buffer: [%s] (%d), RXPTR [%d]',self::LOGKEY,hex_dump($this->buf),strlen($this->buf),$rx_ptr));

			throw $e;
		}

		$rx_ptr += 4;
		$this->type = $result['type'];
		$this->class = $result['class'];

		$this->dns = substr($this->buf,$this->header_len(),$rx_ptr-$this->header_len());

		// Do we have additional records
		if ($this->arcount) {
			// Additional records, EDNS: https://datatracker.ietf.org/doc/html/rfc6891
			if (($haystack = strstr(substr($this->buf,$rx_ptr+1+10),"\x00",true)) !== FALSE) {
				Log::error(sprintf('%s:! DNS additional record format error?',self::LOGKEY),['buf'=>hex_dump($this->buf)]);
				return;
			}

			$this->additional = new RR(substr($this->buf,$rx_ptr,(strlen($haystack) === 0) ? NULL : strlen($haystack)));
			$rx_ptr += $this->additional->length;
		}

		if (strlen($this->buf) !== $rx_ptr)
			throw new \Exception(sprintf('! DNS Buffer still has [%d]: %s',strlen($this->buf)-$rx_ptr,hex_dump(substr($this->buf,$rx_ptr))));
	}

	public function __get($key)
	{
		switch ($key) {
			case 'class':
			case 'dns':
			case 'id':
			case 'labels':
			case 'qdcount':
			case 'arcount':
			case 'header':
			case 'type':
				return $this->{$key};

			case 'domain':
				return $this->labels->join('.');
		}
	}

	public static function header_len()
	{
		return collect(self::header)->sum(function($item) { return $item[2]*2; });
	}

	/**
	 * Unpack our configured DNS header
	 *
	 * @param array $pack
	 * @return string
	 */
	protected static function unpackheader(array $pack): string
	{
		return join('/',
			collect($pack)
				->sortBy(function($k,$v) {return $k[0];})
				->transform(function($k,$v) {return $k[1].$v;})
				->values()
				->toArray()
		);
	}
}